package main import ( "flag" "log" "os" "path/filepath" "sync" "gioui.org/app" "gioui.org/op" "gioui.org/text" "gioui.org/unit" "gioui.org/font/gofont" "gioui.org/io/key" "pad/internal/editor" "pad/internal/io/pool/real" "pad/internal/ui" ) func main() { go func() { w := new(app.Window) w.Option(app.Title("Pad")) w.Option(app.Size(unit.Dp(390), unit.Dp(844))) if err := run(w); err != nil { log.Fatal(err) } os.Exit(0) }() app.Main() } func run(w *app.Window) error { log.Printf("run: starting") var ops op.Ops shaper := text.NewShaper(text.WithCollection(gofont.Collection())) // Determine root directory for real filesystem rootDir := flag.String("root", startpath, "root directory for the filesystem") flag.Parse() abs, err := filepath.Abs(*rootDir) if err != nil { log.Fatalf("invalid root directory: %v", err) } fs := &real.RealFileSystem{Root: abs} log.Printf("using filesystem at %s", abs) logic := editor.NewLogic(fs) renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State()) var mu sync.Mutex var elems []ui.Element var curScale, newScale float32 log.Printf("run: starting frameReceiver") go frameReceiver(w, &mu, &elems, logic.FrameChan()) log.Printf("run: starting logic.Run") go logic.Run() for { switch e := w.Event().(type) { case app.DestroyEvent: return e.Err case app.ConfigEvent: // ConfigEvent: raw pixel dimensions only. logic.ConfigChan() <- editor.ConfigEvent{ PixelWidth: e.Config.Size.X, PixelHeight: e.Config.Size.Y, } case app.FrameEvent: log.Printf("run: FrameEvent") gtx := app.NewContext(&ops, e) newScale = gtx.Metric.PxPerDp curScale = logic.State().Scale() // Deadlock risk: ensure no channel sends while lock is held mu.Lock() currentElems := elems renderer.Draw(gtx, currentElems) glyphLayout := renderer.GlyphLayout() // Send search query update to the logic goroutine when it changes. // The logic goroutine handles filtering and triggers a new frame. newQuery := logic.State().Browser.SearchEditor.Text() sendQuery := false if newQuery != logic.State().Browser.Query { sendQuery = true } events := renderer.CheckGestures(e.Source, gtx.Metric) // Gather key events focusedID := logic.State().FocusedElementID log.Printf("focusedID = %s", focusedID) if focusedID != "" { if reg, ok := renderer.Keys[focusedID]; ok { log.Printf("looking for events") // Use key.Filter to only receive events destined for the focused element. // We need both Key events (for arrow keys) and Edit events (for text input). // In Gio, key.Filter covers key presses, while key.FocusFilter covers // focus and text edit events. for { // Filter for key events and focus/edit events targeted at focusedID evt, ok := gtx.Event(key.Filter{Focus: focusedID}, key.FocusFilter{Target: focusedID}) if !ok { break } log.Printf("found an event: %T", evt) switch k := evt.(type) { case key.Event: log.Printf("key event: %v state=%v", k.Name, k.State) if k.State == key.Press { events = append(events, ui.InputEvent{ Handler: reg.Handler, Data: k.Name, }) } case key.EditEvent: log.Printf("edit event: %v", k.Text) events = append(events, ui.InputEvent{ Handler: reg.Handler, Data: k, }) } } } } e.Frame(&ops) mu.Unlock() if newScale != curScale { logic.ConfigChan() <- editor.ScaleEvent{newScale} } if len(events) > 0 { logic.InputChan() <- events } if sendQuery { logic.SearchQueryChan() <- newQuery } logic.LayoutChan() <- glyphLayout } } } func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) { log.Printf("frameReceiver: loop starting") for { frame := <-frameChan log.Printf("frameReceiver: received frame") mu.Lock() *elems = frame w.Invalidate() mu.Unlock() } }