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" "gioui.org/widget" "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() // Initialize the real filesystem rooted at the system root "/" // so that the browser can navigate the entire system. fs := real.NewRealFileSystem("/") // Get the absolute path of the startup directory startAbs, err := filepath.Abs(*rootDir) if err != nil { startAbs = *rootDir } log.Printf("using filesystem at / (startup directory: %s)", startAbs) logic := editor.NewLogic(fs, startAbs, OpenFile) renderer := ui.New(ui.Theme{FontSize: 14}, shaper) // The search bar's widget.Editor is owned by the MAIN goroutine: Gio // mutates it during draw, and the logic goroutine must never touch it // (architecture.md §1). Its text is forwarded to the logic goroutine via // SearchQueryChan; the logic only stores the result in Browser.Query. var searchEditor widget.Editor renderer.RegisterGioEditor("search_bar", &searchEditor) // frame is the frame-receiver-stored handoff (architecture.md §2.2/§9): // the ONLY data the main goroutine reads from the logic side. var mu sync.Mutex var frame editor.Frame log.Printf("run: starting frameReceiver") go frameReceiver(w, &mu, &frame, logic.FrameChan()) log.Printf("run: starting logic.Run") go logic.Run() for { switch e := w.Event().(type) { case app.DestroyEvent: logic.Shutdown() 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: gtx := app.NewContext(&ops, e) newScale := gtx.Metric.PxPerDp // Read ONLY the frame-receiver-stored snapshot; the main goroutine // never touches logic State (architecture.md §1). mu.Lock() curScale := frame.Scale if curScale <= 0 { curScale = 1 // no frame yet } renderer.Draw(gtx, frame.Elems, curScale) 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 := searchEditor.Text() sendQuery := newQuery != frame.Query events := renderer.CheckGestures(e.Source, gtx.Metric) // Gather key events focusedID := frame.FocusedElementID if focusedID != "" { if reg, ok := renderer.Keys[focusedID]; ok { // 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 text: %q", k.Text) events = append(events, ui.InputEvent{ Handler: reg.Handler, Data: k, }) case key.SnippetEvent: //log.Printf("snippet event: %v", k) // Handle snippet event if necessary, or ignore default: log.Printf("unexpected event type: %T", k) } } } } e.Frame(&ops) mu.Unlock() if newScale != curScale { logic.ConfigChan() <- editor.ScaleEvent{Scale: newScale} } if len(events) > 0 { logic.InputChan() <- events } if sendQuery { logic.SearchQueryChan() <- newQuery } logic.LayoutChan() <- glyphLayout default: //log.Printf("pad: default event") handleEvent(e) } } } func frameReceiver(w *app.Window, mu *sync.Mutex, frame *editor.Frame, frameChan <-chan editor.Frame) { log.Printf("frameReceiver: loop starting") for { f := <-frameChan //log.Printf("frameReceiver: received frame") mu.Lock() *frame = f w.Invalidate() mu.Unlock() } }