Pad/cmd/pad/main.go
Greg Pomerantz 8f185341b3 Implement per-pixel smooth scrolling for ListView
- Changed BrowserScrollOffset and ListView.ScrollOffset from int (row index)
  to ui.Dp (pixel offset) for smooth per-pixel scrolling
- Updated HandleBrowserScroll to use raw Dp delta instead of row-based
  scrolling with minimum ±1 row jumps
- Fixed search not triggering new frames by detecting query changes in
  main.go FrameEvent handler
- Fixed click misalignment by indexing RowFilenames with local index i
  instead of rowGlobalIndex
- Fixed bottom clamping using actual BrowserListHeight and corrected
  formula: maxScroll = totalRows * rowHeight - BrowserListHeight
2026-05-28 16:14:11 -04:00

87 lines
2.0 KiB
Go

package main
import (
"log"
"os"
"sync"
"gioui.org/app"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/font/gofont"
"pad/internal/editor"
"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()))
logic := editor.NewLogic()
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
var mu sync.Mutex
var elems []ui.Element
go logic.Run()
go frameReceiver(w, &mu, &elems, logic.FrameChan())
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:
gtx := app.NewContext(&ops, e)
newScale := gtx.Metric.PxPerDp
curScale := logic.State().Scale()
if newScale != curScale {
logic.ConfigChan() <- editor.ScaleEvent{newScale}
}
mu.Lock()
currentElems := elems
renderer.Draw(gtx, currentElems)
logic.DisplayLineChan() <- int(renderer.LastLineY())
// Check for search query change — trigger a new frame if the
// widget.Editor text differs from the logic's stored query.
// This ensures the filtered list updates as the user types.
newQuery := logic.State().SearchEditor.Text()
if newQuery != logic.State().SearchQuery {
logic.InputChan() <- []ui.InputEvent{}
}
if events := renderer.CheckGestures(e.Source, gtx.Metric); len(events) > 0 {
logic.InputChan() <- events
}
e.Frame(&ops)
mu.Unlock()
}
}
}
func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) {
for {
frame := <-frameChan
mu.Lock()
*elems = frame
w.Invalidate()
mu.Unlock()
}
}