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
This commit is contained in:
parent
23fdeabad4
commit
8f185341b3
|
|
@ -59,6 +59,13 @@ func run(w *app.Window) error {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ type State struct {
|
|||
MaxScroll ui.Dp // max scroll offset (content height - viewport height)
|
||||
Elems []ui.Element
|
||||
// Browser state
|
||||
BrowserScrollOffset int // index of first visible browser entry
|
||||
BrowserScrollOffset ui.Dp // pixel-level scroll offset for browser list
|
||||
BrowserListHeight ui.Dp // height of the list region, set during layout
|
||||
SortMode SortMode // sort mode for browser list
|
||||
SortOrderLabel string // label text for sort order toggle
|
||||
SearchQuery string // current search query
|
||||
|
|
@ -107,7 +108,7 @@ func (s *State) layout() []ui.Element {
|
|||
if s.page == BrowserPage {
|
||||
newQuery := s.SearchEditor.Text()
|
||||
if newQuery != s.SearchQuery {
|
||||
s.BrowserScrollOffset = 0
|
||||
s.BrowserScrollOffset = 0 // reset on query change
|
||||
}
|
||||
s.SearchQuery = newQuery
|
||||
}
|
||||
|
|
@ -140,31 +141,37 @@ func HandleScroll(data any) {
|
|||
}
|
||||
|
||||
// HandleBrowserScroll updates the browser list scroll offset.
|
||||
// The delta is in pixels; convert to Dp, then to row index offset.
|
||||
// The delta is in pixels; convert to Dp for smooth per-pixel scrolling.
|
||||
func HandleBrowserScroll(data any) {
|
||||
delta := data.(int) // pixels
|
||||
deltaDp := ui.ToDp(ui.Px(delta), TheState.scale)
|
||||
rowHeight := ui.Dp(48)
|
||||
rowDelta := int(deltaDp / rowHeight)
|
||||
// Ensure at least ±1 for non-zero scroll events
|
||||
if rowDelta == 0 && delta != 0 {
|
||||
if delta > 0 {
|
||||
rowDelta = 1
|
||||
} else {
|
||||
rowDelta = -1
|
||||
}
|
||||
}
|
||||
fmt.Printf("HandleBrowserScroll: %d\n", delta)
|
||||
TheState.BrowserScrollOffset += rowDelta
|
||||
TheState.BrowserScrollOffset += deltaDp
|
||||
if TheState.BrowserScrollOffset < 0 {
|
||||
TheState.BrowserScrollOffset = 0
|
||||
}
|
||||
maxOffset := len(getFilteredEntries()) - visibleBrowserRows(TheState.PixelHeight, TheState.scale)
|
||||
if TheState.BrowserScrollOffset > maxOffset {
|
||||
TheState.BrowserScrollOffset = maxOffset
|
||||
maxScroll := computeBrowserMaxScroll()
|
||||
if TheState.BrowserScrollOffset > maxScroll {
|
||||
TheState.BrowserScrollOffset = maxScroll
|
||||
}
|
||||
}
|
||||
|
||||
// computeBrowserMaxScroll returns the maximum scroll offset in Dp
|
||||
// so the last row is fully visible at the bottom of the list.
|
||||
func computeBrowserMaxScroll() ui.Dp {
|
||||
rowHeight := ui.Dp(48)
|
||||
totalRows := len(getFilteredEntries())
|
||||
// Calculate max scroll such that the last row's bottom edge
|
||||
// aligns with the list region's bottom edge.
|
||||
// lastRowBottom = totalRows * rowHeight
|
||||
// We want: lastRowBottom - maxScroll = BrowserListHeight
|
||||
// Therefore: maxScroll = totalRows * rowHeight - BrowserListHeight
|
||||
maxScroll := ui.Dp(totalRows)*rowHeight - TheState.BrowserListHeight
|
||||
if maxScroll < 0 {
|
||||
return 0
|
||||
}
|
||||
return maxScroll
|
||||
}
|
||||
|
||||
// visibleBrowserRows estimates how many browser rows fit in the viewport.
|
||||
func visibleBrowserRows(pixelHeight int, scale float32) int {
|
||||
margin := 10
|
||||
|
|
@ -397,14 +404,25 @@ func BrowserLayout(screenWidth, screenHeight ui.Dp, sortMode SortMode) []ui.Elem
|
|||
X: margin, Y: listY,
|
||||
W: contentWidth, H: listHeight,
|
||||
}
|
||||
// Store list height for scroll clamping calculation
|
||||
TheState.BrowserListHeight = listHeight
|
||||
// Filter entries by search query (case-insensitive substring match)
|
||||
filteredEntries := getFilteredEntries()
|
||||
visibleEntries := filteredEntries[TheState.BrowserScrollOffset:]
|
||||
// Compute first visible row from Dp scroll offset
|
||||
rowHeight := ui.Dp(48)
|
||||
firstVisibleRow := int(TheState.BrowserScrollOffset / rowHeight)
|
||||
if firstVisibleRow < 0 {
|
||||
firstVisibleRow = 0
|
||||
}
|
||||
if firstVisibleRow > len(filteredEntries) {
|
||||
firstVisibleRow = len(filteredEntries)
|
||||
}
|
||||
visibleEntries := filteredEntries[firstVisibleRow:]
|
||||
list := ui.NewListView(
|
||||
"browser_list",
|
||||
visibleEntries,
|
||||
listRegion,
|
||||
TheState.BrowserScrollOffset,
|
||||
TheState.BrowserScrollOffset, // Dp, not row index
|
||||
-1, // Selected: none
|
||||
[]ui.Interaction{{Gesture: ui.Scroll, Handler: HandleBrowserScroll}},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ type ListView struct {
|
|||
visible bool
|
||||
interactions []Interaction
|
||||
Items []ListItem
|
||||
ScrollOffset int
|
||||
ScrollOffset Dp // pixel-level scroll offset
|
||||
Selected int
|
||||
RowFilenames []string // filenames for click navigation
|
||||
}
|
||||
|
|
@ -235,25 +235,27 @@ func (lv ListView) Draw(gtx layout.Context, r *Renderer) {
|
|||
}
|
||||
}
|
||||
rowHeight := Dp(48)
|
||||
firstVisibleRow := int(lv.ScrollOffset / rowHeight)
|
||||
for i, item := range lv.Items {
|
||||
// Account for scroll offset: row Y is shifted up by scroll amount
|
||||
rowGlobalIndex := firstVisibleRow + i
|
||||
y := lv.region.Y + Dp(rowGlobalIndex)*rowHeight - lv.ScrollOffset
|
||||
// Only draw visible rows (virtualized)
|
||||
y := lv.region.Y + Dp(i)*rowHeight
|
||||
if y+rowHeight < lv.region.Y || y > lv.region.Y+lv.region.H {
|
||||
continue
|
||||
}
|
||||
// Draw background for selected item
|
||||
if item.Selected || (lv.Selected == i) {
|
||||
r.drawBg(gtx, Region{
|
||||
X: lv.region.X, Y: lv.region.Y + Dp(i)*rowHeight,
|
||||
X: lv.region.X, Y: y,
|
||||
W: lv.region.W, H: rowHeight,
|
||||
}, Color{R: 200, G: 220, B: 255, A: 255})
|
||||
}
|
||||
// Register click area for this row
|
||||
rowGlobalIndex := lv.ScrollOffset + i
|
||||
rowID := fmt.Sprintf("list_row_%d", rowGlobalIndex)
|
||||
var filename string
|
||||
if rowGlobalIndex < len(lv.RowFilenames) {
|
||||
filename = lv.RowFilenames[rowGlobalIndex]
|
||||
if i < len(lv.RowFilenames) {
|
||||
filename = lv.RowFilenames[i]
|
||||
}
|
||||
r.RegisterClick(gtx, rowID, Region{
|
||||
X: lv.region.X, Y: y,
|
||||
|
|
@ -264,7 +266,7 @@ func (lv ListView) Draw(gtx layout.Context, r *Renderer) {
|
|||
// Draw main text
|
||||
textRegion := Region{
|
||||
X: lv.region.X + Dp(8),
|
||||
Y: lv.region.Y + Dp(i)*rowHeight + Dp(4),
|
||||
Y: y + Dp(4),
|
||||
W: lv.region.W - Dp(32),
|
||||
H: rowHeight - Dp(8),
|
||||
}
|
||||
|
|
@ -273,7 +275,7 @@ func (lv ListView) Draw(gtx layout.Context, r *Renderer) {
|
|||
if item.Subtext != "" {
|
||||
subRegion := Region{
|
||||
X: lv.region.X + Dp(8),
|
||||
Y: lv.region.Y + Dp(i)*rowHeight + rowHeight - Dp(22),
|
||||
Y: y + rowHeight - Dp(22),
|
||||
W: lv.region.W - Dp(32),
|
||||
H: Dp(16),
|
||||
}
|
||||
|
|
@ -283,7 +285,7 @@ func (lv ListView) Draw(gtx layout.Context, r *Renderer) {
|
|||
}
|
||||
|
||||
// NewListView creates a visible ListView element.
|
||||
func NewListView(id string, items []ListItem, region Region, scrollOffset, selected int, interactions []Interaction) ListView {
|
||||
func NewListView(id string, items []ListItem, region Region, scrollOffset Dp, selected int, interactions []Interaction) ListView {
|
||||
return ListView{
|
||||
id: id,
|
||||
region: region,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user