From 9de088d440d7db3f7dd513fab4a20497b8ad7f7e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 1 Jan 2025 08:45:07 -0800 Subject: [PATCH 1/8] add logic for scrollling when tapping the scroll area background --- internal/widget/scroller.go | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 5d5d3f3367..b1871faa91 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -32,6 +32,9 @@ const ( scrollBarOrientationVertical scrollBarOrientation = 0 scrollBarOrientationHorizontal scrollBarOrientation = 1 scrollContainerMinSize = float32(32) // TODO consider the smallest useful scroll view? + + // what fraction of the page to scroll when tapping on the scroll bar area + pageScrollFraction = float32(0.95) ) type scrollBarRenderer struct { @@ -152,8 +155,12 @@ func (r *scrollBarAreaRenderer) Layout(_ fyne.Size) { switch r.area.orientation { case scrollBarOrientationHorizontal: barWidth, barHeight, barX, barY = r.barSizeAndOffset(r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width) + r.area.barLeadingEdge = barX + r.area.barTrailingEdge = barX + barWidth default: barHeight, barWidth, barY, barX = r.barSizeAndOffset(r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height) + r.area.barLeadingEdge = barY + r.area.barTrailingEdge = barY + barHeight } r.bar.Move(fyne.NewPos(barX, barY)) r.bar.Resize(fyne.NewSize(barWidth, barHeight)) @@ -205,6 +212,7 @@ func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, s } var _ desktop.Hoverable = (*scrollBarArea)(nil) +var _ fyne.Tappable = (*scrollBarArea)(nil) type scrollBarArea struct { Base @@ -213,6 +221,11 @@ type scrollBarArea struct { isMouseIn bool scroll *Scroll orientation scrollBarOrientation + + // updated from renderer Layout + // coordinates Y in vertical orientation, X in horizontal + barLeadingEdge float32 + barTrailingEdge float32 } func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer { @@ -220,6 +233,35 @@ func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer { return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{bar}), area: a, bar: bar} } +func (a *scrollBarArea) Tapped(e *fyne.PointEvent) { + // when tapping above/below or left/right of the bar, scroll the content + // nearly a full page (pageScrollFraction) up/down or left/right, respectively + newOffset := a.scroll.Offset + switch a.orientation { + case scrollBarOrientationHorizontal: + if e.Position.X < a.barLeadingEdge { + newOffset.X = fyne.Max(0, newOffset.X-a.scroll.Size().Width*pageScrollFraction) + } else if e.Position.X > a.barTrailingEdge { + newOffset.X = fyne.Min(a.scroll.Content.Size().Width, newOffset.X+a.scroll.Size().Width*pageScrollFraction) + } + default: + if e.Position.Y < a.barLeadingEdge { + newOffset.Y = fyne.Max(0, newOffset.Y-a.scroll.Size().Height*pageScrollFraction) + } else if e.Position.Y > a.barTrailingEdge { + newOffset.Y = fyne.Min(a.scroll.Content.Size().Height, newOffset.Y+a.scroll.Size().Height*pageScrollFraction) + } + } + if newOffset == a.scroll.Offset { + return + } + + a.scroll.Offset = newOffset + if f := a.scroll.OnScrolled; f != nil { + f(a.scroll.Offset) + } + a.scroll.refreshWithoutOffsetUpdate() +} + func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) { a.isMouseIn = true a.scroll.Refresh() From d714713f33c30b7284c8584aa047aa6bb13ab0f0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 1 Jan 2025 09:29:36 -0800 Subject: [PATCH 2/8] correct max scroll offset for the bottom/right of the content --- internal/widget/scroller.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index b1871faa91..1a4db7c0e5 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -242,13 +242,15 @@ func (a *scrollBarArea) Tapped(e *fyne.PointEvent) { if e.Position.X < a.barLeadingEdge { newOffset.X = fyne.Max(0, newOffset.X-a.scroll.Size().Width*pageScrollFraction) } else if e.Position.X > a.barTrailingEdge { - newOffset.X = fyne.Min(a.scroll.Content.Size().Width, newOffset.X+a.scroll.Size().Width*pageScrollFraction) + viewWid := a.scroll.Size().Width + newOffset.X = fyne.Min(a.scroll.Content.Size().Width-viewWid, newOffset.X+viewWid*pageScrollFraction) } default: if e.Position.Y < a.barLeadingEdge { newOffset.Y = fyne.Max(0, newOffset.Y-a.scroll.Size().Height*pageScrollFraction) } else if e.Position.Y > a.barTrailingEdge { - newOffset.Y = fyne.Min(a.scroll.Content.Size().Height, newOffset.Y+a.scroll.Size().Height*pageScrollFraction) + viewHt := a.scroll.Size().Height + newOffset.Y = fyne.Min(a.scroll.Content.Size().Height-viewHt, newOffset.Y+viewHt*pageScrollFraction) } } if newOffset == a.scroll.Offset { From 6f34467aa82088d1a6ea780f608aac3bae44b979 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 1 Jan 2025 16:04:33 -0800 Subject: [PATCH 3/8] Add background behind scroll bar when large --- internal/widget/scroller.go | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 1a4db7c0e5..9b0f5d71ea 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -146,24 +146,34 @@ func (a *scrollBarArea) isLarge() bool { type scrollBarAreaRenderer struct { BaseRenderer - area *scrollBarArea - bar *scrollBar + area *scrollBarArea + bar *scrollBar + background *canvas.Rectangle +} + +func (r *scrollBarAreaRenderer) Layout(size fyne.Size) { + r.layoutWithTheme(theme.CurrentForWidget(r.area), size) } -func (r *scrollBarAreaRenderer) Layout(_ fyne.Size) { +func (r *scrollBarAreaRenderer) layoutWithTheme(th fyne.Theme, size fyne.Size) { var barHeight, barWidth, barX, barY float32 + var bkgHeight, bkgWidth, bkgX, bkgY float32 switch r.area.orientation { case scrollBarOrientationHorizontal: - barWidth, barHeight, barX, barY = r.barSizeAndOffset(r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width) + barWidth, barHeight, barX, barY = r.barSizeAndOffset(th, r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width) r.area.barLeadingEdge = barX r.area.barTrailingEdge = barX + barWidth + bkgWidth, bkgHeight, bkgX, bkgY = size.Width, barHeight, 0, barY default: - barHeight, barWidth, barY, barX = r.barSizeAndOffset(r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height) + barHeight, barWidth, barY, barX = r.barSizeAndOffset(th, r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height) r.area.barLeadingEdge = barY r.area.barTrailingEdge = barY + barHeight + bkgWidth, bkgHeight, bkgX, bkgY = barWidth, size.Height, barX, 0 } r.bar.Move(fyne.NewPos(barX, barY)) r.bar.Resize(fyne.NewSize(barWidth, barHeight)) + r.background.Move(fyne.NewPos(bkgX, bkgY)) + r.background.Resize(fyne.NewSize(bkgWidth, bkgHeight)) } func (r *scrollBarAreaRenderer) MinSize() fyne.Size { @@ -183,14 +193,16 @@ func (r *scrollBarAreaRenderer) MinSize() fyne.Size { } func (r *scrollBarAreaRenderer) Refresh() { + th := theme.CurrentForWidget(r.area) r.bar.Refresh() - r.Layout(r.area.Size()) + r.background.FillColor = th.Color(theme.ColorNameInputBackground, fyne.CurrentApp().Settings().ThemeVariant()) + r.background.Hidden = !r.area.isLarge() + r.layoutWithTheme(th, r.area.Size()) canvas.Refresh(r.bar) + canvas.Refresh(r.background) } -func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) { - th := theme.CurrentForWidget(r.area) - +func (r *scrollBarAreaRenderer) barSizeAndOffset(th fyne.Theme, contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) { scrollBarSize := th.Size(theme.SizeNameScrollBar) if scrollLength < contentLength { portion := scrollLength / contentLength @@ -229,8 +241,12 @@ type scrollBarArea struct { } func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer { + th := theme.CurrentForWidget(a) + v := fyne.CurrentApp().Settings().ThemeVariant() bar := newScrollBar(a) - return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{bar}), area: a, bar: bar} + background := canvas.NewRectangle(th.Color(theme.ColorNameInputBackground, v)) + background.Hidden = !a.isLarge() + return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{background, bar}), area: a, bar: bar, background: background} } func (a *scrollBarArea) Tapped(e *fyne.PointEvent) { From f11f183ed422733ed9abed089c7c9b9e5da248cf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Jan 2025 07:53:39 -0800 Subject: [PATCH 4/8] add new theme ColorNameScrollBarBackground --- internal/widget/scroller.go | 4 ++-- test/theme.go | 2 ++ theme/color.go | 7 +++++++ theme/legacy.go | 2 ++ theme/theme.go | 4 ++++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 9b0f5d71ea..4e278dcebd 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -195,7 +195,7 @@ func (r *scrollBarAreaRenderer) MinSize() fyne.Size { func (r *scrollBarAreaRenderer) Refresh() { th := theme.CurrentForWidget(r.area) r.bar.Refresh() - r.background.FillColor = th.Color(theme.ColorNameInputBackground, fyne.CurrentApp().Settings().ThemeVariant()) + r.background.FillColor = th.Color(theme.ColorNameScrollBarBackground, fyne.CurrentApp().Settings().ThemeVariant()) r.background.Hidden = !r.area.isLarge() r.layoutWithTheme(th, r.area.Size()) canvas.Refresh(r.bar) @@ -244,7 +244,7 @@ func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer { th := theme.CurrentForWidget(a) v := fyne.CurrentApp().Settings().ThemeVariant() bar := newScrollBar(a) - background := canvas.NewRectangle(th.Color(theme.ColorNameInputBackground, v)) + background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBarBackground, v)) background.Hidden = !a.isLarge() return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{background, bar}), area: a, bar: bar, background: background} } diff --git a/test/theme.go b/test/theme.go index bd5b707b0c..cff6eb0a24 100644 --- a/test/theme.go +++ b/test/theme.go @@ -37,6 +37,7 @@ var knownColorNames = []fyne.ThemeColorName{ theme.ColorNamePressed, theme.ColorNamePrimary, theme.ColorNameScrollBar, + theme.ColorNameScrollBarBackground, theme.ColorNameSelection, theme.ColorNameSeparator, theme.ColorNameShadow, @@ -111,6 +112,7 @@ func NewTheme() fyne.Theme { theme.ColorNamePressed: blue(250), theme.ColorNamePrimary: green(255), theme.ColorNameScrollBar: blue(220), + theme.ColorNameScrollBarBackground: red(20), theme.ColorNameSelection: red(55), theme.ColorNameSeparator: gray(30), theme.ColorNameShadow: blue(150), diff --git a/theme/color.go b/theme/color.go index 484f686738..8aac84b92f 100644 --- a/theme/color.go +++ b/theme/color.go @@ -152,6 +152,11 @@ const ( // Since: 2.0 ColorNameScrollBar fyne.ThemeColorName = "scrollBar" + // ColorNameScrollBarBackground is the name of theme lookup for scrollbar background color. + // + // Since: 2.6 + ColorNameScrollBarBackground fyne.ThemeColorName = "scrollBarBackground" + // ColorNameSelection is the name of theme lookup for selection color. // // Since: 2.1 @@ -197,6 +202,7 @@ var ( colorDarkPlaceholder = color.NRGBA{R: 0xb2, G: 0xb2, B: 0xb2, A: 0xff} colorDarkPressed = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0x66} colorDarkScrollBar = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0x99} + colorDarkScrollBarBackground = color.NRGBA{R: 0x20, G: 0x20, B: 0x23, A: 0xff} colorDarkSeparator = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} colorDarkShadow = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x66} colorDarkSuccess = color.NRGBA{R: 0x43, G: 0xf4, B: 0x36, A: 0xff} @@ -228,6 +234,7 @@ var ( colorLightPlaceholder = color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff} colorLightPressed = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x19} colorLightScrollBar = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x99} + colorLightScrollBarBackground = color.NRGBA{R: 0xdb, G: 0xdb, B: 0xdb, A: 0xff} colorLightSelectionBlue = color.NRGBA{R: 0x00, G: 0x6c, B: 0xff, A: 0x40} colorLightSelectionBrown = color.NRGBA{R: 0x79, G: 0x55, B: 0x48, A: 0x3f} colorLightSelectionGray = color.NRGBA{R: 0x9e, G: 0x9e, B: 0x9e, A: 0x3f} diff --git a/theme/legacy.go b/theme/legacy.go index 72aaaa17ee..bff13427aa 100644 --- a/theme/legacy.go +++ b/theme/legacy.go @@ -42,6 +42,8 @@ func (l *legacyWrapper) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color. return l.old.PrimaryColor() case ColorNameScrollBar: return l.old.ScrollBarColor() + case ColorNameScrollBarBackground: + return l.old.BackgroundColor() case ColorNameShadow: return l.old.ShadowColor() default: diff --git a/theme/theme.go b/theme/theme.go index f59b4b8d9d..a5d669db94 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -260,6 +260,8 @@ func darkPaletteColorNamed(name fyne.ThemeColorName) color.Color { return colorDarkPressed case ColorNameScrollBar: return colorDarkScrollBar + case ColorNameScrollBarBackground: + return colorDarkScrollBarBackground case ColorNameSeparator: return colorDarkSeparator case ColorNameShadow: @@ -334,6 +336,8 @@ func lightPaletteColorNamed(name fyne.ThemeColorName) color.Color { return colorLightPressed case ColorNameScrollBar: return colorLightScrollBar + case ColorNameScrollBarBackground: + return colorLightScrollBarBackground case ColorNameSeparator: return colorLightSeparator case ColorNameShadow: From 07ed0cd6d64c7de2a7688f7e12347cc53a9fd2a9 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Jan 2025 08:04:12 -0800 Subject: [PATCH 5/8] fix missing color in test theme --- test/theme.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/theme.go b/test/theme.go index cff6eb0a24..ce944bbbce 100644 --- a/test/theme.go +++ b/test/theme.go @@ -175,6 +175,7 @@ func Theme() fyne.Theme { theme.ColorNamePressed: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x33}, theme.ColorNamePrimary: color.NRGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0xff}, theme.ColorNameScrollBar: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xaa}, + theme.ColorNameScrollBarBackground: color.NRGBA{R: 0x67, G: 0x66, B: 0x66, A: 0xff}, theme.ColorNameSelection: color.NRGBA{R: 0x78, G: 0x3a, B: 0x3a, A: 0x99}, theme.ColorNameSeparator: color.NRGBA{R: 0x90, G: 0x90, B: 0x90, A: 0xff}, theme.ColorNameShadow: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x88}, From 85ed1e9f8fc5690d8471a5c406574a367de6941a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Jan 2025 08:07:23 -0800 Subject: [PATCH 6/8] fix another place of missing color --- test/markup_renderer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/markup_renderer.go b/test/markup_renderer.go index 882380651b..5678de5813 100644 --- a/test/markup_renderer.go +++ b/test/markup_renderer.go @@ -408,6 +408,7 @@ func knownColor(c color.Color) string { nrgbaColor(theme.Color(theme.ColorNamePressed)): "pressed", nrgbaColor(theme.Color(theme.ColorNamePrimary)): "primary", nrgbaColor(theme.Color(theme.ColorNameScrollBar)): "scrollbar", + nrgbaColor(theme.Color(theme.ColorNameScrollBarBackground)): "scrollbarBackground", nrgbaColor(theme.Color(theme.ColorNameSelection)): "selection", nrgbaColor(theme.Color(theme.ColorNameSeparator)): "separator", nrgbaColor(theme.Color(theme.ColorNameSuccess)): "success", From e67f8d85a67886293844072b596add5876827399 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Jan 2025 08:18:31 -0800 Subject: [PATCH 7/8] update xml render fixtures --- internal/driver/glfw/testdata/windows_hover_object.xml | 4 ++-- .../driver/glfw/testdata/windows_no_hover_outside_object.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/driver/glfw/testdata/windows_hover_object.xml b/internal/driver/glfw/testdata/windows_hover_object.xml index 6d5d0f8668..a98a93fe52 100644 --- a/internal/driver/glfw/testdata/windows_hover_object.xml +++ b/internal/driver/glfw/testdata/windows_hover_object.xml @@ -5,7 +5,7 @@ - + @@ -22,7 +22,7 @@ - + diff --git a/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml b/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml index 9d6408dff6..99fe8a3f47 100644 --- a/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml +++ b/internal/driver/glfw/testdata/windows_no_hover_outside_object.xml @@ -5,7 +5,7 @@ - + @@ -21,7 +21,7 @@ - + From fc9ce4db5f9cb6c722578db0d34a9aa8475de014 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 7 Jan 2025 07:39:00 -0800 Subject: [PATCH 8/8] add unit test for tap-to-scroll --- internal/widget/scroller_internal_test.go | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/internal/widget/scroller_internal_test.go b/internal/widget/scroller_internal_test.go index 10eeb9908b..48d61ba7f1 100644 --- a/internal/widget/scroller_internal_test.go +++ b/internal/widget/scroller_internal_test.go @@ -807,3 +807,58 @@ func TestScrollBar_LargeHandleWhileInDrag(t *testing.T) { scrollBarHoriz.MouseOut() assert.False(t, scrollBarHoriz.area.isLarge()) } + +func TestScrollContainer_TapToScroll(t *testing.T) { + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(250, 250)) + s := NewScroll(rect) + s.Resize(fyne.NewSize(100, 100)) + r := s.CreateRenderer().(*scrollContainerRenderer) + + // Testing the vertical scroll bar... + // tapping on the scroll bar itself does nothing + r.vertArea.MouseIn(nil) + r.vertArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(2, 2), + }) + assert.Equal(t, fyne.NewPos(0, 0), s.Offset) + + // tapping below the bar scrolls down + r.vertArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(2, 50), + }) + assert.Greater(t, s.Offset.Y, float32(0)) + oldY := s.Offset.Y + + // tapping above the bar scrolls up + r.Refresh() // updates bar location + r.vertArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(2, 2), + }) + assert.Less(t, s.Offset.Y, oldY) + + // Testing the horizontal scroll bar... + s.Offset = fyne.NewPos(0, 0) + s.Refresh() + r.horizArea.MouseIn(nil) + + // tapping on the scroll bar itself does nothing + r.horizArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(2, 2), + }) + assert.Equal(t, fyne.NewPos(0, 0), s.Offset) + + // tapping right of bar scrolls right + r.horizArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(50, 2), + }) + assert.Greater(t, s.Offset.X, float32(0)) + oldX := s.Offset.X + + // tapping left of the bar scrolls left + r.Refresh() // updates bar location + r.horizArea.Tapped(&fyne.PointEvent{ + Position: fyne.NewPos(2, 2), + }) + assert.Less(t, s.Offset.X, oldX) +}