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 @@ - + diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index 5d5d3f3367..4e278dcebd 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 { @@ -143,20 +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 { @@ -176,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.ColorNameScrollBarBackground, 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 @@ -205,6 +224,7 @@ func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, s } var _ desktop.Hoverable = (*scrollBarArea)(nil) +var _ fyne.Tappable = (*scrollBarArea)(nil) type scrollBarArea struct { Base @@ -213,11 +233,51 @@ 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 { + 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.ColorNameScrollBarBackground, 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) { + // 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 { + 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 { + viewHt := a.scroll.Size().Height + newOffset.Y = fyne.Min(a.scroll.Content.Size().Height-viewHt, newOffset.Y+viewHt*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) { 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) +} 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", diff --git a/test/theme.go b/test/theme.go index bd5b707b0c..ce944bbbce 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), @@ -173,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}, 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: