Drop the log.Printf/fmt.Printf traces that fired on every config/input/ click/scroll/open/worker-result during IME and tap debugging. These were noise (and some were commented out). Error, limit, and recovery logs are kept; the default-off profiler and gated IME debug remain untouched.
207 lines
5.7 KiB
Go
207 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/io/key"
|
|
"gioui.org/op"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
|
|
"pad/internal/editor"
|
|
"pad/internal/io/pool/real"
|
|
"pad/internal/perf"
|
|
"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 {
|
|
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)
|
|
|
|
// In-app frame profiler (default off). Enabled by the presence of a marker
|
|
// file so the shipping APK needs no rebuild to toggle profiling. When on,
|
|
// it records logic-frame cadence plus scroll/visible-range context to a CSV
|
|
// and logs rolling percentiles; it also enables the scroll-jump debug poller.
|
|
const perfDir = "/storage/emulated/0/PadPerf"
|
|
var prof *perf.Profiler
|
|
var perfOn bool
|
|
var presentN int
|
|
var presentStart time.Time
|
|
if _, err := os.Stat(filepath.Join(perfDir, "enable")); err == nil {
|
|
prof = perf.New(true, perfDir, "logic_frames.csv")
|
|
editor.PerfRecord = func(rec editor.ProbeRecord) {
|
|
prof.Record(perf.Ctx{
|
|
Page: rec.Page,
|
|
ScrollDP: rec.ScrollDP,
|
|
MaxScrollDP: rec.MaxScrollDP,
|
|
TotalLines: rec.TotalLines,
|
|
VisStart: rec.VisStart,
|
|
VisEnd: rec.VisEnd,
|
|
})
|
|
}
|
|
logic.EnableDebugCmdPoll(perfDir)
|
|
perfOn = true
|
|
log.Printf("PERF: enabled (dir=%s)", perfDir)
|
|
}
|
|
|
|
// 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
|
|
|
|
go frameReceiver(w, &mu, &frame, logic.FrameChan())
|
|
go logic.Run()
|
|
|
|
for {
|
|
switch e := w.Event().(type) {
|
|
case app.DestroyEvent:
|
|
if prof != nil {
|
|
prof.Stop()
|
|
}
|
|
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:
|
|
if perfOn {
|
|
presentN++
|
|
if presentStart.IsZero() {
|
|
presentStart = time.Now()
|
|
}
|
|
if time.Since(presentStart) >= 2*time.Second {
|
|
dt := time.Since(presentStart).Seconds()
|
|
log.Printf("PERF-PRESENT frames=%d fps=%.1f", presentN, float64(presentN)/dt)
|
|
presentN = 0
|
|
presentStart = time.Time{}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
switch k := evt.(type) {
|
|
case key.Event:
|
|
if k.State == key.Press {
|
|
events = append(events, ui.InputEvent{
|
|
Handler: reg.Handler,
|
|
Data: k.Name,
|
|
})
|
|
}
|
|
case key.EditEvent:
|
|
events = append(events, ui.InputEvent{
|
|
Handler: reg.Handler,
|
|
Data: k,
|
|
})
|
|
case key.SnippetEvent:
|
|
// 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:
|
|
handleEvent(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func frameReceiver(w *app.Window, mu *sync.Mutex, frame *editor.Frame, frameChan <-chan editor.Frame) {
|
|
for {
|
|
f := <-frameChan
|
|
mu.Lock()
|
|
*frame = f
|
|
w.Invalidate()
|
|
mu.Unlock()
|
|
}
|
|
}
|