diff --git a/container/apptabs.go b/container/apptabs.go index 9becbb065f..5f2d5cd416 100644 --- a/container/apptabs.go +++ b/container/apptabs.go @@ -48,11 +48,14 @@ func NewAppTabs(items ...*TabItem) *AppTabs { // Implements: fyne.Widget func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer { t.BaseWidget.ExtendBaseWidget(t) + th := t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r := &appTabsRenderer{ baseTabsRenderer: baseTabsRenderer{ bar: &fyne.Container{}, - divider: canvas.NewRectangle(theme.Color(theme.ColorNameShadow)), - indicator: canvas.NewRectangle(theme.Color(theme.ColorNamePrimary)), + divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)), + indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)), }, appTabs: t, } @@ -420,23 +423,25 @@ func (r *appTabsRenderer) updateIndicator(animate bool) { var indicatorPos fyne.Position var indicatorSize fyne.Size + th := r.appTabs.Theme() + pad := th.Size(theme.SizeNamePadding) switch r.appTabs.location { case TabLocationTop: indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height) - indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding()) + indicatorSize = fyne.NewSize(selectedSize.Width, pad) case TabLocationLeading: indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y) - indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height) + indicatorSize = fyne.NewSize(pad, selectedSize.Height) case TabLocationBottom: - indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-theme.Padding()) - indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding()) + indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-pad) + indicatorSize = fyne.NewSize(selectedSize.Width, pad) case TabLocationTrailing: - indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y) - indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height) + indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y) + indicatorSize = fyne.NewSize(pad, selectedSize.Height) } - r.moveIndicator(indicatorPos, indicatorSize, animate) + r.moveIndicator(indicatorPos, indicatorSize, th, animate) } func (r *appTabsRenderer) updateTabs(max int) { diff --git a/container/doctabs.go b/container/doctabs.go index 6981800461..a2bdc8cfc5 100644 --- a/container/doctabs.go +++ b/container/doctabs.go @@ -56,11 +56,14 @@ func (t *DocTabs) Append(item *TabItem) { // Implements: fyne.Widget func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer { t.ExtendBaseWidget(t) + th := t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r := &docTabsRenderer{ baseTabsRenderer: baseTabsRenderer{ bar: &fyne.Container{}, - divider: canvas.NewRectangle(theme.Color(theme.ColorNameShadow)), - indicator: canvas.NewRectangle(theme.Color(theme.ColorNamePrimary)), + divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)), + indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)), }, docTabs: t, scroller: NewScroll(&fyne.Container{}), @@ -384,9 +387,10 @@ func (r *docTabsRenderer) scrollToSelected() { } func (r *docTabsRenderer) updateIndicator(animate bool) { + th := r.docTabs.Theme() if r.docTabs.current < 0 { r.indicator.FillColor = color.Transparent - r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), animate) + r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), th, animate) return } @@ -419,20 +423,21 @@ func (r *docTabsRenderer) updateIndicator(animate bool) { var indicatorPos fyne.Position var indicatorSize fyne.Size + pad := th.Size(theme.SizeNamePadding) switch r.docTabs.location { case TabLocationTop: indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height) - indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding()) + indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad) case TabLocationLeading: indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y) - indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) + indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) case TabLocationBottom: - indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-theme.Padding()) - indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding()) + indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-pad) + indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad) case TabLocationTrailing: - indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y-scrollOffset.Y) - indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) + indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y-scrollOffset.Y) + indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) } if indicatorPos.X < 0 { @@ -449,7 +454,7 @@ func (r *docTabsRenderer) updateIndicator(animate bool) { return } - r.moveIndicator(indicatorPos, indicatorSize, animate) + r.moveIndicator(indicatorPos, indicatorSize, th, animate) } func (r *docTabsRenderer) updateAllTabs() { diff --git a/container/innerwindow.go b/container/innerwindow.go index 0597adbf29..32e0839113 100644 --- a/container/innerwindow.go +++ b/container/innerwindow.go @@ -76,10 +76,12 @@ func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer { } title := newDraggableLabel(w.title, w) title.Truncation = fyne.TextTruncateEllipsis + th := w.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() bar := NewBorder(nil, nil, buttons, icon, title) - bg := canvas.NewRectangle(theme.Color(theme.ColorNameOverlayBackground)) - contentBG := canvas.NewRectangle(theme.Color(theme.ColorNameBackground)) + bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v)) + contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v)) corner := newDraggableCorner(w) objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner} @@ -119,7 +121,9 @@ type innerWindowRenderer struct { } func (i *innerWindowRenderer) Layout(size fyne.Size) { - pad := theme.Padding() + th := i.win.Theme() + pad := th.Size(theme.SizeNamePadding) + pos := fyne.NewSquareOffsetPos(pad / 2) size = size.Subtract(fyne.NewSquareSize(pad)) i.LayoutShadow(size, pos) @@ -144,7 +148,8 @@ func (i *innerWindowRenderer) Layout(size fyne.Size) { } func (i *innerWindowRenderer) MinSize() fyne.Size { - pad := theme.Padding() + th := i.win.Theme() + pad := th.Size(theme.SizeNamePadding) contentMin := i.win.content.MinSize() barMin := i.bar.MinSize() @@ -154,9 +159,11 @@ func (i *innerWindowRenderer) MinSize() fyne.Size { } func (i *innerWindowRenderer) Refresh() { - i.bg.FillColor = theme.Color(theme.ColorNameOverlayBackground) + th := i.win.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v) i.bg.Refresh() - i.contentBG.FillColor = theme.Color(theme.ColorNameBackground) + i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v) i.contentBG.Refresh() i.bar.Refresh() diff --git a/container/split.go b/container/split.go index 5290de5e2c..75891bafed 100644 --- a/container/split.go +++ b/container/split.go @@ -100,7 +100,7 @@ func (r *splitContainerRenderer) Layout(size fyne.Size) { leadingSize.Width = lw leadingSize.Height = size.Height dividerPos.X = lw - dividerSize.Width = dividerThickness() + dividerSize.Width = dividerThickness(r.divider) dividerSize.Height = size.Height trailingPos.X = lw + dividerSize.Width trailingSize.Width = tw @@ -112,7 +112,7 @@ func (r *splitContainerRenderer) Layout(size fyne.Size) { leadingSize.Height = lh dividerPos.Y = lh dividerSize.Width = size.Width - dividerSize.Height = dividerThickness() + dividerSize.Height = dividerThickness(r.divider) trailingPos.Y = lh + dividerSize.Height trailingSize.Width = size.Width trailingSize.Height = th @@ -151,11 +151,13 @@ func (r *splitContainerRenderer) Refresh() { // [1] is divider which doesn't change r.objects[2] = r.split.Trailing r.Layout(r.split.Size()) + + r.divider.Refresh() canvas.Refresh(r.split) } func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) { - available := float64(total - dividerThickness()) + available := float64(total - dividerThickness(r.divider)) if available <= 0 { return 0, 0 } @@ -234,8 +236,11 @@ func newDivider(split *Split) *divider { // CreateRenderer is a private method to Fyne which links this widget to its renderer func (d *divider) CreateRenderer() fyne.WidgetRenderer { d.ExtendBaseWidget(d) - background := canvas.NewRectangle(theme.Color(theme.ColorNameShadow)) - foreground := canvas.NewRectangle(theme.Color(theme.ColorNameForeground)) + th := d.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)) + foreground := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v)) return ÷rRenderer{ divider: d, background: background, @@ -267,12 +272,12 @@ func (d *divider) Dragged(e *fyne.DragEvent) { x, y := d.currentDragPos.Components() var offset, leadingRatio, trailingRatio float64 if d.split.Horizontal { - widthFree := float64(d.split.Size().Width - dividerThickness()) + widthFree := float64(d.split.Size().Width - dividerThickness(d)) leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree) offset = float64(x-d.startDragOff.X) / widthFree } else { - heightFree := float64(d.split.Size().Height - dividerThickness()) + heightFree := float64(d.split.Size().Height - dividerThickness(d)) leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree) offset = float64(y-d.startDragOff.Y) / heightFree @@ -315,15 +320,15 @@ func (r *dividerRenderer) Layout(size fyne.Size) { r.background.Resize(size) var x, y, w, h float32 if r.divider.split.Horizontal { - x = (dividerThickness() - handleThickness()) / 2 - y = (size.Height - handleLength()) / 2 - w = handleThickness() - h = handleLength() + x = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2 + y = (size.Height - handleLength(r.divider)) / 2 + w = handleThickness(r.divider) + h = handleLength(r.divider) } else { - x = (size.Width - handleLength()) / 2 - y = (dividerThickness() - handleThickness()) / 2 - w = handleLength() - h = handleThickness() + x = (size.Width - handleLength(r.divider)) / 2 + y = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2 + w = handleLength(r.divider) + h = handleThickness(r.divider) } r.foreground.Move(fyne.NewPos(x, y)) r.foreground.Resize(fyne.NewSize(w, h)) @@ -331,9 +336,9 @@ func (r *dividerRenderer) Layout(size fyne.Size) { func (r *dividerRenderer) MinSize() fyne.Size { if r.divider.split.Horizontal { - return fyne.NewSize(dividerThickness(), dividerLength()) + return fyne.NewSize(dividerThickness(r.divider), dividerLength(r.divider)) } - return fyne.NewSize(dividerLength(), dividerThickness()) + return fyne.NewSize(dividerLength(r.divider), dividerThickness(r.divider)) } func (r *dividerRenderer) Objects() []fyne.CanvasObject { @@ -341,29 +346,45 @@ func (r *dividerRenderer) Objects() []fyne.CanvasObject { } func (r *dividerRenderer) Refresh() { + th := r.divider.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + if r.divider.hovered { - r.background.FillColor = theme.Color(theme.ColorNameHover) + r.background.FillColor = th.Color(theme.ColorNameHover, v) } else { - r.background.FillColor = theme.Color(theme.ColorNameShadow) + r.background.FillColor = th.Color(theme.ColorNameShadow, v) } r.background.Refresh() - r.foreground.FillColor = theme.Color(theme.ColorNameForeground) + r.foreground.FillColor = th.Color(theme.ColorNameForeground, v) r.foreground.Refresh() r.Layout(r.divider.Size()) } -func dividerThickness() float32 { - return theme.Padding() * 2 +func dividerTheme(d *divider) fyne.Theme { + if d == nil { + return theme.Current() + } + + return d.Theme() +} + +func dividerThickness(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 2 } -func dividerLength() float32 { - return theme.Padding() * 6 +func dividerLength(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 6 } -func handleThickness() float32 { - return theme.Padding() / 2 +func handleThickness(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) / 2 + } -func handleLength() float32 { - return theme.Padding() * 4 +func handleLength(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 4 } diff --git a/container/split_test.go b/container/split_test.go index a7c1ee52d1..4a2d776dee 100644 --- a/container/split_test.go +++ b/container/split_test.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/theme" "github.com/stretchr/testify/assert" ) @@ -18,13 +19,13 @@ func TestSplitContainer_MinSize(t *testing.T) { rectB.SetMinSize(fyne.NewSize(10, 10)) t.Run("Horizontal", func(t *testing.T) { min := NewHSplit(rectA, rectB).MinSize() - assert.Equal(t, rectA.MinSize().Width+rectB.MinSize().Width+dividerThickness(), min.Width) - assert.Equal(t, fyne.Max(rectA.MinSize().Height, fyne.Max(rectB.MinSize().Height, dividerLength())), min.Height) + assert.Equal(t, rectA.MinSize().Width+rectB.MinSize().Width+dividerThickness(nil), min.Width) + assert.Equal(t, fyne.Max(rectA.MinSize().Height, fyne.Max(rectB.MinSize().Height, dividerLength(nil))), min.Height) }) t.Run("Vertical", func(t *testing.T) { min := NewVSplit(rectA, rectB).MinSize() - assert.Equal(t, fyne.Max(rectA.MinSize().Width, fyne.Max(rectB.MinSize().Width, dividerLength())), min.Width) - assert.Equal(t, rectA.MinSize().Height+rectB.MinSize().Height+dividerThickness(), min.Height) + assert.Equal(t, fyne.Max(rectA.MinSize().Width, fyne.Max(rectB.MinSize().Width, dividerLength(nil))), min.Width) + assert.Equal(t, rectA.MinSize().Height+rectB.MinSize().Height+dividerThickness(nil), min.Height) }) } @@ -41,66 +42,66 @@ func TestSplitContainer_Resize(t *testing.T) { true, fyne.NewSize(100, 100), fyne.NewPos(0, 0), - fyne.NewSize(50-dividerThickness()/2, 100), - fyne.NewPos(50+dividerThickness()/2, 0), - fyne.NewSize(50-dividerThickness()/2, 100), + fyne.NewSize(50-dividerThickness(nil)/2, 100), + fyne.NewPos(50+dividerThickness(nil)/2, 0), + fyne.NewSize(50-dividerThickness(nil)/2, 100), }, "vertical": { false, fyne.NewSize(100, 100), fyne.NewPos(0, 0), - fyne.NewSize(100, 50-dividerThickness()/2), - fyne.NewPos(0, 50+dividerThickness()/2), - fyne.NewSize(100, 50-dividerThickness()/2), + fyne.NewSize(100, 50-dividerThickness(nil)/2), + fyne.NewPos(0, 50+dividerThickness(nil)/2), + fyne.NewSize(100, 50-dividerThickness(nil)/2), }, "horizontal insufficient width": { true, fyne.NewSize(20, 100), fyne.NewPos(0, 0), // minSize of leading is 1/3 of minSize of trailing - fyne.NewSize((20-dividerThickness())/4, 100), - fyne.NewPos((20-dividerThickness())/4+dividerThickness(), 0), - fyne.NewSize((20-dividerThickness())*3/4, 100), + fyne.NewSize((20-dividerThickness(nil))/4, 100), + fyne.NewPos((20-dividerThickness(nil))/4+dividerThickness(nil), 0), + fyne.NewSize((20-dividerThickness(nil))*3/4, 100), }, "vertical insufficient height": { false, fyne.NewSize(100, 20), fyne.NewPos(0, 0), // minSize of leading is 1/3 of minSize of trailing - fyne.NewSize(100, (20-dividerThickness())/4), - fyne.NewPos(0, (20-dividerThickness())/4+dividerThickness()), - fyne.NewSize(100, (20-dividerThickness())*3/4), + fyne.NewSize(100, (20-dividerThickness(nil))/4), + fyne.NewPos(0, (20-dividerThickness(nil))/4+dividerThickness(nil)), + fyne.NewSize(100, (20-dividerThickness(nil))*3/4), }, "horizontal zero width": { true, fyne.NewSize(0, 100), fyne.NewPos(0, 0), fyne.NewSize(0, 100), - fyne.NewPos(dividerThickness(), 0), + fyne.NewPos(dividerThickness(nil), 0), fyne.NewSize(0, 100), }, "horizontal zero height": { true, fyne.NewSize(100, 0), fyne.NewPos(0, 0), - fyne.NewSize(50-dividerThickness()/2, 0), - fyne.NewPos(50+dividerThickness()/2, 0), - fyne.NewSize(50-dividerThickness()/2, 0), + fyne.NewSize(50-dividerThickness(nil)/2, 0), + fyne.NewPos(50+dividerThickness(nil)/2, 0), + fyne.NewSize(50-dividerThickness(nil)/2, 0), }, "vertical zero width": { false, fyne.NewSize(0, 100), fyne.NewPos(0, 0), - fyne.NewSize(0, 50-dividerThickness()/2), - fyne.NewPos(0, 50+dividerThickness()/2), - fyne.NewSize(0, 50-dividerThickness()/2), + fyne.NewSize(0, 50-dividerThickness(nil)/2), + fyne.NewPos(0, 50+dividerThickness(nil)/2), + fyne.NewSize(0, 50-dividerThickness(nil)/2), }, "vertical zero height": { false, fyne.NewSize(100, 0), fyne.NewPos(0, 0), fyne.NewSize(100, 0), - fyne.NewPos(0, dividerThickness()), + fyne.NewPos(0, dividerThickness(nil)), fyne.NewSize(100, 0), }, } { @@ -127,7 +128,7 @@ func TestSplitContainer_Resize(t *testing.T) { func TestSplitContainer_SetRatio(t *testing.T) { size := fyne.NewSize(100, 100) - usableLength := 100 - float64(dividerThickness()) + usableLength := 100 - float64(dividerThickness(nil)) objA := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) objB := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) @@ -191,7 +192,7 @@ func TestSplitContainer_SetRatio_limits(t *testing.T) { sc.Resize(fyne.NewSize(200, 50)) sizeA := objA.Size() sizeB := objB.Size() - assert.Equal(t, 150-dividerThickness(), sizeA.Width) + assert.Equal(t, 150-dividerThickness(nil), sizeA.Width) assert.Equal(t, float32(50), sizeA.Height) assert.Equal(t, float32(50), sizeB.Width) assert.Equal(t, float32(50), sizeB.Height) @@ -203,7 +204,7 @@ func TestSplitContainer_SetRatio_limits(t *testing.T) { sizeB := objB.Size() assert.Equal(t, float32(50), sizeA.Width) assert.Equal(t, float32(50), sizeA.Height) - assert.Equal(t, 150-dividerThickness(), sizeB.Width) + assert.Equal(t, 150-dividerThickness(nil), sizeB.Width) assert.Equal(t, float32(50), sizeB.Height) }) }) @@ -215,7 +216,7 @@ func TestSplitContainer_SetRatio_limits(t *testing.T) { sizeA := objA.Size() sizeB := objB.Size() assert.Equal(t, float32(50), sizeA.Width) - assert.Equal(t, 150-dividerThickness(), sizeA.Height) + assert.Equal(t, 150-dividerThickness(nil), sizeA.Height) assert.Equal(t, float32(50), sizeB.Width) assert.Equal(t, float32(50), sizeB.Height) }) @@ -227,14 +228,37 @@ func TestSplitContainer_SetRatio_limits(t *testing.T) { assert.Equal(t, float32(50), sizeA.Width) assert.Equal(t, float32(50), sizeA.Height) assert.Equal(t, float32(50), sizeB.Width) - assert.Equal(t, 150-dividerThickness(), sizeB.Height) + assert.Equal(t, 150-dividerThickness(nil), sizeB.Height) }) }) } +func TestSplitContainer_ThemeOverride(t *testing.T) { + test.NewTempApp(t) + + split := NewHSplit(canvas.NewRectangle(color.Transparent), canvas.NewRectangle(color.Transparent)) + w := test.NewWindow(split) + defer w.Close() + w.SetPadded(false) + w.Resize(fyne.NewSize(100, 100)) + c := w.Canvas() + + test.AssertRendersToImage(t, "split/default.png", c) + + test.ApplyTheme(t, test.NewTheme()) + test.AssertRendersToImage(t, "split/ugly.png", c) + + // set a BG that matches the theme, this is outside our container scope + normal := test.Theme() + bg := canvas.NewRectangle(normal.Color(theme.ColorNameBackground, theme.VariantDark)) + w.SetContent(NewStack(bg, NewThemeOverride(split, normal))) + w.Resize(fyne.NewSize(100, 100)) + test.AssertRendersToImage(t, "split/default.png", c) +} + func TestSplitContainer_swap_contents(t *testing.T) { - dl := dividerLength() - dt := dividerThickness() + dl := dividerLength(nil) + dt := dividerThickness(nil) initialWidth := 10 + 10 + dt initialHeight := fyne.Max(10, dl) expectedWidth := 100 + 10 + dt @@ -442,19 +466,19 @@ func TestSplitContainer_divider_MinSize(t *testing.T) { t.Run("Horizontal", func(t *testing.T) { divider := newDivider(&Split{Horizontal: true}) min := divider.MinSize() - assert.Equal(t, dividerThickness(), min.Width) - assert.Equal(t, dividerLength(), min.Height) + assert.Equal(t, dividerThickness(nil), min.Width) + assert.Equal(t, dividerLength(nil), min.Height) }) t.Run("Vertical", func(t *testing.T) { divider := newDivider(&Split{Horizontal: false}) min := divider.MinSize() - assert.Equal(t, dividerLength(), min.Width) - assert.Equal(t, dividerThickness(), min.Height) + assert.Equal(t, dividerLength(nil), min.Width) + assert.Equal(t, dividerThickness(nil), min.Height) }) } func TestSplitContainer_Hidden(t *testing.T) { - dt := dividerThickness() + dt := dividerThickness(nil) size := fyne.NewSize(10, 10) objA := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) objA.SetMinSize(size) diff --git a/container/tabs.go b/container/tabs.go index c8747e49ce..742523e91c 100644 --- a/container/tabs.go +++ b/container/tabs.go @@ -74,6 +74,8 @@ func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObje } type baseTabs interface { + fyne.Widget + onUnselected() func(*TabItem) onSelected() func(*TabItem) @@ -304,9 +306,12 @@ func (r *baseTabsRenderer) applyTheme(t baseTabs) { if r.action != nil { r.action.SetIcon(moreIcon(t)) } - r.divider.FillColor = theme.Color(theme.ColorNameShadow) - r.indicator.FillColor = theme.Color(theme.ColorNamePrimary) - r.indicator.CornerRadius = theme.SelectionRadiusSize() + th := theme.CurrentForWidget(t) + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.divider.FillColor = th.Color(theme.ColorNameShadow, v) + r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v) + r.indicator.CornerRadius = th.Size(theme.SizeNameSelectionRadius) for _, tab := range r.tabs.items() { tab.Content.Refresh() @@ -321,7 +326,8 @@ func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) { barMin := r.bar.MinSize() - padding := theme.Padding() + th := theme.CurrentForWidget(t) + padding := th.Size(theme.SizeNamePadding) switch t.tabLocation() { case TabLocationTop: barHeight := barMin.Height @@ -337,7 +343,7 @@ func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) { barSize = fyne.NewSize(barWidth, size.Height) dividerPos = fyne.NewPos(barWidth, 0) dividerSize = fyne.NewSize(padding, size.Height) - contentPos = fyne.NewPos(barWidth+theme.Padding(), 0) + contentPos = fyne.NewPos(barWidth+padding, 0) contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height) case TabLocationBottom: barHeight := barMin.Height @@ -374,7 +380,8 @@ func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) { } func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size { - pad := theme.Padding() + th := theme.CurrentForWidget(t) + pad := th.Size(theme.SizeNamePadding) buttonPad := pad barMin := r.bar.MinSize() tabsMin := r.bar.Objects[0].MinSize() @@ -407,7 +414,7 @@ func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size { } } -func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, animate bool) { +func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, th fyne.Theme, animate bool) { r.lastIndicatorMutex.RLock() isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() && r.lastIndicatorHidden == r.indicator.Hidden @@ -425,7 +432,8 @@ func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, anima r.sizeAnimation = nil } - r.indicator.FillColor = theme.Color(theme.ColorNamePrimary) + v := fyne.CurrentApp().Settings().ThemeVariant() + r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v) if r.indicator.Position().IsZero() { r.indicator.Move(pos) r.indicator.Resize(siz) @@ -507,15 +515,18 @@ type tabButton struct { func (b *tabButton) CreateRenderer() fyne.WidgetRenderer { b.ExtendBaseWidget(b) - background := canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - background.CornerRadius = theme.SelectionRadiusSize() + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) background.Hide() icon := canvas.NewImageFromResource(b.icon) if b.icon == nil { icon.Hide() } - label := canvas.NewText(b.text, theme.Color(theme.ColorNameForeground)) + label := canvas.NewText(b.text, th.Color(theme.ColorNameForeground, v)) label.TextStyle.Bold = true close := &tabCloseButton{ @@ -581,6 +592,8 @@ func (r *tabButtonRenderer) Destroy() { } func (r *tabButtonRenderer) Layout(size fyne.Size) { + th := r.button.Theme() + pad := th.Size(theme.SizeNamePadding) r.background.Resize(size) padding := r.padding() innerSize := size.Subtract(padding) @@ -596,7 +609,7 @@ func (r *tabButtonRenderer) Layout(size fyne.Size) { } r.icon.Resize(fyne.NewSquareSize(iconSize)) r.icon.Move(innerOffset.Add(iconOffset)) - labelShift = iconSize + theme.Padding() + labelShift = iconSize + pad } if r.label.Text != "" { var labelOffset fyne.Position @@ -611,16 +624,17 @@ func (r *tabButtonRenderer) Layout(size fyne.Size) { r.label.Resize(labelSize) r.label.Move(innerOffset.Add(labelOffset)) } - inlineIconSize := theme.IconInlineSize() - r.close.Move(fyne.NewPos(size.Width-inlineIconSize-theme.Padding(), (size.Height-inlineIconSize)/2)) + inlineIconSize := th.Size(theme.SizeNameInlineIcon) + r.close.Move(fyne.NewPos(size.Width-inlineIconSize-pad, (size.Height-inlineIconSize)/2)) r.close.Resize(fyne.NewSquareSize(inlineIconSize)) } func (r *tabButtonRenderer) MinSize() fyne.Size { + th := r.button.Theme() var contentWidth, contentHeight float32 textSize := r.label.MinSize() iconSize := r.iconSize() - padding := theme.Padding() + padding := th.Size(theme.SizeNamePadding) if r.button.iconPosition == buttonIconTop { contentWidth = fyne.Max(textSize.Width, iconSize) if r.icon.Visible() { @@ -645,7 +659,7 @@ func (r *tabButtonRenderer) MinSize() fyne.Size { } } if r.button.onClosed != nil { - inlineIconSize := theme.IconInlineSize() + inlineIconSize := th.Size(theme.SizeNameInlineIcon) contentWidth += inlineIconSize + padding contentHeight = fyne.Max(contentHeight, inlineIconSize) } @@ -657,9 +671,12 @@ func (r *tabButtonRenderer) Objects() []fyne.CanvasObject { } func (r *tabButtonRenderer) Refresh() { + th := r.button.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + if r.button.hovered && !r.button.Disabled() { - r.background.FillColor = theme.Color(theme.ColorNameHover) - r.background.CornerRadius = theme.SelectionRadiusSize() + r.background.FillColor = th.Color(theme.ColorNameHover, v) + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) r.background.Show() } else { r.background.Hide() @@ -670,14 +687,14 @@ func (r *tabButtonRenderer) Refresh() { r.label.Alignment = r.button.textAlignment if !r.button.Disabled() { if r.button.importance == widget.HighImportance { - r.label.Color = theme.Color(theme.ColorNamePrimary) + r.label.Color = th.Color(theme.ColorNamePrimary, v) } else { - r.label.Color = theme.Color(theme.ColorNameForeground) + r.label.Color = th.Color(theme.ColorNameForeground, v) } } else { - r.label.Color = theme.Color(theme.ColorNameDisabled) + r.label.Color = th.Color(theme.ColorNameDisabled, v) } - r.label.TextSize = theme.TextSize() + r.label.TextSize = th.Size(theme.SizeNameText) if r.button.text == "" { r.label.Hide() } else { @@ -714,15 +731,16 @@ func (r *tabButtonRenderer) Refresh() { } func (r *tabButtonRenderer) iconSize() float32 { + iconSize := r.button.Theme().Size(theme.SizeNameInlineIcon) if r.button.iconPosition == buttonIconTop { - return 2 * theme.IconInlineSize() + return 2 * iconSize } - return theme.IconInlineSize() + return iconSize } func (r *tabButtonRenderer) padding() fyne.Size { - padding := theme.InnerPadding() + padding := r.button.Theme().Size(theme.SizeNameInnerPadding) if r.label.Text != "" && r.button.iconPosition == buttonIconInline { return fyne.NewSquareSize(padding * 2) } @@ -742,8 +760,11 @@ type tabCloseButton struct { func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer { b.ExtendBaseWidget(b) - background := canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - background.CornerRadius = theme.SelectionRadiusSize() + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) background.Hide() icon := canvas.NewImageFromResource(theme.CancelIcon()) @@ -795,7 +816,7 @@ func (r *tabCloseButtonRenderer) Layout(size fyne.Size) { } func (r *tabCloseButtonRenderer) MinSize() fyne.Size { - return fyne.NewSquareSize(theme.IconInlineSize()) + return fyne.NewSquareSize(r.button.Theme().Size(theme.SizeNameInlineIcon)) } func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject { @@ -803,9 +824,12 @@ func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject { } func (r *tabCloseButtonRenderer) Refresh() { + th := r.button.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + if r.button.hovered { - r.background.FillColor = theme.Color(theme.ColorNameHover) - r.background.CornerRadius = theme.SelectionRadiusSize() + r.background.FillColor = th.Color(theme.ColorNameHover, v) + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) r.background.Show() } else { r.background.Hide() diff --git a/container/testdata/split/default.png b/container/testdata/split/default.png new file mode 100644 index 0000000000..536dfb74bc Binary files /dev/null and b/container/testdata/split/default.png differ diff --git a/container/testdata/split/ugly.png b/container/testdata/split/ugly.png new file mode 100644 index 0000000000..b37024d229 Binary files /dev/null and b/container/testdata/split/ugly.png differ diff --git a/internal/cache/theme.go b/internal/cache/theme.go index 25efb597d1..6a50ec2a67 100644 --- a/internal/cache/theme.go +++ b/internal/cache/theme.go @@ -26,7 +26,18 @@ type overrideScope struct { func OverrideTheme(o fyne.CanvasObject, th fyne.Theme) { id := overrideCount.Add(1) s := &overrideScope{th: th, cacheID: strconv.Itoa(int(id))} - overrideTheme(o, s, id) + overrideTheme(o, s) +} + +func OverrideThemeMatchingScope(o, parent fyne.CanvasObject) bool { + data, ok := overrides.Load(parent) + if !ok { // not overridden in parent + return false + } + + scope := data.(*overrideScope) + overrideTheme(o, scope) + return true } func WidgetScopeID(o fyne.CanvasObject) string { @@ -47,24 +58,24 @@ func WidgetTheme(o fyne.CanvasObject) fyne.Theme { return data.(*overrideScope).th } -func overrideContainer(c *fyne.Container, s *overrideScope, id uint32) { +func overrideContainer(c *fyne.Container, s *overrideScope) { for _, o := range c.Objects { - overrideTheme(o, s, id) + overrideTheme(o, s) } } -func overrideTheme(o fyne.CanvasObject, s *overrideScope, id uint32) { +func overrideTheme(o fyne.CanvasObject, s *overrideScope) { switch c := o.(type) { case fyne.Widget: - overrideWidget(c, s, id) + overrideWidget(c, s) case *fyne.Container: - overrideContainer(c, s, id) + overrideContainer(c, s) default: overrides.Store(c, s) } } -func overrideWidget(w fyne.Widget, s *overrideScope, id uint32) { +func overrideWidget(w fyne.Widget, s *overrideScope) { ResetThemeCaches() overrides.Store(w, s) @@ -74,6 +85,6 @@ func overrideWidget(w fyne.Widget, s *overrideScope, id uint32) { } for _, o := range r.Objects() { - overrideTheme(o, s, id) + overrideTheme(o, s) } } diff --git a/internal/widget/scroller.go b/internal/widget/scroller.go index a846043f23..5d5d3f3367 100644 --- a/internal/widget/scroller.go +++ b/internal/widget/scroller.go @@ -50,8 +50,11 @@ func (r *scrollBarRenderer) MinSize() fyne.Size { } func (r *scrollBarRenderer) Refresh() { - r.background.FillColor = theme.Color(theme.ColorNameScrollBar) - r.background.CornerRadius = theme.Size(theme.SizeNameScrollBarRadius) + th := theme.CurrentForWidget(r.scrollBar) + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.background.FillColor = th.Color(theme.ColorNameScrollBar, v) + r.background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius) r.background.Refresh() } @@ -67,8 +70,11 @@ type scrollBar struct { } func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer { - background := canvas.NewRectangle(theme.Color(theme.ColorNameScrollBar)) - background.CornerRadius = theme.Size(theme.SizeNameScrollBarRadius) + th := theme.CurrentForWidget(b) + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBar, v)) + background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius) r := &scrollBarRenderer{ scrollBar: b, background: background, @@ -154,25 +160,31 @@ func (r *scrollBarAreaRenderer) Layout(_ fyne.Size) { } func (r *scrollBarAreaRenderer) MinSize() fyne.Size { - min := theme.ScrollBarSize() + th := theme.CurrentForWidget(r.area) + + barSize := th.Size(theme.SizeNameScrollBar) + min := barSize if !r.area.isLarge() { - min = theme.ScrollBarSmallSize() * 2 + min = th.Size(theme.SizeNameScrollBarSmall) * 2 } switch r.area.orientation { case scrollBarOrientationHorizontal: - return fyne.NewSize(theme.ScrollBarSize(), min) + return fyne.NewSize(barSize, min) default: - return fyne.NewSize(min, theme.ScrollBarSize()) + return fyne.NewSize(min, barSize) } } func (r *scrollBarAreaRenderer) Refresh() { + r.bar.Refresh() r.Layout(r.area.Size()) canvas.Refresh(r.bar) } func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) { - scrollBarSize := theme.ScrollBarSize() + th := theme.CurrentForWidget(r.area) + + scrollBarSize := th.Size(theme.SizeNameScrollBar) if scrollLength < contentLength { portion := scrollLength / contentLength length = float32(int(scrollLength)) * portion @@ -186,7 +198,7 @@ func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, s if r.area.isLarge() { width = scrollBarSize } else { - widthOffset = theme.ScrollBarSmallSize() + widthOffset = th.Size(theme.SizeNameScrollBarSmall) width = widthOffset } return @@ -301,6 +313,13 @@ func (r *scrollContainerRenderer) MinSize() fyne.Size { } func (r *scrollContainerRenderer) Refresh() { + r.horizArea.Refresh() + r.vertArea.Refresh() + r.leftShadow.Refresh() + r.topShadow.Refresh() + r.rightShadow.Refresh() + r.bottomShadow.Refresh() + if len(r.BaseRenderer.Objects()) == 0 || r.BaseRenderer.Objects()[0] != r.scroll.Content { // push updated content object to baseRenderer r.BaseRenderer.Objects()[0] = r.scroll.Content diff --git a/internal/widget/scroller_internal_test.go b/internal/widget/scroller_internal_test.go new file mode 100644 index 0000000000..10eeb9908b --- /dev/null +++ b/internal/widget/scroller_internal_test.go @@ -0,0 +1,809 @@ +package widget + +import ( + "image/color" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/theme" +) + +func TestNewScroll(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(10, 10)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + barArea := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea + bar := cache.Renderer(barArea).(*scrollBarAreaRenderer).bar + assert.Equal(t, float32(0), scroll.Offset.Y) + assert.Equal(t, theme.ScrollBarSmallSize()*2, barArea.Size().Width) + assert.Equal(t, theme.ScrollBarSmallSize(), bar.Size().Width) + assert.Equal(t, theme.ScrollBarSmallSize(), bar.Position().X) + assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), barArea.Position()) +} + +func TestScrollContainer_MinSize(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 50)) + scroll := NewScroll(rect) + assert.Equal(t, fyne.NewSize(32, 32), scroll.MinSize()) + + scrollMin := fyne.NewSize(100, 100) + scroll.SetMinSize(scrollMin) + cache.Renderer(scroll).Layout(scroll.minSize) + + assert.Equal(t, scrollMin, scroll.MinSize()) + assert.Equal(t, fyne.NewSize(500, 100), rect.Size()) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) +} + +func TestScrollContainer_ScrollToTop(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 50)) + scroll := NewScroll(rect) + scroll.ScrollToTop() + Y := scroll.Offset.Y + assert.Equal(t, float32(0), Y) +} + +func TestScrollContainer_ScrollToBottom(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 50)) + scroll := NewScroll(rect) + scroll.ScrollToBottom() + ExpectedY := float32(50) + Y := scroll.Content.Size().Height - scroll.Size().Height + assert.Equal(t, ExpectedY, Y) +} + +func TestScrollContainer_MinSize_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + size := scroll.MinSize() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + size := scroll.MinSize() + assert.Equal(t, float32(100), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + size := scroll.MinSize() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(100), size.Width) + }) +} + +func TestScrollContainer_OnScrolled(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + + scrolled := false + scroll.OnScrolled = func(fyne.Position) { + scrolled = true + } + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) + assert.True(t, scrolled) + scrolled = false + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, 0)}) + assert.False(t, scrolled) // don't repeat for no-change +} + +func TestScrollContainer_SetMinSize_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := scroll.MinSize() + assert.Equal(t, float32(50), size.Height) + assert.Equal(t, float32(50), size.Width) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := scroll.MinSize() + assert.Equal(t, float32(100), size.Height) + assert.Equal(t, float32(50), size.Width) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := scroll.MinSize() + assert.Equal(t, float32(50), size.Height) + assert.Equal(t, float32(100), size.Width) + }) +} + +func TestScrollContainer_Resize_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(scroll.MinSize()) + size := scroll.Size() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + scroll.Resize(scroll.MinSize()) + size := scroll.Size() + assert.Equal(t, float32(100), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + scroll.Resize(scroll.MinSize()) + size := scroll.Size() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(100), size.Width) + }) +} + +func TestScrollContainer_Refresh(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, fyne.NewSize(1000, 1000), rect.Size()) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-1000, -1000)}) + assert.Equal(t, float32(900), scroll.Offset.X) + assert.Equal(t, float32(900), scroll.Offset.Y) + assert.Equal(t, fyne.NewSize(1000, 1000), rect.Size()) + rect.SetMinSize(fyne.NewSize(500, 500)) + scroll.Refresh() + assert.Equal(t, float32(400), scroll.Offset.X) + assert.Equal(t, fyne.NewSize(500, 500), rect.Size()) + + rect2 := canvas.NewRectangle(color.White) + scroll.Content = rect2 + scroll.Refresh() + assert.Equal(t, rect2, cache.Renderer(scroll).Objects()[0]) +} + +func TestScrollContainer_Scrolled(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) + assert.Equal(t, float32(10), scroll.Offset.X) + assert.Equal(t, float32(10), scroll.Offset.Y) +} + +func TestScrollContainer_Scrolled_Limit(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(80, 80)) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-25, -25)}) + assert.Equal(t, float32(20), scroll.Offset.X) +} + +func TestScrollContainer_Scrolled_Back(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + scroll.Offset.X = 10 + scroll.Offset.Y = 10 + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(10, 10)}) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) +} + +func TestScrollContainer_Scrolled_ScrollNone(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Direction = ScrollNone + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) +} + +func TestScrollContainer_Scrolled_BackLimit(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll.Resize(fyne.NewSize(100, 100)) + scroll.Offset.X = 10 + scroll.Offset.Y = 10 + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(20, 20)}) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + +} + +func TestScrollContainer_Resize(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll.Resize(fyne.NewSize(80, 80)) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -20)}) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + +} + +func TestScrollContainer_ResizeOffset(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll.Resize(fyne.NewSize(80, 80)) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -20)}) + scroll.Resize(fyne.NewSize(90, 90)) + assert.Equal(t, float32(10), scroll.Offset.X) + assert.Equal(t, float32(10), scroll.Offset.Y) +} + +func TestScrollContainer_ResizeExpand(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(120, 140)) + assert.Equal(t, float32(120), rect.Size().Width) + assert.Equal(t, float32(140), rect.Size().Height) +} + +func TestScrollContainer_ScrollBarForSmallContentIsHidden(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 200)) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.False(t, r.vertArea.Visible()) + assert.False(t, r.horizArea.Visible()) +} + +func TestScrollContainer_ShowHiddenScrollBarIfContentGrows(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll.Resize(fyne.NewSize(200, 200)) + require.False(t, r.horizArea.Visible()) + require.False(t, r.vertArea.Visible()) + rect.SetMinSize(fyne.NewSize(300, 300)) + r.Layout(scroll.Size()) + assert.True(t, r.horizArea.Visible()) + assert.True(t, r.vertArea.Visible()) +} + +func TestScrollContainer_HideScrollBarIfContentShrinks(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + rect.SetMinSize(fyne.NewSize(300, 300)) + scroll.Resize(fyne.NewSize(200, 200)) + require.True(t, r.horizArea.Visible()) + require.True(t, r.vertArea.Visible()) + rect.SetMinSize(fyne.NewSize(200, 200)) + r.Layout(scroll.Size()) + assert.False(t, r.horizArea.Visible()) + assert.False(t, r.vertArea.Visible()) +} + +func TestScrollContainer_ScrollBarIsSmall(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + rect.SetMinSize(fyne.NewSize(500, 500)) + scroll.Resize(fyne.NewSize(100, 100)) + areaHoriz := cache.Renderer(scroll).(*scrollContainerRenderer).horizArea + areaVert := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea + barHoriz := cache.Renderer(areaHoriz).(*scrollBarAreaRenderer).bar + barVert := cache.Renderer(areaVert).(*scrollBarAreaRenderer).bar + require.True(t, areaHoriz.Visible()) + require.True(t, areaVert.Visible()) + require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) + + assert.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) + assert.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) + assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) + assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) + assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) + assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) + assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) + assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) + +} + +func TestScrollContainer_ScrollBarGrowsAndShrinksOnMouseInAndMouseOut(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + scroll := NewScroll(rect) + rect.SetMinSize(fyne.NewSize(500, 500)) + scroll.Resize(fyne.NewSize(100, 100)) + areaHoriz := cache.Renderer(scroll).(*scrollContainerRenderer).horizArea + areaVert := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea + barHoriz := cache.Renderer(areaHoriz).(*scrollBarAreaRenderer).bar + barVert := cache.Renderer(areaVert).(*scrollBarAreaRenderer).bar + require.True(t, barHoriz.Visible()) + require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) + require.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) + require.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) + require.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) + require.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) + barHoriz.MouseIn(&desktop.MouseEvent{}) + assert.Equal(t, theme.ScrollBarSize(), areaHoriz.Size().Height) + assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSize()), areaHoriz.Position()) + assert.Equal(t, theme.ScrollBarSize(), barHoriz.Size().Height) + assert.Equal(t, float32(0), barHoriz.Position().Y) + barHoriz.MouseOut() + assert.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) + assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) + assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) + assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) + require.True(t, barVert.Visible()) + require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) + require.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) + require.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) + require.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) + require.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) + barVert.MouseIn(&desktop.MouseEvent{}) + assert.Equal(t, theme.ScrollBarSize(), areaVert.Size().Width) + assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSize(), 0), areaVert.Position()) + assert.Equal(t, theme.ScrollBarSize(), barVert.Size().Width) + assert.Equal(t, float32(0), barVert.Position().X) + barVert.MouseOut() + assert.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) + assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) + assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) + assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) +} + +func TestScrollContainer_ShowShadowOnLeftIfContentIsScrolled(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.False(t, r.leftShadow.Visible()) + assert.Equal(t, fyne.NewPos(0, 0), r.leftShadow.Position()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: -1}}) + assert.True(t, r.leftShadow.Visible()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: 1}}) + assert.False(t, r.leftShadow.Visible()) +} + +func TestScrollContainer_ShowShadowOnRightIfContentCanScroll(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.True(t, r.rightShadow.Visible()) + assert.Equal(t, scroll.Size().Width, r.rightShadow.Position().X+r.rightShadow.Size().Width) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: -400}}) + assert.False(t, r.rightShadow.Visible()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: 100}}) + assert.True(t, r.rightShadow.Visible()) +} + +func TestScrollContainer_ShowShadowOnTopIfContentIsScrolled(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 500)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.False(t, r.topShadow.Visible()) + assert.Equal(t, fyne.NewPos(0, 0), r.topShadow.Position()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: -1}}) + assert.True(t, r.topShadow.Visible()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: 1}}) + assert.False(t, r.topShadow.Visible()) +} + +func TestScrollContainer_ShowShadowOnBottomIfContentCanScroll(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 500)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.True(t, r.bottomShadow.Visible()) + assert.Equal(t, scroll.Size().Height, r.bottomShadow.Position().Y+r.bottomShadow.Size().Height) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: -400}}) + assert.False(t, r.bottomShadow.Visible()) + + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: 100}}) + assert.True(t, r.bottomShadow.Visible()) +} + +func TestScrollContainer_ScrollHorizontallyWithVerticalMouseScroll(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 50)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -10)}) + assert.Equal(t, float32(10), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + + t.Run("not if scroll event includes horizontal offset", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 50)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -40)}) + assert.Equal(t, float32(20), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + }) + + t.Run("not if content is vertically scrollable", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(0), scroll.Offset.Y) + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -10)}) + assert.Equal(t, float32(0), scroll.Offset.X) + assert.Equal(t, float32(10), scroll.Offset.Y) + }) +} + +func TestScrollBarRenderer_BarSize(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + areaHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer) + areaVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer) + + assert.Equal(t, float32(100), areaHoriz.bar.Size().Width) + assert.Equal(t, float32(100), areaVert.bar.Size().Height) + + // resize so content is twice our size. Bar should therefore be half again. + scroll.Resize(fyne.NewSize(50, 50)) + assert.Equal(t, float32(25), areaHoriz.bar.Size().Width) + assert.Equal(t, float32(25), areaVert.bar.Size().Height) +} + +func TestScrollContainerRenderer_LimitBarSize(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(120, 120)) + areaHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer) + areaVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer) + + assert.Equal(t, float32(120), areaHoriz.bar.Size().Width) + assert.Equal(t, float32(120), areaVert.bar.Size().Height) +} + +func TestScrollContainerRenderer_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.NotNil(t, r.vertArea) + assert.NotNil(t, r.topShadow) + assert.NotNil(t, r.bottomShadow) + assert.NotNil(t, r.horizArea) + assert.NotNil(t, r.leftShadow) + assert.NotNil(t, r.rightShadow) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.NotNil(t, r.vertArea) + assert.False(t, r.vertArea.Visible()) + assert.NotNil(t, r.topShadow) + assert.False(t, r.topShadow.Visible()) + assert.NotNil(t, r.bottomShadow) + assert.False(t, r.bottomShadow.Visible()) + assert.NotNil(t, r.horizArea) + assert.NotNil(t, r.leftShadow) + assert.NotNil(t, r.rightShadow) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + r := cache.Renderer(scroll).(*scrollContainerRenderer) + assert.NotNil(t, r.vertArea) + assert.NotNil(t, r.topShadow) + assert.NotNil(t, r.bottomShadow) + assert.NotNil(t, r.horizArea) + assert.False(t, r.horizArea.Visible()) + assert.NotNil(t, r.leftShadow) + assert.False(t, r.leftShadow.Visible()) + assert.NotNil(t, r.rightShadow) + assert.False(t, r.rightShadow.Visible()) + }) +} + +func TestScrollContainerRenderer_MinSize_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(100), size.Height) + assert.Equal(t, float32(32), size.Width) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(32), size.Height) + assert.Equal(t, float32(100), size.Width) + }) +} + +func TestScrollContainerRenderer_SetMinSize_Direction(t *testing.T) { + t.Run("Both", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(50), size.Height) + assert.Equal(t, float32(50), size.Width) + }) + t.Run("HorizontalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewHScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(100), size.Height) + assert.Equal(t, float32(50), size.Width) + }) + t.Run("VerticalOnly", func(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(100, 100)) + scroll := NewVScroll(rect) + scroll.SetMinSize(fyne.NewSize(50, 50)) + size := cache.Renderer(scroll).MinSize() + assert.Equal(t, float32(50), size.Height) + assert.Equal(t, float32(100), size.Width) + }) +} + +func TestScrollBar_Dragged_ClickedInside(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 500)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar + + // Create drag event with starting position inside scroll rectangle area + dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} + assert.Equal(t, float32(0), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(100), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} + assert.Equal(t, float32(0), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(100), scroll.Offset.Y) +} + +func TestScrollBar_DraggedBack_ClickedInside(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(500, 500)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(100, 100)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar + + // Drag forward + dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} + scrollBarHoriz.Dragged(&dragEvent) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} + scrollBarVert.Dragged(&dragEvent) + + // Drag back + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -10}} + assert.Equal(t, float32(100), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(50), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -10}} + assert.Equal(t, float32(100), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(50), scroll.Offset.Y) +} + +func TestScrollBar_Dragged_Limit(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(200, 200)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar + + // Drag over limit + dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 2000}} + assert.Equal(t, float32(0), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 2000}} + assert.Equal(t, float32(0), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.Y) + + // Drag again + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 100}} + // Offset doesn't go over limit + assert.Equal(t, float32(800), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 100}} + // Offset doesn't go over limit + assert.Equal(t, float32(800), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.Y) + + // Drag back (still outside limit) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -1000}} + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1000}} + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(800), scroll.Offset.Y) + + // Drag back (inside limit) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -1040}} + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(300), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1040}} + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(300), scroll.Offset.Y) +} + +func TestScrollBar_Dragged_BackLimit(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(200, 200)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar + + // Drag over back limit + dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: -1000}} + // Offset doesn't go over limit + assert.Equal(t, float32(0), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(0), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1000}} + // Offset doesn't go over limit + assert.Equal(t, float32(0), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(0), scroll.Offset.Y) + + // Drag (still outside limit) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 500}} + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(0), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 500}} + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(0), scroll.Offset.Y) + + // Drag (inside limit) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 520}} + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(100), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 520}} + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(100), scroll.Offset.Y) +} + +func TestScrollBar_DraggedWithNonZeroStartPosition(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(200, 200)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar + + dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 50}} + assert.Equal(t, float32(0), scroll.Offset.X) + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(250), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 50}} + assert.Equal(t, float32(0), scroll.Offset.Y) + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(250), scroll.Offset.Y) + + // Drag again (after releasing mouse button) + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} + scrollBarHoriz.Dragged(&dragEvent) + assert.Equal(t, float32(350), scroll.Offset.X) + + dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} + scrollBarVert.Dragged(&dragEvent) + assert.Equal(t, float32(350), scroll.Offset.Y) +} + +func TestScrollBar_LargeHandleWhileInDrag(t *testing.T) { + rect := canvas.NewRectangle(color.Black) + rect.SetMinSize(fyne.NewSize(1000, 1000)) + scroll := NewScroll(rect) + scroll.Resize(fyne.NewSize(200, 200)) + scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar + + // Make sure that hovering makes the bar large. + mouseEvent := &desktop.MouseEvent{} + assert.False(t, scrollBarHoriz.area.isLarge()) + scrollBarHoriz.MouseIn(mouseEvent) + assert.True(t, scrollBarHoriz.area.isLarge()) + scrollBarHoriz.MouseOut() + assert.False(t, scrollBarHoriz.area.isLarge()) + + // Make sure that the bar stays large when dragging, even if the mouse leaves the bar. + dragEvent := &fyne.DragEvent{Dragged: fyne.Delta{DX: 10}} + scrollBarHoriz.Dragged(dragEvent) + assert.True(t, scrollBarHoriz.area.isLarge()) + scrollBarHoriz.MouseOut() + assert.True(t, scrollBarHoriz.area.isLarge()) + scrollBarHoriz.DragEnd() + scrollBarHoriz.MouseOut() + assert.False(t, scrollBarHoriz.area.isLarge()) +} diff --git a/internal/widget/scroller_test.go b/internal/widget/scroller_test.go index 10eeb9908b..06679e90c1 100644 --- a/internal/widget/scroller_test.go +++ b/internal/widget/scroller_test.go @@ -1,809 +1,51 @@ -package widget +package widget_test import ( "image/color" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" ) -func TestNewScroll(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(10, 10)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - barArea := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea - bar := cache.Renderer(barArea).(*scrollBarAreaRenderer).bar - assert.Equal(t, float32(0), scroll.Offset.Y) - assert.Equal(t, theme.ScrollBarSmallSize()*2, barArea.Size().Width) - assert.Equal(t, theme.ScrollBarSmallSize(), bar.Size().Width) - assert.Equal(t, theme.ScrollBarSmallSize(), bar.Position().X) - assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), barArea.Position()) -} - -func TestScrollContainer_MinSize(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 50)) - scroll := NewScroll(rect) - assert.Equal(t, fyne.NewSize(32, 32), scroll.MinSize()) - - scrollMin := fyne.NewSize(100, 100) - scroll.SetMinSize(scrollMin) - cache.Renderer(scroll).Layout(scroll.minSize) +func TestScrollContainer_Theme(t *testing.T) { + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(250, 250)) + scroll := widget.NewScroll(rect) - assert.Equal(t, scrollMin, scroll.MinSize()) - assert.Equal(t, fyne.NewSize(500, 100), rect.Size()) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) -} - -func TestScrollContainer_ScrollToTop(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 50)) - scroll := NewScroll(rect) - scroll.ScrollToTop() - Y := scroll.Offset.Y - assert.Equal(t, float32(0), Y) -} - -func TestScrollContainer_ScrollToBottom(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 50)) - scroll := NewScroll(rect) - scroll.ScrollToBottom() - ExpectedY := float32(50) - Y := scroll.Content.Size().Height - scroll.Size().Height - assert.Equal(t, ExpectedY, Y) -} + w := test.NewTempWindow(t, scroll) + w.SetPadded(false) + w.Resize(fyne.NewSize(100, 100)) + test.AssertImageMatches(t, "scroll/theme_initial.png", w.Canvas().Capture()) -func TestScrollContainer_MinSize_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - size := scroll.MinSize() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - size := scroll.MinSize() - assert.Equal(t, float32(100), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - size := scroll.MinSize() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(100), size.Width) + test.WithTestTheme(t, func() { + time.Sleep(100 * time.Millisecond) + scroll.Refresh() + test.AssertImageMatches(t, "scroll/theme_changed.png", w.Canvas().Capture()) }) } -func TestScrollContainer_OnScrolled(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) +func TestScrollContainer_ThemeOverride(t *testing.T) { + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(250, 250)) + scroll := widget.NewScroll(rect) scroll.Resize(fyne.NewSize(100, 100)) - scrolled := false - scroll.OnScrolled = func(fyne.Position) { - scrolled = true - } - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) - assert.True(t, scrolled) - scrolled = false - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, 0)}) - assert.False(t, scrolled) // don't repeat for no-change -} - -func TestScrollContainer_SetMinSize_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := scroll.MinSize() - assert.Equal(t, float32(50), size.Height) - assert.Equal(t, float32(50), size.Width) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := scroll.MinSize() - assert.Equal(t, float32(100), size.Height) - assert.Equal(t, float32(50), size.Width) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := scroll.MinSize() - assert.Equal(t, float32(50), size.Height) - assert.Equal(t, float32(100), size.Width) - }) -} - -func TestScrollContainer_Resize_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(scroll.MinSize()) - size := scroll.Size() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - scroll.Resize(scroll.MinSize()) - size := scroll.Size() - assert.Equal(t, float32(100), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - scroll.Resize(scroll.MinSize()) - size := scroll.Size() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(100), size.Width) - }) -} - -func TestScrollContainer_Refresh(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, fyne.NewSize(1000, 1000), rect.Size()) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-1000, -1000)}) - assert.Equal(t, float32(900), scroll.Offset.X) - assert.Equal(t, float32(900), scroll.Offset.Y) - assert.Equal(t, fyne.NewSize(1000, 1000), rect.Size()) - rect.SetMinSize(fyne.NewSize(500, 500)) - scroll.Refresh() - assert.Equal(t, float32(400), scroll.Offset.X) - assert.Equal(t, fyne.NewSize(500, 500), rect.Size()) - - rect2 := canvas.NewRectangle(color.White) - scroll.Content = rect2 - scroll.Refresh() - assert.Equal(t, rect2, cache.Renderer(scroll).Objects()[0]) -} - -func TestScrollContainer_Scrolled(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) - assert.Equal(t, float32(10), scroll.Offset.X) - assert.Equal(t, float32(10), scroll.Offset.Y) -} - -func TestScrollContainer_Scrolled_Limit(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(80, 80)) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-25, -25)}) - assert.Equal(t, float32(20), scroll.Offset.X) -} - -func TestScrollContainer_Scrolled_Back(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - scroll.Offset.X = 10 - scroll.Offset.Y = 10 - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(10, 10)}) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) -} - -func TestScrollContainer_Scrolled_ScrollNone(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Direction = ScrollNone - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-10, -10)}) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) -} - -func TestScrollContainer_Scrolled_BackLimit(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll.Resize(fyne.NewSize(100, 100)) - scroll.Offset.X = 10 - scroll.Offset.Y = 10 - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(20, 20)}) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - -} - -func TestScrollContainer_Resize(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll.Resize(fyne.NewSize(80, 80)) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -20)}) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - -} - -func TestScrollContainer_ResizeOffset(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll.Resize(fyne.NewSize(80, 80)) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -20)}) - scroll.Resize(fyne.NewSize(90, 90)) - assert.Equal(t, float32(10), scroll.Offset.X) - assert.Equal(t, float32(10), scroll.Offset.Y) -} - -func TestScrollContainer_ResizeExpand(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(120, 140)) - assert.Equal(t, float32(120), rect.Size().Width) - assert.Equal(t, float32(140), rect.Size().Height) -} - -func TestScrollContainer_ScrollBarForSmallContentIsHidden(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 200)) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.False(t, r.vertArea.Visible()) - assert.False(t, r.horizArea.Visible()) -} - -func TestScrollContainer_ShowHiddenScrollBarIfContentGrows(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll.Resize(fyne.NewSize(200, 200)) - require.False(t, r.horizArea.Visible()) - require.False(t, r.vertArea.Visible()) - rect.SetMinSize(fyne.NewSize(300, 300)) - r.Layout(scroll.Size()) - assert.True(t, r.horizArea.Visible()) - assert.True(t, r.vertArea.Visible()) -} - -func TestScrollContainer_HideScrollBarIfContentShrinks(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - rect.SetMinSize(fyne.NewSize(300, 300)) - scroll.Resize(fyne.NewSize(200, 200)) - require.True(t, r.horizArea.Visible()) - require.True(t, r.vertArea.Visible()) - rect.SetMinSize(fyne.NewSize(200, 200)) - r.Layout(scroll.Size()) - assert.False(t, r.horizArea.Visible()) - assert.False(t, r.vertArea.Visible()) -} - -func TestScrollContainer_ScrollBarIsSmall(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - rect.SetMinSize(fyne.NewSize(500, 500)) - scroll.Resize(fyne.NewSize(100, 100)) - areaHoriz := cache.Renderer(scroll).(*scrollContainerRenderer).horizArea - areaVert := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea - barHoriz := cache.Renderer(areaHoriz).(*scrollBarAreaRenderer).bar - barVert := cache.Renderer(areaVert).(*scrollBarAreaRenderer).bar - require.True(t, areaHoriz.Visible()) - require.True(t, areaVert.Visible()) - require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) - - assert.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) - assert.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) - assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) - assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) - assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) - assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) - assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) - assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) - -} - -func TestScrollContainer_ScrollBarGrowsAndShrinksOnMouseInAndMouseOut(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - scroll := NewScroll(rect) - rect.SetMinSize(fyne.NewSize(500, 500)) - scroll.Resize(fyne.NewSize(100, 100)) - areaHoriz := cache.Renderer(scroll).(*scrollContainerRenderer).horizArea - areaVert := cache.Renderer(scroll).(*scrollContainerRenderer).vertArea - barHoriz := cache.Renderer(areaHoriz).(*scrollBarAreaRenderer).bar - barVert := cache.Renderer(areaVert).(*scrollBarAreaRenderer).bar - require.True(t, barHoriz.Visible()) - require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) - require.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) - require.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) - require.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) - require.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) - barHoriz.MouseIn(&desktop.MouseEvent{}) - assert.Equal(t, theme.ScrollBarSize(), areaHoriz.Size().Height) - assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSize()), areaHoriz.Position()) - assert.Equal(t, theme.ScrollBarSize(), barHoriz.Size().Height) - assert.Equal(t, float32(0), barHoriz.Position().Y) - barHoriz.MouseOut() - assert.Equal(t, theme.ScrollBarSmallSize()*2, areaHoriz.Size().Height) - assert.Equal(t, fyne.NewPos(0, 100-theme.ScrollBarSmallSize()*2), areaHoriz.Position()) - assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Size().Height) - assert.Equal(t, theme.ScrollBarSmallSize(), barHoriz.Position().Y) - require.True(t, barVert.Visible()) - require.Less(t, theme.ScrollBarSmallSize(), theme.ScrollBarSize()) - require.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) - require.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) - require.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) - require.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) - barVert.MouseIn(&desktop.MouseEvent{}) - assert.Equal(t, theme.ScrollBarSize(), areaVert.Size().Width) - assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSize(), 0), areaVert.Position()) - assert.Equal(t, theme.ScrollBarSize(), barVert.Size().Width) - assert.Equal(t, float32(0), barVert.Position().X) - barVert.MouseOut() - assert.Equal(t, theme.ScrollBarSmallSize()*2, areaVert.Size().Width) - assert.Equal(t, fyne.NewPos(100-theme.ScrollBarSmallSize()*2, 0), areaVert.Position()) - assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Size().Width) - assert.Equal(t, theme.ScrollBarSmallSize(), barVert.Position().X) -} - -func TestScrollContainer_ShowShadowOnLeftIfContentIsScrolled(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.False(t, r.leftShadow.Visible()) - assert.Equal(t, fyne.NewPos(0, 0), r.leftShadow.Position()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: -1}}) - assert.True(t, r.leftShadow.Visible()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: 1}}) - assert.False(t, r.leftShadow.Visible()) -} - -func TestScrollContainer_ShowShadowOnRightIfContentCanScroll(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.True(t, r.rightShadow.Visible()) - assert.Equal(t, scroll.Size().Width, r.rightShadow.Position().X+r.rightShadow.Size().Width) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: -400}}) - assert.False(t, r.rightShadow.Visible()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DX: 100}}) - assert.True(t, r.rightShadow.Visible()) -} - -func TestScrollContainer_ShowShadowOnTopIfContentIsScrolled(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 500)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.False(t, r.topShadow.Visible()) - assert.Equal(t, fyne.NewPos(0, 0), r.topShadow.Position()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: -1}}) - assert.True(t, r.topShadow.Visible()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: 1}}) - assert.False(t, r.topShadow.Visible()) -} - -func TestScrollContainer_ShowShadowOnBottomIfContentCanScroll(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 500)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.True(t, r.bottomShadow.Visible()) - assert.Equal(t, scroll.Size().Height, r.bottomShadow.Position().Y+r.bottomShadow.Size().Height) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: -400}}) - assert.False(t, r.bottomShadow.Visible()) - - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.Delta{DY: 100}}) - assert.True(t, r.bottomShadow.Visible()) -} - -func TestScrollContainer_ScrollHorizontallyWithVerticalMouseScroll(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 50)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -10)}) - assert.Equal(t, float32(10), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - - t.Run("not if scroll event includes horizontal offset", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 50)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(-20, -40)}) - assert.Equal(t, float32(20), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - }) - - t.Run("not if content is vertically scrollable", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(0), scroll.Offset.Y) - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -10)}) - assert.Equal(t, float32(0), scroll.Offset.X) - assert.Equal(t, float32(10), scroll.Offset.Y) - }) -} - -func TestScrollBarRenderer_BarSize(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - areaHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer) - areaVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer) - - assert.Equal(t, float32(100), areaHoriz.bar.Size().Width) - assert.Equal(t, float32(100), areaVert.bar.Size().Height) - - // resize so content is twice our size. Bar should therefore be half again. - scroll.Resize(fyne.NewSize(50, 50)) - assert.Equal(t, float32(25), areaHoriz.bar.Size().Width) - assert.Equal(t, float32(25), areaVert.bar.Size().Height) -} - -func TestScrollContainerRenderer_LimitBarSize(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(120, 120)) - areaHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer) - areaVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer) - - assert.Equal(t, float32(120), areaHoriz.bar.Size().Width) - assert.Equal(t, float32(120), areaVert.bar.Size().Height) -} - -func TestScrollContainerRenderer_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.NotNil(t, r.vertArea) - assert.NotNil(t, r.topShadow) - assert.NotNil(t, r.bottomShadow) - assert.NotNil(t, r.horizArea) - assert.NotNil(t, r.leftShadow) - assert.NotNil(t, r.rightShadow) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.NotNil(t, r.vertArea) - assert.False(t, r.vertArea.Visible()) - assert.NotNil(t, r.topShadow) - assert.False(t, r.topShadow.Visible()) - assert.NotNil(t, r.bottomShadow) - assert.False(t, r.bottomShadow.Visible()) - assert.NotNil(t, r.horizArea) - assert.NotNil(t, r.leftShadow) - assert.NotNil(t, r.rightShadow) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - r := cache.Renderer(scroll).(*scrollContainerRenderer) - assert.NotNil(t, r.vertArea) - assert.NotNil(t, r.topShadow) - assert.NotNil(t, r.bottomShadow) - assert.NotNil(t, r.horizArea) - assert.False(t, r.horizArea.Visible()) - assert.NotNil(t, r.leftShadow) - assert.False(t, r.leftShadow.Visible()) - assert.NotNil(t, r.rightShadow) - assert.False(t, r.rightShadow.Visible()) - }) -} - -func TestScrollContainerRenderer_MinSize_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(100), size.Height) - assert.Equal(t, float32(32), size.Width) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(32), size.Height) - assert.Equal(t, float32(100), size.Width) - }) -} - -func TestScrollContainerRenderer_SetMinSize_Direction(t *testing.T) { - t.Run("Both", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(50), size.Height) - assert.Equal(t, float32(50), size.Width) - }) - t.Run("HorizontalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewHScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(100), size.Height) - assert.Equal(t, float32(50), size.Width) - }) - t.Run("VerticalOnly", func(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(100, 100)) - scroll := NewVScroll(rect) - scroll.SetMinSize(fyne.NewSize(50, 50)) - size := cache.Renderer(scroll).MinSize() - assert.Equal(t, float32(50), size.Height) - assert.Equal(t, float32(100), size.Width) - }) -} - -func TestScrollBar_Dragged_ClickedInside(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 500)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar - - // Create drag event with starting position inside scroll rectangle area - dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} - assert.Equal(t, float32(0), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(100), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} - assert.Equal(t, float32(0), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(100), scroll.Offset.Y) -} - -func TestScrollBar_DraggedBack_ClickedInside(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(500, 500)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(100, 100)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar - - // Drag forward - dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} - scrollBarHoriz.Dragged(&dragEvent) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} - scrollBarVert.Dragged(&dragEvent) - - // Drag back - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -10}} - assert.Equal(t, float32(100), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(50), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -10}} - assert.Equal(t, float32(100), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(50), scroll.Offset.Y) -} - -func TestScrollBar_Dragged_Limit(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(200, 200)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar - - // Drag over limit - dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 2000}} - assert.Equal(t, float32(0), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 2000}} - assert.Equal(t, float32(0), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.Y) - - // Drag again - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 100}} - // Offset doesn't go over limit - assert.Equal(t, float32(800), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 100}} - // Offset doesn't go over limit - assert.Equal(t, float32(800), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.Y) - - // Drag back (still outside limit) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -1000}} - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1000}} - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(800), scroll.Offset.Y) - - // Drag back (inside limit) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: -1040}} - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(300), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1040}} - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(300), scroll.Offset.Y) -} - -func TestScrollBar_Dragged_BackLimit(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(200, 200)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar - - // Drag over back limit - dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: -1000}} - // Offset doesn't go over limit - assert.Equal(t, float32(0), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(0), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: -1000}} - // Offset doesn't go over limit - assert.Equal(t, float32(0), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(0), scroll.Offset.Y) - - // Drag (still outside limit) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 500}} - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(0), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 500}} - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(0), scroll.Offset.Y) - - // Drag (inside limit) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 520}} - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(100), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 520}} - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(100), scroll.Offset.Y) -} - -func TestScrollBar_DraggedWithNonZeroStartPosition(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(200, 200)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - scrollBarVert := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).vertArea).(*scrollBarAreaRenderer).bar - - dragEvent := fyne.DragEvent{Dragged: fyne.Delta{DX: 50}} - assert.Equal(t, float32(0), scroll.Offset.X) - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(250), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 50}} - assert.Equal(t, float32(0), scroll.Offset.Y) - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(250), scroll.Offset.Y) - - // Drag again (after releasing mouse button) - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DX: 20}} - scrollBarHoriz.Dragged(&dragEvent) - assert.Equal(t, float32(350), scroll.Offset.X) - - dragEvent = fyne.DragEvent{Dragged: fyne.Delta{DY: 20}} - scrollBarVert.Dragged(&dragEvent) - assert.Equal(t, float32(350), scroll.Offset.Y) -} - -func TestScrollBar_LargeHandleWhileInDrag(t *testing.T) { - rect := canvas.NewRectangle(color.Black) - rect.SetMinSize(fyne.NewSize(1000, 1000)) - scroll := NewScroll(rect) - scroll.Resize(fyne.NewSize(200, 200)) - scrollBarHoriz := cache.Renderer(cache.Renderer(scroll).(*scrollContainerRenderer).horizArea).(*scrollBarAreaRenderer).bar - - // Make sure that hovering makes the bar large. - mouseEvent := &desktop.MouseEvent{} - assert.False(t, scrollBarHoriz.area.isLarge()) - scrollBarHoriz.MouseIn(mouseEvent) - assert.True(t, scrollBarHoriz.area.isLarge()) - scrollBarHoriz.MouseOut() - assert.False(t, scrollBarHoriz.area.isLarge()) + w := test.NewTempWindow(t, scroll) + w.SetPadded(false) + w.Resize(fyne.NewSize(100, 100)) + test.ApplyTheme(t, test.NewTheme()) + test.AssertImageMatches(t, "scroll/theme_changed.png", w.Canvas().Capture()) - // Make sure that the bar stays large when dragging, even if the mouse leaves the bar. - dragEvent := &fyne.DragEvent{Dragged: fyne.Delta{DX: 10}} - scrollBarHoriz.Dragged(dragEvent) - assert.True(t, scrollBarHoriz.area.isLarge()) - scrollBarHoriz.MouseOut() - assert.True(t, scrollBarHoriz.area.isLarge()) - scrollBarHoriz.DragEnd() - scrollBarHoriz.MouseOut() - assert.False(t, scrollBarHoriz.area.isLarge()) + normal := test.Theme() + bg := canvas.NewRectangle(normal.Color(theme.ColorNameBackground, theme.VariantDark)) + w.SetContent(container.NewStack(bg, container.NewThemeOverride(scroll, normal))) + w.Resize(fyne.NewSize(100, 100)) + // TODO why is this off by a 1bit RGB difference? + //test.AssertImageMatches(t, "scroll/theme_initial.png", w.Canvas().Capture()) } diff --git a/internal/widget/shadow.go b/internal/widget/shadow.go index 509cef55f9..4356ac0f21 100644 --- a/internal/widget/shadow.go +++ b/internal/widget/shadow.go @@ -115,75 +115,83 @@ func (r *shadowRenderer) Refresh() { } func (r *shadowRenderer) createShadows() { + th := theme.CurrentForWidget(r.s) + v := fyne.CurrentApp().Settings().ThemeVariant() + fg := th.Color(theme.ColorNameShadow, v) + switch r.s.typ { case ShadowLeft: - r.l = canvas.NewHorizontalGradient(color.Transparent, theme.Color(theme.ColorNameShadow)) + r.l = canvas.NewHorizontalGradient(color.Transparent, fg) r.SetObjects([]fyne.CanvasObject{r.l}) case ShadowRight: - r.r = canvas.NewHorizontalGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.r = canvas.NewHorizontalGradient(fg, color.Transparent) r.SetObjects([]fyne.CanvasObject{r.r}) case ShadowBottom: - r.b = canvas.NewVerticalGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.b = canvas.NewVerticalGradient(fg, color.Transparent) r.SetObjects([]fyne.CanvasObject{r.b}) case ShadowTop: - r.t = canvas.NewVerticalGradient(color.Transparent, theme.Color(theme.ColorNameShadow)) + r.t = canvas.NewVerticalGradient(color.Transparent, fg) r.SetObjects([]fyne.CanvasObject{r.t}) case ShadowAround: - r.tl = canvas.NewRadialGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.tl = canvas.NewRadialGradient(fg, color.Transparent) r.tl.CenterOffsetX = 0.5 r.tl.CenterOffsetY = 0.5 - r.t = canvas.NewVerticalGradient(color.Transparent, theme.Color(theme.ColorNameShadow)) - r.tr = canvas.NewRadialGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.t = canvas.NewVerticalGradient(color.Transparent, fg) + r.tr = canvas.NewRadialGradient(fg, color.Transparent) r.tr.CenterOffsetX = -0.5 r.tr.CenterOffsetY = 0.5 - r.r = canvas.NewHorizontalGradient(theme.Color(theme.ColorNameShadow), color.Transparent) - r.br = canvas.NewRadialGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.r = canvas.NewHorizontalGradient(fg, color.Transparent) + r.br = canvas.NewRadialGradient(fg, color.Transparent) r.br.CenterOffsetX = -0.5 r.br.CenterOffsetY = -0.5 - r.b = canvas.NewVerticalGradient(theme.Color(theme.ColorNameShadow), color.Transparent) - r.bl = canvas.NewRadialGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + r.b = canvas.NewVerticalGradient(fg, color.Transparent) + r.bl = canvas.NewRadialGradient(fg, color.Transparent) r.bl.CenterOffsetX = 0.5 r.bl.CenterOffsetY = -0.5 - r.l = canvas.NewHorizontalGradient(color.Transparent, theme.Color(theme.ColorNameShadow)) + r.l = canvas.NewHorizontalGradient(color.Transparent, fg) r.SetObjects([]fyne.CanvasObject{r.tl, r.t, r.tr, r.r, r.br, r.b, r.bl, r.l}) } } func (r *shadowRenderer) refreshShadows() { - updateShadowEnd(r.l) - updateShadowStart(r.r) - updateShadowStart(r.b) - updateShadowEnd(r.t) - - updateShadowRadial(r.tl) - updateShadowRadial(r.tr) - updateShadowRadial(r.bl) - updateShadowRadial(r.br) + th := theme.CurrentForWidget(r.s) + v := fyne.CurrentApp().Settings().ThemeVariant() + fg := th.Color(theme.ColorNameShadow, v) + + updateShadowEnd(r.l, fg) + updateShadowStart(r.r, fg) + updateShadowStart(r.b, fg) + updateShadowEnd(r.t, fg) + + updateShadowRadial(r.tl, fg) + updateShadowRadial(r.tr, fg) + updateShadowRadial(r.bl, fg) + updateShadowRadial(r.br, fg) } -func updateShadowEnd(g *canvas.LinearGradient) { +func updateShadowEnd(g *canvas.LinearGradient, fg color.Color) { if g == nil { return } - g.EndColor = theme.Color(theme.ColorNameShadow) + g.EndColor = fg g.Refresh() } -func updateShadowRadial(g *canvas.RadialGradient) { +func updateShadowRadial(g *canvas.RadialGradient, fg color.Color) { if g == nil { return } - g.StartColor = theme.Color(theme.ColorNameShadow) + g.StartColor = fg g.Refresh() } -func updateShadowStart(g *canvas.LinearGradient) { +func updateShadowStart(g *canvas.LinearGradient, fg color.Color) { if g == nil { return } - g.StartColor = theme.Color(theme.ColorNameShadow) + g.StartColor = fg g.Refresh() } diff --git a/internal/widget/testdata/scroll/theme_changed.png b/internal/widget/testdata/scroll/theme_changed.png new file mode 100644 index 0000000000..e4a246bb7c Binary files /dev/null and b/internal/widget/testdata/scroll/theme_changed.png differ diff --git a/internal/widget/testdata/scroll/theme_initial.png b/internal/widget/testdata/scroll/theme_initial.png new file mode 100644 index 0000000000..c3410b97ae Binary files /dev/null and b/internal/widget/testdata/scroll/theme_initial.png differ diff --git a/widget/gridwrap.go b/widget/gridwrap.go index 858d483fe0..a164ccf735 100644 --- a/widget/gridwrap.go +++ b/widget/gridwrap.go @@ -82,7 +82,9 @@ func (l *GridWrap) CreateRenderer() fyne.WidgetRenderer { l.ExtendBaseWidget(l) if f := l.CreateItem; f != nil && l.itemMin.IsZero() { - l.itemMin = f().MinSize() + item := createItemAndApplyThemeScope(f, l) + + l.itemMin = item.MinSize() } layout := &fyne.Container{Layout: newGridWrapLayout(l)} @@ -119,8 +121,10 @@ func (l *GridWrap) scrollTo(id GridWrapItemID) { if l.scroller == nil { return } + + pad := l.Theme().Size(theme.SizeNamePadding) row := math.Floor(float64(id) / float64(l.ColumnCount())) - y := float32(row)*l.itemMin.Height + float32(row)*theme.Padding() + y := float32(row)*l.itemMin.Height + float32(row)*pad if y < l.scroller.Offset.Y { l.scroller.Offset.Y = y } else if size := l.scroller.Size(); y+l.itemMin.Height > l.scroller.Offset.Y+size.Height { @@ -331,7 +335,7 @@ func (l *GridWrap) UnselectAll() { } func (l *GridWrap) contentMinSize() fyne.Size { - padding := theme.Padding() + padding := l.Theme().Size(theme.SizeNamePadding) if l.Length == nil { return fyne.NewSize(0, 0) } @@ -368,7 +372,9 @@ func (l *gridWrapRenderer) MinSize() fyne.Size { func (l *gridWrapRenderer) Refresh() { if f := l.list.CreateItem; f != nil { - l.list.itemMin = f().MinSize() + item := createItemAndApplyThemeScope(f, l.list) + + l.list.itemMin = item.MinSize() } l.Layout(l.list.Size()) l.scroller.Refresh() @@ -410,9 +416,11 @@ func newGridWrapItem(child fyne.CanvasObject, tapped func()) *gridWrapItem { // CreateRenderer is a private method to Fyne which links this widget to its renderer. func (gw *gridWrapItem) CreateRenderer() fyne.WidgetRenderer { gw.ExtendBaseWidget(gw) + th := gw.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() - gw.background = canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - gw.background.CornerRadius = theme.SelectionRadiusSize() + gw.background = canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + gw.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) gw.background.Hide() objects := []fyne.CanvasObject{gw.background, gw.child} @@ -473,12 +481,15 @@ func (gw *gridWrapItemRenderer) Layout(size fyne.Size) { } func (gw *gridWrapItemRenderer) Refresh() { - gw.item.background.CornerRadius = theme.SelectionRadiusSize() + th := gw.item.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + gw.item.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) if gw.item.selected { - gw.item.background.FillColor = theme.Color(theme.ColorNameSelection) + gw.item.background.FillColor = th.Color(theme.ColorNameSelection, v) gw.item.background.Show() } else if gw.item.hovered { - gw.item.background.FillColor = theme.Color(theme.ColorNameHover) + gw.item.background.FillColor = th.Color(theme.ColorNameHover, v) gw.item.background.Show() } else { gw.item.background.Hide() @@ -526,7 +537,9 @@ func (l *gridWrapLayout) getItem() *gridWrapItem { item := l.itemPool.Obtain() if item == nil { if f := l.list.CreateItem; f != nil { - item = newGridWrapItem(f(), nil) + child := createItemAndApplyThemeScope(f, l.list) + + item = newGridWrapItem(child, nil) } } return item.(*gridWrapItem) @@ -580,7 +593,7 @@ func (l *gridWrapLayout) setupGridItem(li *gridWrapItem, id GridWrapItemID, focu // Since: 2.5 func (l *GridWrap) ColumnCount() int { if l.colCountCache < 1 { - padding := theme.Padding() + padding := l.Theme().Size(theme.SizeNamePadding) l.colCountCache = 1 width := l.Size().Width if width > l.itemMin.Width { @@ -592,7 +605,7 @@ func (l *GridWrap) ColumnCount() int { func (l *gridWrapLayout) updateGrid(refresh bool) { // code here is a mashup of listLayout.updateList and gridWrapLayout.Layout - padding := theme.Padding() + padding := l.list.Theme().Size(theme.SizeNamePadding) l.renderLock.Lock() length := 0 diff --git a/widget/list.go b/widget/list.go index 5be6143dcc..3b9adff487 100644 --- a/widget/list.go +++ b/widget/list.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/theme" ) @@ -86,7 +87,9 @@ func (l *List) CreateRenderer() fyne.WidgetRenderer { l.ExtendBaseWidget(l) if f := l.CreateItem; f != nil && l.itemMin.IsZero() { - l.itemMin = f().MinSize() + item := createItemAndApplyThemeScope(f, l) + + l.itemMin = item.MinSize() } layout := &fyne.Container{Layout: newListLayout(l)} @@ -162,7 +165,7 @@ func (l *List) scrollTo(id ListItemID) { return } - separatorThickness := theme.Padding() + separatorThickness := l.Theme().Size(theme.SizeNamePadding) y := float32(0) lastItemHeight := l.itemMin.Height if l.itemHeights == nil || len(l.itemHeights) == 0 { @@ -356,6 +359,7 @@ func (l *List) UnselectAll() { } func (l *List) contentMinSize() fyne.Size { + separatorThickness := l.Theme().Size(theme.SizeNamePadding) l.propertyLock.Lock() defer l.propertyLock.Unlock() if l.Length == nil { @@ -363,7 +367,6 @@ func (l *List) contentMinSize() fyne.Size { } items := l.Length() - separatorThickness := theme.Padding() if l.itemHeights == nil || len(l.itemHeights) == 0 { return fyne.NewSize(l.itemMin.Width, (l.itemMin.Height+separatorThickness)*float32(items)-separatorThickness) @@ -384,7 +387,7 @@ func (l *List) contentMinSize() fyne.Size { } // fills l.visibleRowHeights and also returns offY and minRow -func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int) (offY float32, minRow int) { +func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int, th fyne.Theme) (offY float32, minRow int) { rowOffset := float32(0) isVisible := false l.visibleRowHeights = l.visibleRowHeights[:0] @@ -393,8 +396,7 @@ func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int) return } - // theme.Padding is a slow call, so we cache it - padding := theme.Padding() + padding := th.Size(theme.SizeNamePadding) if len(l.list.itemHeights) == 0 { paddedItemHeight := itemHeight + padding @@ -473,11 +475,17 @@ func (l *listRenderer) MinSize() fyne.Size { func (l *listRenderer) Refresh() { if f := l.list.CreateItem; f != nil { - l.list.itemMin = f().MinSize() + item := createItemAndApplyThemeScope(f, l.list) + l.list.itemMin = item.MinSize() } l.Layout(l.list.Size()) l.scroller.Refresh() - l.layout.Layout.(*listLayout).updateList(false) + layout := l.layout.Layout.(*listLayout) + layout.updateList(false) + + for _, s := range layout.separators { + s.Refresh() + } canvas.Refresh(l.list.super()) } @@ -508,9 +516,11 @@ func newListItem(child fyne.CanvasObject, tapped func()) *listItem { // CreateRenderer is a private method to Fyne which links this widget to its renderer. func (li *listItem) CreateRenderer() fyne.WidgetRenderer { li.ExtendBaseWidget(li) + th := li.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() - li.background = canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - li.background.CornerRadius = theme.SelectionRadiusSize() + li.background = canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + li.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) li.background.Hide() objects := []fyne.CanvasObject{li.background, li.child} @@ -571,12 +581,15 @@ func (li *listItemRenderer) Layout(size fyne.Size) { } func (li *listItemRenderer) Refresh() { - li.item.background.CornerRadius = theme.SelectionRadiusSize() + th := li.item.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + li.item.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) if li.item.selected { - li.item.background.FillColor = theme.Color(theme.ColorNameSelection) + li.item.background.FillColor = th.Color(theme.ColorNameSelection, v) li.item.background.Show() } else if li.item.hovered { - li.item.background.FillColor = theme.Color(theme.ColorNameHover) + li.item.background.FillColor = th.Color(theme.ColorNameHover, v) li.item.background.Show() } else { li.item.background.Hide() @@ -627,7 +640,9 @@ func (l *listLayout) getItem() *listItem { item := l.itemPool.Obtain() if item == nil { if f := l.list.CreateItem; f != nil { - item = newListItem(f(), nil) + item2 := createItemAndApplyThemeScope(f, l.list) + + item = newListItem(item2, nil) } } return item.(*listItem) @@ -675,8 +690,9 @@ func (l *listLayout) setupListItem(li *listItem, id ListItemID, focus bool) { } func (l *listLayout) updateList(newOnly bool) { + th := l.list.Theme() + separatorThickness := th.Size(theme.SizeNamePadding) l.renderLock.Lock() - separatorThickness := theme.Padding() width := l.list.Size().Width length := 0 if f := l.list.Length; f != nil { @@ -693,7 +709,7 @@ func (l *listLayout) updateList(newOnly bool) { wasVisible = append(wasVisible, l.visible...) l.list.propertyLock.Lock() - offY, minRow := l.calculateVisibleRowHeights(l.list.itemMin.Height, length) + offY, minRow := l.calculateVisibleRowHeights(l.list.itemMin.Height, length, th) l.list.propertyLock.Unlock() if len(l.visibleRowHeights) == 0 && length > 0 { // we can't show anything until we have some dimensions l.renderLock.Unlock() // user code should not be locked @@ -786,15 +802,22 @@ func (l *listLayout) updateSeparators() { l.separators = l.separators[:lenChildren] } else { for i := lenSep; i < lenChildren; i++ { - l.separators = append(l.separators, NewSeparator()) + + sep := NewSeparator() + if cache.OverrideThemeMatchingScope(sep, l.list) { + sep.Refresh() + } + + l.separators = append(l.separators, sep) } } } else { l.separators = nil } - separatorThickness := theme.SeparatorThicknessSize() - dividerOff := (theme.Padding() + separatorThickness) / 2 + th := l.list.Theme() + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) + dividerOff := (th.Size(theme.SizeNamePadding) + separatorThickness) / 2 for i, child := range l.children { if i == 0 { continue @@ -832,3 +855,13 @@ func (l *listLayout) nilOldVisibleSliceData(objs []listItemAndID, len, oldLen in } } } + +func createItemAndApplyThemeScope(f func() fyne.CanvasObject, scope fyne.Widget) fyne.CanvasObject { + item := f() + if !cache.OverrideThemeMatchingScope(item, scope) { + return item + } + + item.Refresh() + return item +} diff --git a/widget/list_internal_test.go b/widget/list_internal_test.go new file mode 100644 index 0000000000..c5e72cc7ca --- /dev/null +++ b/widget/list_internal_test.go @@ -0,0 +1,682 @@ +package widget + +import ( + "fmt" + "image/color" + "testing" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/theme" + + "github.com/stretchr/testify/assert" +) + +func TestNewList(t *testing.T) { + list := createList(1000) + + content := &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ + NewIcon(theme.DocumentIcon()), + NewLabel("Template Object")}, + } + template := newListItem(content, nil) + + assert.Equal(t, 1000, list.Length()) + assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) + assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) + assert.Equal(t, float32(0), list.offsetY) +} + +func TestNewListWithData(t *testing.T) { + data := binding.NewStringList() + for i := 0; i < 1000; i++ { + data.Append(fmt.Sprintf("Test Item %d", i)) + } + + list := NewListWithData(data, + func() fyne.CanvasObject { + return NewLabel("Template Object") + }, + func(data binding.DataItem, item fyne.CanvasObject) { + item.(*Label).Bind(data.(binding.String)) + }, + ) + + template := NewLabel("Template Object") + + assert.Equal(t, 1000, list.Length()) + assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) + assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) + assert.Equal(t, float32(0), list.offsetY) +} + +func TestList_MinSize(t *testing.T) { + for name, tt := range map[string]struct { + cellSize fyne.Size + expectedMinSize fyne.Size + }{ + "small": { + fyne.NewSize(1, 1), + fyne.NewSize(float32(32), float32(32)), + }, + "large": { + fyne.NewSize(100, 100), + fyne.NewSize(100, 100), + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tt.expectedMinSize, NewList( + func() int { return 5 }, + func() fyne.CanvasObject { + r := canvas.NewRectangle(color.Black) + r.SetMinSize(tt.cellSize) + r.Resize(tt.cellSize) + return r + }, + func(ListItemID, fyne.CanvasObject) {}).MinSize()) + }) + } +} + +func TestList_Resize(t *testing.T) { + test.NewTempApp(t) + list, w := setupList(t) + + assert.Equal(t, float32(0), list.offsetY) + + w.Resize(fyne.NewSize(200, 600)) + + assert.Equal(t, float32(0), list.offsetY) + test.AssertRendersToMarkup(t, "list/resized.xml", w.Canvas()) + + // and check empty too + list = NewList( + func() int { + return 0 + }, + func() fyne.CanvasObject { + return NewButton("", func() {}) + }, + func(ListItemID, fyne.CanvasObject) { + }) + list.Resize(list.Size()) +} + +func TestList_SetItemHeight(t *testing.T) { + list := NewList( + func() int { return 5 }, + func() fyne.CanvasObject { + r := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0x33}) + r.SetMinSize(fyne.NewSize(10, 10)) + return r + }, + func(ListItemID, fyne.CanvasObject) { + }) + + lay := test.TempWidgetRenderer(t, list).(*listRenderer).layout + assert.Equal(t, fyne.NewSize(32, 32), list.MinSize()) + assert.Equal(t, fyne.NewSize(10, 10*5+(4*theme.Padding())), lay.MinSize()) + + list.SetItemHeight(2, 50) + assert.Equal(t, fyne.NewSize(10, 10*5+(4*theme.Padding())+40), lay.MinSize()) + + list.Select(2) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 200)) + test.AssertImageMatches(t, "list/list_item_height.png", w.Canvas().Capture()) +} + +func TestList_SetItemHeight_InUpdate(t *testing.T) { + var list *List + list = NewList( + func() int { return 5 }, + func() fyne.CanvasObject { + r := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0x33}) + r.SetMinSize(fyne.NewSize(10, 10)) + return r + }, + func(id ListItemID, o fyne.CanvasObject) { + list.SetItemHeight(id, 32) + }) + + done := make(chan struct{}) + go func() { + select { + case <-done: + case <-time.After(1 * time.Second): + assert.Fail(t, "Timed out waiting for list to complete refresh") + } + }() + list.Refresh() // could block + done <- struct{}{} +} + +func TestList_OffsetChange(t *testing.T) { + test.NewTempApp(t) + + list := createList(1000) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 400)) + + assert.Equal(t, float32(0), list.offsetY) + + scroll := test.TempWidgetRenderer(t, list).(*listRenderer).scroller + scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -280)}) + + assert.NotEqual(t, 0, list.offsetY) + test.AssertRendersToMarkup(t, "list/offset_changed.xml", w.Canvas()) +} + +func TestList_Hover(t *testing.T) { + list := createList(1000) + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + + for i := 0; i < 2; i++ { + assert.False(t, children[i].(*listItem).background.Visible()) + children[i].(*listItem).MouseIn(&desktop.MouseEvent{}) + assert.Equal(t, children[i].(*listItem).background.FillColor, theme.Color(theme.ColorNameHover)) + children[i].(*listItem).MouseOut() + assert.False(t, children[i].(*listItem).background.Visible()) + } +} + +func TestList_ScrollTo(t *testing.T) { + list := createList(1000) + + offset := 0 + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + list.ScrollTo(20) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + offset = 6850 + list.ScrollTo(200) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + offset = 38074 + list.ScrollTo(999) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + offset = 19539 + list.ScrollTo(500) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + list.ScrollTo(1000) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + offset = 39 + list.ScrollTo(1) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) +} + +func TestList_ScrollToBottom(t *testing.T) { + list := createList(1000) + + offset := 38074 + list.ScrollToBottom() + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) +} + +func TestList_ScrollToTop(t *testing.T) { + list := createList(1000) + + offset := float32(0) + list.ScrollToTop() + assert.Equal(t, offset, list.offsetY) + assert.Equal(t, offset, list.scroller.Offset.Y) +} + +func TestList_ScrollOffset(t *testing.T) { + list := createList(10) + list.Resize(fyne.NewSize(20, 15)) + + offset := float32(25) + list.ScrollToOffset(25) + assert.Equal(t, offset, list.GetScrollOffset()) + + list.ScrollToOffset(-2) + assert.Equal(t, float32(0), list.GetScrollOffset()) + + list.ScrollToOffset(1000) + assert.LessOrEqual(t, list.GetScrollOffset(), float32(500) /*upper bound on content height*/) + + // list viewport is larger than content size + list.Resize(fyne.NewSize(100, 500)) + list.ScrollToOffset(20) + assert.Equal(t, float32(0), list.GetScrollOffset()) // doesn't scroll +} + +func TestList_Selection(t *testing.T) { + list := createList(1000) + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + + assert.False(t, children[0].(*listItem).background.Visible()) + children[0].(*listItem).Tapped(&fyne.PointEvent{}) + assert.Equal(t, children[0].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, children[0].(*listItem).background.Visible()) + assert.Equal(t, 1, len(list.selected)) + assert.Equal(t, 0, list.selected[0]) + children[1].(*listItem).Tapped(&fyne.PointEvent{}) + assert.Equal(t, children[1].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, children[1].(*listItem).background.Visible()) + assert.Equal(t, 1, len(list.selected)) + assert.Equal(t, 1, list.selected[0]) + assert.False(t, children[0].(*listItem).background.Visible()) + + offset := 0 + list.SetItemHeight(2, 220) + list.SetItemHeight(3, 220) + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) + + list.Select(200) + offset = 7220 + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) +} + +func TestList_Select(t *testing.T) { + list := createList(1000) + + assert.Equal(t, float32(0), list.offsetY) + list.Select(50) + assert.Equal(t, 988, int(list.offsetY)) + lo := list.scroller.Content.(*fyne.Container).Layout.(*listLayout) + visible50, _ := lo.searchVisible(lo.visible, 50) + assert.Equal(t, visible50.background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, visible50.background.Visible()) + + list.Select(5) + assert.Equal(t, 195, int(list.offsetY)) + visible5, _ := lo.searchVisible(lo.visible, 5) + assert.Equal(t, visible5.background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, visible5.background.Visible()) + + list.Select(6) + assert.Equal(t, 195, int(list.offsetY)) + visible5, _ = lo.searchVisible(lo.visible, 5) + visible6, _ := lo.searchVisible(lo.visible, 6) + assert.False(t, visible5.background.Visible()) + assert.Equal(t, visible6.background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, visible6.background.Visible()) +} + +func TestList_Unselect(t *testing.T) { + list := createList(1000) + var unselected ListItemID + list.OnUnselected = func(id ListItemID) { + unselected = id + } + + list.Select(10) + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.Equal(t, children[10].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) + assert.True(t, children[10].(*listItem).background.Visible()) + + list.Unselect(10) + children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.False(t, children[10].(*listItem).background.Visible()) + assert.Nil(t, list.selected) + assert.Equal(t, 10, unselected) + + unselected = -1 + list.Select(11) + list.Unselect(9) + assert.Equal(t, 1, len(list.selected)) + assert.Equal(t, -1, unselected) + + list.UnselectAll() + assert.Nil(t, list.selected) + assert.Equal(t, 11, unselected) +} + +func TestList_DataChange(t *testing.T) { + test.NewTempApp(t) + + list, w := setupList(t) + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "Test Item 0") + changeData(list) + list.Refresh() + children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "a") + test.AssertRendersToMarkup(t, "list/new_data.xml", w.Canvas()) +} + +func TestList_ItemDataChange(t *testing.T) { + test.NewTempApp(t) + + list, _ := setupList(t) + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "Test Item 0") + changeData(list) + list.RefreshItem(0) + children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "a") +} + +func TestList_SmallList(t *testing.T) { + test.NewTempApp(t) + + var data []string + data = append(data, "Test Item 0") + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ + NewIcon(theme.DocumentIcon()), + NewLabel("Template Object")}, + } + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) + }, + ) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 400)) + + visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + assert.Equal(t, visibleCount, 1) + + data = append(data, "Test Item 1") + list.Refresh() + + visibleCount = len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + assert.Equal(t, visibleCount, 2) + + test.AssertRendersToMarkup(t, "list/small.xml", w.Canvas()) +} + +func TestList_ClearList(t *testing.T) { + test.NewTempApp(t) + list, w := setupList(t) + assert.Equal(t, 1000, list.Length()) + + list.Length = func() int { + return 0 + } + list.Refresh() + + visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + + assert.Equal(t, 0, visibleCount) + test.AssertRendersToMarkup(t, "list/cleared.xml", w.Canvas()) +} + +func TestList_RemoveItem(t *testing.T) { + test.NewTempApp(t) + + data := []string{"Test Item 0", "Test Item 1", "Test Item 2"} + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ + NewIcon(theme.DocumentIcon()), + NewLabel("Template Object")}, + } + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) + }, + ) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 400)) + + visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + assert.Equal(t, visibleCount, 3) + + data = data[:len(data)-1] + list.Refresh() + + visibleCount = len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + assert.Equal(t, visibleCount, 2) + test.AssertRendersToMarkup(t, "list/item_removed.xml", w.Canvas()) +} + +func TestList_ScrollThenShrink(t *testing.T) { + test.NewTempApp(t) + + data := make([]string, 0, 20) + for i := 0; i < 20; i++ { + data = append(data, fmt.Sprintf("Data %d", i)) + } + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return NewLabel("TEMPLATE") + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*Label).SetText(data[id]) + }, + ) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(300, 300)) + + visibles := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + visibleCount := len(visibles) + assert.Equal(t, visibleCount, 9) + + list.scroller.ScrollToBottom() + visibles = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.Equal(t, "Data 19", visibles[len(visibles)-1].(*listItem).child.(*Label).Text) + + data = data[:1] + assert.NotPanics(t, func() { list.Refresh() }) + + visibles = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + visibleCount = len(visibles) + assert.Equal(t, visibleCount, 1) + assert.Equal(t, "Data 0", visibles[0].(*listItem).child.(*Label).Text) +} + +func TestList_ScrollThenResizeWindow(t *testing.T) { + test.NewTempApp(t) + + data := make([]string, 0, 20) + for i := 0; i < 20; i++ { + data = append(data, fmt.Sprintf("Data %d", i)) + } + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return NewLabel("TEMPLATE") + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*Label).SetText(data[id]) + }, + ) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(300, 300)) + + list.scroller.ScrollToBottom() + + // increase window size enough so that all elements are visible + w.Resize(fyne.NewSize(300, 1000)) + + visibles := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + visibleCount := len(visibles) + assert.Equal(t, 20, visibleCount) + assert.Equal(t, "Data 0", visibles[0].(*listItem).child.(*Label).Text) +} + +func TestList_NoFunctionsSet(t *testing.T) { + list := &List{} + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 400)) + list.Refresh() +} + +func TestList_Focus(t *testing.T) { + test.NewTempApp(t) + list := createList(10) + window := test.NewWindow(list) + defer window.Close() + window.Resize(list.MinSize().Max(fyne.NewSize(150, 200))) + + canvas := window.Canvas().(test.WindowlessCanvas) + assert.Nil(t, canvas.Focused()) + + canvas.FocusNext() + assert.NotNil(t, canvas.Focused()) + assert.Equal(t, 0, canvas.Focused().(*List).currentFocus) + + children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children + assert.True(t, children[0].(*listItem).hovered) + assert.False(t, children[1].(*listItem).hovered) + assert.False(t, children[2].(*listItem).hovered) + + list.TypedKey(&fyne.KeyEvent{Name: fyne.KeyDown}) + assert.False(t, children[0].(*listItem).hovered) + assert.True(t, children[1].(*listItem).hovered) + assert.False(t, children[2].(*listItem).hovered) + + list.TypedKey(&fyne.KeyEvent{Name: fyne.KeyUp}) + assert.True(t, children[0].(*listItem).hovered) + assert.False(t, children[1].(*listItem).hovered) + assert.False(t, children[2].(*listItem).hovered) + + canvas.Focused().TypedKey(&fyne.KeyEvent{Name: fyne.KeySpace}) + assert.True(t, children[0].(*listItem).selected) +} + +func createList(items int) *List { + var data []string + for i := 0; i < items; i++ { + data = append(data, fmt.Sprintf("Test Item %d", i)) + } + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + icon := NewIcon(theme.DocumentIcon()) + return &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, icon, nil), Objects: []fyne.CanvasObject{icon, NewLabel("Template Object")}} + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) + }, + ) + list.Resize(fyne.NewSize(200, 1000)) + return list +} + +func changeData(list *List) { + data := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} + list.Length = func() int { + return len(data) + } + list.UpdateItem = func(id ListItemID, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) + } +} + +func setupList(t *testing.T) (*List, fyne.Window) { + test.NewApp() + list := createList(1000) + w := test.NewTempWindow(t, list) + w.Resize(fyne.NewSize(200, 400)) + test.AssertRendersToMarkup(t, "list/initial.xml", w.Canvas()) + return list, w +} + +func TestList_LimitUpdateItem(t *testing.T) { + app := test.NewApp() + w := app.NewWindow("") + defer w.Close() + printOut := "" + list := NewList( + func() int { + return 5 + }, + func() fyne.CanvasObject { + return NewLabel("") + }, + func(id ListItemID, item fyne.CanvasObject) { + printOut += fmt.Sprintf("%d.", id) + }, + ) + w.SetContent(list) + w.ShowAndRun() + assert.Equal(t, "0.1.0.1.", printOut) + list.scrollTo(1) + assert.Equal(t, "0.1.0.1.2.", printOut) + list.scrollTo(2) + assert.Equal(t, "0.1.0.1.2.3.", printOut) +} + +func TestList_RefreshUpdatesAllItems(t *testing.T) { + app := test.NewApp() + w := app.NewWindow("") + defer w.Close() + printOut := "" + list := NewList( + func() int { + return 1 + }, + func() fyne.CanvasObject { + return NewLabel("Test") + }, + func(id ListItemID, item fyne.CanvasObject) { + printOut += fmt.Sprintf("%d.", id) + }, + ) + w.SetContent(list) + w.ShowAndRun() + assert.Equal(t, "0.", printOut) + + list.Refresh() + assert.Equal(t, "0.0.", printOut) +} + +var minSize fyne.Size + +func BenchmarkContentMinSize(b *testing.B) { + b.StopTimer() + + l := NewList( + func() int { return 1000000 }, + func() fyne.CanvasObject { + return NewLabel("Test") + }, + func(id ListItemID, item fyne.CanvasObject) { + item.(*Label).SetText(fmt.Sprintf("%d", id)) + }, + ) + l.SetItemHeight(10, 55) + l.SetItemHeight(12345, 2) + + min := fyne.Size{} + b.StartTimer() + for i := 0; i < b.N; i++ { + min = l.contentMinSize() + } + + minSize = min +} diff --git a/widget/list_test.go b/widget/list_test.go index 06a4662adb..aa2f3f1389 100644 --- a/widget/list_test.go +++ b/widget/list_test.go @@ -1,376 +1,19 @@ -package widget +package widget_test import ( "fmt" - "image/color" "testing" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/data/binding" - "fyne.io/fyne/v2/driver/desktop" - "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" - - "github.com/stretchr/testify/assert" + "fyne.io/fyne/v2/widget" ) -func TestNewList(t *testing.T) { - list := createList(1000) - - content := &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ - NewIcon(theme.DocumentIcon()), - NewLabel("Template Object")}, - } - template := newListItem(content, nil) - - assert.Equal(t, 1000, list.Length()) - assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) - assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) - assert.Equal(t, float32(0), list.offsetY) -} - -func TestNewListWithData(t *testing.T) { - data := binding.NewStringList() - for i := 0; i < 1000; i++ { - data.Append(fmt.Sprintf("Test Item %d", i)) - } - - list := NewListWithData(data, - func() fyne.CanvasObject { - return NewLabel("Template Object") - }, - func(data binding.DataItem, item fyne.CanvasObject) { - item.(*Label).Bind(data.(binding.String)) - }, - ) - - template := NewLabel("Template Object") - - assert.Equal(t, 1000, list.Length()) - assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) - assert.Equal(t, list.MinSize(), template.MinSize().Max(test.TempWidgetRenderer(t, list).(*listRenderer).scroller.MinSize())) - assert.Equal(t, float32(0), list.offsetY) -} - -func TestList_MinSize(t *testing.T) { - for name, tt := range map[string]struct { - cellSize fyne.Size - expectedMinSize fyne.Size - }{ - "small": { - fyne.NewSize(1, 1), - fyne.NewSize(float32(32), float32(32)), - }, - "large": { - fyne.NewSize(100, 100), - fyne.NewSize(100, 100), - }, - } { - t.Run(name, func(t *testing.T) { - assert.Equal(t, tt.expectedMinSize, NewList( - func() int { return 5 }, - func() fyne.CanvasObject { - r := canvas.NewRectangle(color.Black) - r.SetMinSize(tt.cellSize) - r.Resize(tt.cellSize) - return r - }, - func(ListItemID, fyne.CanvasObject) {}).MinSize()) - }) - } -} - -func TestList_Resize(t *testing.T) { - test.NewTempApp(t) - list, w := setupList(t) - - assert.Equal(t, float32(0), list.offsetY) - - w.Resize(fyne.NewSize(200, 600)) - - assert.Equal(t, float32(0), list.offsetY) - test.AssertRendersToMarkup(t, "list/resized.xml", w.Canvas()) - - // and check empty too - list = NewList( - func() int { - return 0 - }, - func() fyne.CanvasObject { - return NewButton("", func() {}) - }, - func(ListItemID, fyne.CanvasObject) { - }) - list.Resize(list.Size()) -} - -func TestList_SetItemHeight(t *testing.T) { - list := NewList( - func() int { return 5 }, - func() fyne.CanvasObject { - r := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0x33}) - r.SetMinSize(fyne.NewSize(10, 10)) - return r - }, - func(ListItemID, fyne.CanvasObject) { - }) - - lay := test.TempWidgetRenderer(t, list).(*listRenderer).layout - assert.Equal(t, fyne.NewSize(32, 32), list.MinSize()) - assert.Equal(t, fyne.NewSize(10, 10*5+(4*theme.Padding())), lay.MinSize()) - - list.SetItemHeight(2, 50) - assert.Equal(t, fyne.NewSize(10, 10*5+(4*theme.Padding())+40), lay.MinSize()) - - list.Select(2) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 200)) - test.AssertImageMatches(t, "list/list_item_height.png", w.Canvas().Capture()) -} - -func TestList_SetItemHeight_InUpdate(t *testing.T) { - var list *List - list = NewList( - func() int { return 5 }, - func() fyne.CanvasObject { - r := canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 0x33}) - r.SetMinSize(fyne.NewSize(10, 10)) - return r - }, - func(id ListItemID, o fyne.CanvasObject) { - list.SetItemHeight(id, 32) - }) - - done := make(chan struct{}) - go func() { - select { - case <-done: - case <-time.After(1 * time.Second): - assert.Fail(t, "Timed out waiting for list to complete refresh") - } - }() - list.Refresh() // could block - done <- struct{}{} -} - -func TestList_OffsetChange(t *testing.T) { - test.NewTempApp(t) - - list := createList(1000) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 400)) - - assert.Equal(t, float32(0), list.offsetY) - - scroll := test.TempWidgetRenderer(t, list).(*listRenderer).scroller - scroll.Scrolled(&fyne.ScrollEvent{Scrolled: fyne.NewDelta(0, -280)}) - - assert.NotEqual(t, 0, list.offsetY) - test.AssertRendersToMarkup(t, "list/offset_changed.xml", w.Canvas()) -} - -func TestList_Hover(t *testing.T) { - list := createList(1000) - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - - for i := 0; i < 2; i++ { - assert.False(t, children[i].(*listItem).background.Visible()) - children[i].(*listItem).MouseIn(&desktop.MouseEvent{}) - assert.Equal(t, children[i].(*listItem).background.FillColor, theme.Color(theme.ColorNameHover)) - children[i].(*listItem).MouseOut() - assert.False(t, children[i].(*listItem).background.Visible()) - } -} - -func TestList_ScrollTo(t *testing.T) { - list := createList(1000) - - offset := 0 - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - list.ScrollTo(20) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - offset = 6850 - list.ScrollTo(200) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - offset = 38074 - list.ScrollTo(999) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - offset = 19539 - list.ScrollTo(500) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - list.ScrollTo(1000) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - offset = 39 - list.ScrollTo(1) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) -} - -func TestList_ScrollToBottom(t *testing.T) { - list := createList(1000) - - offset := 38074 - list.ScrollToBottom() - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) -} - -func TestList_ScrollToTop(t *testing.T) { - list := createList(1000) - - offset := float32(0) - list.ScrollToTop() - assert.Equal(t, offset, list.offsetY) - assert.Equal(t, offset, list.scroller.Offset.Y) -} - -func TestList_ScrollOffset(t *testing.T) { - list := createList(10) - list.Resize(fyne.NewSize(20, 15)) - - offset := float32(25) - list.ScrollToOffset(25) - assert.Equal(t, offset, list.GetScrollOffset()) - - list.ScrollToOffset(-2) - assert.Equal(t, float32(0), list.GetScrollOffset()) - - list.ScrollToOffset(1000) - assert.LessOrEqual(t, list.GetScrollOffset(), float32(500) /*upper bound on content height*/) - - // list viewport is larger than content size - list.Resize(fyne.NewSize(100, 500)) - list.ScrollToOffset(20) - assert.Equal(t, float32(0), list.GetScrollOffset()) // doesn't scroll -} - -func TestList_Selection(t *testing.T) { - list := createList(1000) - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - - assert.False(t, children[0].(*listItem).background.Visible()) - children[0].(*listItem).Tapped(&fyne.PointEvent{}) - assert.Equal(t, children[0].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, children[0].(*listItem).background.Visible()) - assert.Equal(t, 1, len(list.selected)) - assert.Equal(t, 0, list.selected[0]) - children[1].(*listItem).Tapped(&fyne.PointEvent{}) - assert.Equal(t, children[1].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, children[1].(*listItem).background.Visible()) - assert.Equal(t, 1, len(list.selected)) - assert.Equal(t, 1, list.selected[0]) - assert.False(t, children[0].(*listItem).background.Visible()) - - offset := 0 - list.SetItemHeight(2, 220) - list.SetItemHeight(3, 220) - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) - - list.Select(200) - offset = 7220 - assert.Equal(t, offset, int(list.offsetY)) - assert.Equal(t, offset, int(list.scroller.Offset.Y)) -} - -func TestList_Select(t *testing.T) { - list := createList(1000) - - assert.Equal(t, float32(0), list.offsetY) - list.Select(50) - assert.Equal(t, 988, int(list.offsetY)) - lo := list.scroller.Content.(*fyne.Container).Layout.(*listLayout) - visible50, _ := lo.searchVisible(lo.visible, 50) - assert.Equal(t, visible50.background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, visible50.background.Visible()) - - list.Select(5) - assert.Equal(t, 195, int(list.offsetY)) - visible5, _ := lo.searchVisible(lo.visible, 5) - assert.Equal(t, visible5.background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, visible5.background.Visible()) - - list.Select(6) - assert.Equal(t, 195, int(list.offsetY)) - visible5, _ = lo.searchVisible(lo.visible, 5) - visible6, _ := lo.searchVisible(lo.visible, 6) - assert.False(t, visible5.background.Visible()) - assert.Equal(t, visible6.background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, visible6.background.Visible()) -} - -func TestList_Unselect(t *testing.T) { - list := createList(1000) - var unselected ListItemID - list.OnUnselected = func(id ListItemID) { - unselected = id - } - - list.Select(10) - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.Equal(t, children[10].(*listItem).background.FillColor, theme.Color(theme.ColorNameSelection)) - assert.True(t, children[10].(*listItem).background.Visible()) - - list.Unselect(10) - children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.False(t, children[10].(*listItem).background.Visible()) - assert.Nil(t, list.selected) - assert.Equal(t, 10, unselected) - - unselected = -1 - list.Select(11) - list.Unselect(9) - assert.Equal(t, 1, len(list.selected)) - assert.Equal(t, -1, unselected) - - list.UnselectAll() - assert.Nil(t, list.selected) - assert.Equal(t, 11, unselected) -} - -func TestList_DataChange(t *testing.T) { - test.NewTempApp(t) - - list, w := setupList(t) - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - - assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "Test Item 0") - changeData(list) - list.Refresh() - children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "a") - test.AssertRendersToMarkup(t, "list/new_data.xml", w.Canvas()) -} - -func TestList_ItemDataChange(t *testing.T) { - test.NewTempApp(t) - - list, _ := setupList(t) - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "Test Item 0") - changeData(list) - list.RefreshItem(0) - children = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "a") -} - func TestList_ThemeChange(t *testing.T) { - test.NewTempApp(t) list, w := setupList(t) test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) @@ -382,314 +25,34 @@ func TestList_ThemeChange(t *testing.T) { }) } -func TestList_SmallList(t *testing.T) { - test.NewTempApp(t) - - var data []string - data = append(data, "Test Item 0") - - list := NewList( - func() int { - return len(data) - }, - func() fyne.CanvasObject { - return &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ - NewIcon(theme.DocumentIcon()), - NewLabel("Template Object")}, - } - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) - }, - ) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 400)) - - visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) - assert.Equal(t, visibleCount, 1) - - data = append(data, "Test Item 1") - list.Refresh() - - visibleCount = len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) - assert.Equal(t, visibleCount, 2) - - test.AssertRendersToMarkup(t, "list/small.xml", w.Canvas()) -} - -func TestList_ClearList(t *testing.T) { - test.NewTempApp(t) +func TestList_ThemeOverride(t *testing.T) { list, w := setupList(t) - assert.Equal(t, 1000, list.Length()) - - list.Length = func() int { - return 0 - } - list.Refresh() - visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) + test.ApplyTheme(t, test.NewTheme()) + test.AssertImageMatches(t, "list/list_theme_changed.png", w.Canvas().Capture()) - assert.Equal(t, 0, visibleCount) - test.AssertRendersToMarkup(t, "list/cleared.xml", w.Canvas()) -} - -func TestList_RemoveItem(t *testing.T) { - test.NewTempApp(t) - - data := []string{"Test Item 0", "Test Item 1", "Test Item 2"} - - list := NewList( - func() int { - return len(data) - }, - func() fyne.CanvasObject { - return &fyne.Container{Layout: layout.NewHBoxLayout(), Objects: []fyne.CanvasObject{ - NewIcon(theme.DocumentIcon()), - NewLabel("Template Object")}, - } - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) - }, - ) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 400)) - - visibleCount := len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) - assert.Equal(t, visibleCount, 3) - - data = data[:len(data)-1] - list.Refresh() - - visibleCount = len(list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children) - assert.Equal(t, visibleCount, 2) - test.AssertRendersToMarkup(t, "list/item_removed.xml", w.Canvas()) -} - -func TestList_ScrollThenShrink(t *testing.T) { - test.NewTempApp(t) - - data := make([]string, 0, 20) - for i := 0; i < 20; i++ { - data = append(data, fmt.Sprintf("Data %d", i)) - } - - list := NewList( - func() int { - return len(data) - }, - func() fyne.CanvasObject { - return NewLabel("TEMPLATE") - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*Label).SetText(data[id]) - }, - ) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(300, 300)) - - visibles := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - visibleCount := len(visibles) - assert.Equal(t, visibleCount, 9) - - list.scroller.ScrollToBottom() - visibles = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.Equal(t, "Data 19", visibles[len(visibles)-1].(*listItem).child.(*Label).Text) - - data = data[:1] - assert.NotPanics(t, func() { list.Refresh() }) - - visibles = list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - visibleCount = len(visibles) - assert.Equal(t, visibleCount, 1) - assert.Equal(t, "Data 0", visibles[0].(*listItem).child.(*Label).Text) + normal := test.Theme() + bg := canvas.NewRectangle(normal.Color(theme.ColorNameBackground, theme.VariantDark)) + w.SetContent(container.NewStack(bg, container.NewThemeOverride(list, normal))) + w.Resize(fyne.NewSize(200, 200)) + test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) } -func TestList_ScrollThenResizeWindow(t *testing.T) { +func setupList(t *testing.T) (*widget.List, fyne.Window) { test.NewTempApp(t) - - data := make([]string, 0, 20) - for i := 0; i < 20; i++ { - data = append(data, fmt.Sprintf("Data %d", i)) - } - - list := NewList( + list := widget.NewList( func() int { - return len(data) + return 25 }, func() fyne.CanvasObject { - return NewLabel("TEMPLATE") - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*Label).SetText(data[id]) + return widget.NewLabel("Test Item 55") }, - ) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(300, 300)) - - list.scroller.ScrollToBottom() - - // increase window size enough so that all elements are visible - w.Resize(fyne.NewSize(300, 1000)) - - visibles := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - visibleCount := len(visibles) - assert.Equal(t, 20, visibleCount) - assert.Equal(t, "Data 0", visibles[0].(*listItem).child.(*Label).Text) -} - -func TestList_NoFunctionsSet(t *testing.T) { - list := &List{} + func(id widget.ListItemID, o fyne.CanvasObject) { + o.(*widget.Label).SetText(fmt.Sprintf("Test Item %d", id)) + }) w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 400)) - list.Refresh() -} - -func TestList_Focus(t *testing.T) { - test.NewTempApp(t) - list := createList(10) - window := test.NewWindow(list) - defer window.Close() - window.Resize(list.MinSize().Max(fyne.NewSize(150, 200))) - - canvas := window.Canvas().(test.WindowlessCanvas) - assert.Nil(t, canvas.Focused()) - - canvas.FocusNext() - assert.NotNil(t, canvas.Focused()) - assert.Equal(t, 0, canvas.Focused().(*List).currentFocus) - - children := list.scroller.Content.(*fyne.Container).Layout.(*listLayout).children - assert.True(t, children[0].(*listItem).hovered) - assert.False(t, children[1].(*listItem).hovered) - assert.False(t, children[2].(*listItem).hovered) - - list.TypedKey(&fyne.KeyEvent{Name: fyne.KeyDown}) - assert.False(t, children[0].(*listItem).hovered) - assert.True(t, children[1].(*listItem).hovered) - assert.False(t, children[2].(*listItem).hovered) - - list.TypedKey(&fyne.KeyEvent{Name: fyne.KeyUp}) - assert.True(t, children[0].(*listItem).hovered) - assert.False(t, children[1].(*listItem).hovered) - assert.False(t, children[2].(*listItem).hovered) - - canvas.Focused().TypedKey(&fyne.KeyEvent{Name: fyne.KeySpace}) - assert.True(t, children[0].(*listItem).selected) -} - -func createList(items int) *List { - var data []string - for i := 0; i < items; i++ { - data = append(data, fmt.Sprintf("Test Item %d", i)) - } - - list := NewList( - func() int { - return len(data) - }, - func() fyne.CanvasObject { - icon := NewIcon(theme.DocumentIcon()) - return &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, icon, nil), Objects: []fyne.CanvasObject{icon, NewLabel("Template Object")}} - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) - }, - ) - list.Resize(fyne.NewSize(200, 1000)) - return list -} - -func changeData(list *List) { - data := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} - list.Length = func() int { - return len(data) - } - list.UpdateItem = func(id ListItemID, item fyne.CanvasObject) { - item.(*fyne.Container).Objects[1].(*Label).SetText(data[id]) - } -} + w.SetPadded(false) + w.Resize(fyne.NewSize(200, 200)) -func setupList(t *testing.T) (*List, fyne.Window) { - test.NewApp() - list := createList(1000) - w := test.NewTempWindow(t, list) - w.Resize(fyne.NewSize(200, 400)) - test.AssertRendersToMarkup(t, "list/initial.xml", w.Canvas()) return list, w } - -func TestList_LimitUpdateItem(t *testing.T) { - app := test.NewApp() - w := app.NewWindow("") - defer w.Close() - printOut := "" - list := NewList( - func() int { - return 5 - }, - func() fyne.CanvasObject { - return NewLabel("") - }, - func(id ListItemID, item fyne.CanvasObject) { - printOut += fmt.Sprintf("%d.", id) - }, - ) - w.SetContent(list) - w.ShowAndRun() - assert.Equal(t, "0.1.0.1.", printOut) - list.scrollTo(1) - assert.Equal(t, "0.1.0.1.2.", printOut) - list.scrollTo(2) - assert.Equal(t, "0.1.0.1.2.3.", printOut) -} - -func TestList_RefreshUpdatesAllItems(t *testing.T) { - app := test.NewApp() - w := app.NewWindow("") - defer w.Close() - printOut := "" - list := NewList( - func() int { - return 1 - }, - func() fyne.CanvasObject { - return NewLabel("Test") - }, - func(id ListItemID, item fyne.CanvasObject) { - printOut += fmt.Sprintf("%d.", id) - }, - ) - w.SetContent(list) - w.ShowAndRun() - assert.Equal(t, "0.", printOut) - - list.Refresh() - assert.Equal(t, "0.0.", printOut) -} - -var minSize fyne.Size - -func BenchmarkContentMinSize(b *testing.B) { - b.StopTimer() - - l := NewList( - func() int { return 1000000 }, - func() fyne.CanvasObject { - return NewLabel("Test") - }, - func(id ListItemID, item fyne.CanvasObject) { - item.(*Label).SetText(fmt.Sprintf("%d", id)) - }, - ) - l.SetItemHeight(10, 55) - l.SetItemHeight(12345, 2) - - min := fyne.Size{} - b.StartTimer() - for i := 0; i < b.N; i++ { - min = l.contentMinSize() - } - - minSize = min -} diff --git a/widget/table.go b/widget/table.go index 38ebcc9881..cd670aaa48 100644 --- a/widget/table.go +++ b/widget/table.go @@ -614,7 +614,7 @@ func (t *Table) columnAt(pos fyne.Position) int { pos.X += t.content.Offset.X offX += t.stuckXOff } - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) for x := offX; i < end; x += visibleColWidths[i-1] + padding { if pos.X < x { return -i // the space between i-1 and i @@ -639,7 +639,7 @@ func (t *Table) createHeader() fyne.CanvasObject { func (t *Table) findX(col int) (cellX float32, cellWidth float32) { cellSize := t.templateSize() - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) for i := 0; i <= col; i++ { if cellWidth > 0 { cellX += cellWidth + padding @@ -656,7 +656,7 @@ func (t *Table) findX(col int) (cellX float32, cellWidth float32) { func (t *Table) findY(row int) (cellY float32, cellHeight float32) { cellSize := t.templateSize() - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) for i := 0; i <= row; i++ { if cellHeight > 0 { cellY += cellHeight + padding @@ -745,7 +745,7 @@ func (t *Table) rowAt(pos fyne.Position) int { pos.Y += t.content.Offset.Y offY += t.stuckYOff } - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) for y := offY; i < end; y += visibleRowHeights[i-1] + padding { if pos.Y < y { return -i // the space between i-1 and i @@ -782,7 +782,7 @@ func (t *Table) tapped(pos fyne.Position) { func (t *Table) templateSize() fyne.Size { if f := t.CreateCell; f != nil { - template := f() // don't use cache, we need new template + template := createItemAndApplyThemeScope(f, t) // don't use cache, we need new template if !t.ShowHeaderRow && !t.ShowHeaderColumn { return template.MinSize() } @@ -856,8 +856,7 @@ func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int return } - // theme.Padding is a slow call, so we cache it - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) stick := t.StickyColumnCount size := t.size.Load() @@ -957,8 +956,7 @@ func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int] return } - // theme.Padding is a slow call, so we cache it - padding := theme.Padding() + padding := t.Theme().Size(theme.SizeNamePadding) stick := t.StickyRowCount size := t.size.Load() @@ -1026,9 +1024,10 @@ type tableRenderer struct { } func (t *tableRenderer) Layout(s fyne.Size) { + th := t.t.Theme() t.t.propertyLock.RLock() - t.calculateHeaderSizes() + t.calculateHeaderSizes(th) off := fyne.NewPos(t.t.stuckWidth, t.t.stuckHeight) if t.t.ShowHeaderRow { off.Y += t.t.headerSize.Height @@ -1056,7 +1055,7 @@ func (t *tableRenderer) Layout(s fyne.Size) { } func (t *tableRenderer) MinSize() fyne.Size { - sep := theme.Padding() + sep := t.t.Theme().Size(theme.SizeNamePadding) t.t.propertyLock.RLock() defer t.t.propertyLock.RUnlock() @@ -1091,6 +1090,7 @@ func (t *tableRenderer) MinSize() fyne.Size { } func (t *tableRenderer) Refresh() { + th := t.t.Theme() t.t.propertyLock.Lock() t.t.headerSize = t.t.createHeader().MinSize() if t.t.columnWidths != nil { @@ -1104,14 +1104,14 @@ func (t *tableRenderer) Refresh() { } } t.t.cellSize = t.t.templateSize() - t.calculateHeaderSizes() + t.calculateHeaderSizes(th) t.t.propertyLock.Unlock() t.Layout(t.t.Size()) t.t.cells.Refresh() } -func (t *tableRenderer) calculateHeaderSizes() { +func (t *tableRenderer) calculateHeaderSizes(th fyne.Theme) { t.t.stuckXOff = 0 t.t.stuckYOff = 0 @@ -1122,7 +1122,7 @@ func (t *tableRenderer) calculateHeaderSizes() { t.t.stuckXOff = t.t.headerSize.Width } - separatorThickness := theme.Padding() + separatorThickness := th.Size(theme.SizeNamePadding) stickyColWidths := t.t.stickyColumnWidths(t.t.cellSize.Width, t.t.StickyColumnCount) stickyRowHeights := t.t.stickyRowHeights(t.t.cellSize.Height, t.t.StickyRowCount) @@ -1153,15 +1153,17 @@ func newTableCells(t *Table) *tableCells { } func (c *tableCells) CreateRenderer() fyne.WidgetRenderer { - marker := canvas.NewRectangle(theme.Color(theme.ColorNameSelection)) - marker.CornerRadius = theme.SelectionRadiusSize() - hover := canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - hover.CornerRadius = theme.SelectionRadiusSize() + th := c.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + marker := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v)) + marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + hover := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius) r := &tableCellsRenderer{cells: c, pool: &syncPool{}, headerPool: &syncPool{}, visible: make(map[TableCellID]fyne.CanvasObject), headers: make(map[TableCellID]fyne.CanvasObject), - headRowBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), headColBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), - headRowStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), headColStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), + headRowBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), + headRowStickyBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), marker: marker, hover: hover} c.t.moveCallback = r.moveIndicators @@ -1237,7 +1239,7 @@ func (r *tableCellsRenderer) MinSize() fyne.Size { } } - separatorSize := theme.Padding() + separatorSize := r.cells.t.Theme().Size(theme.SizeNamePadding) return fyne.NewSize(width+float32(cols-stickCols-1)*separatorSize, height+float32(rows-stickRows-1)*separatorSize) } @@ -1246,8 +1248,11 @@ func (r *tableCellsRenderer) Refresh() { } func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) { + th := r.cells.t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r.cells.propertyLock.Lock() - separatorThickness := theme.Padding() + separatorThickness := th.Size(theme.SizeNamePadding) dataRows, dataCols := 0, 0 if f := r.cells.t.Length; f != nil { dataRows, dataCols = r.cells.t.Length() @@ -1296,7 +1301,7 @@ func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) { if !ok { c = r.pool.Obtain() if f := r.cells.t.CreateCell; f != nil && c == nil { - c = f() + c = createItemAndApplyThemeScope(f, r.cells.t) } if c == nil { return @@ -1332,7 +1337,8 @@ func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) { displayRow(row, &cells) } - inline := r.refreshHeaders(visibleRowHeights, visibleColWidths, offX, offY, startRow, maxRow, startCol, maxCol, separatorThickness) + inline := r.refreshHeaders(visibleRowHeights, visibleColWidths, offX, offY, startRow, maxRow, startCol, maxCol, + separatorThickness, th, v) cells = append(cells, inline...) offX -= r.cells.t.content.Offset.X @@ -1386,11 +1392,11 @@ func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) { } r.moveIndicators() - r.marker.FillColor = theme.Color(theme.ColorNameSelection) - r.marker.CornerRadius = theme.SelectionRadiusSize() + r.marker.FillColor = th.Color(theme.ColorNameSelection, v) + r.marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius) r.marker.Refresh() - r.hover.FillColor = theme.Color(theme.ColorNameHover) - r.hover.CornerRadius = theme.SelectionRadiusSize() + r.hover.FillColor = th.Color(theme.ColorNameHover, v) + r.hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius) r.hover.Refresh() } @@ -1401,8 +1407,9 @@ func (r *tableCellsRenderer) moveIndicators() { } visibleColWidths, offX, minCol, maxCol := r.cells.t.visibleColumnWidths(r.cells.t.cellSize.Width, cols) visibleRowHeights, offY, minRow, maxRow := r.cells.t.visibleRowHeights(r.cells.t.cellSize.Height, rows) - separatorThickness := theme.SeparatorThicknessSize() - padding := theme.Padding() + th := r.cells.t.Theme() + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) + padding := th.Size(theme.SizeNamePadding) dividerOff := (padding - separatorThickness) / 2 stickRows := r.cells.t.StickyRowCount @@ -1525,7 +1532,7 @@ func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, minCol = 0 } - padding := theme.Padding() + padding := r.cells.t.Theme().Size(theme.SizeNamePadding) for i := minCol; i < col; i++ { xPos += widths[i] @@ -1578,7 +1585,7 @@ func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, } func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths map[int]float32, offX, offY float32, - startRow, maxRow, startCol, maxCol int, separatorThickness float32) []fyne.CanvasObject { + startRow, maxRow, startCol, maxCol int, separatorThickness float32, th fyne.Theme, v fyne.ThemeVariant) []fyne.CanvasObject { wasVisible := r.headers r.headers = make(map[TableCellID]fyne.CanvasObject) headerMin := r.cells.t.headerSize @@ -1672,17 +1679,17 @@ func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths r.cells.t.left.Content.Refresh() r.headColBG.Hidden = !r.cells.t.ShowHeaderColumn - r.headColBG.FillColor = theme.Color(theme.ColorNameHeaderBackground) + r.headColBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) r.headColBG.Resize(fyne.NewSize(colWidth, r.cells.t.Size().Height)) r.headColStickyBG.Hidden = !r.cells.t.ShowHeaderColumn - r.headColStickyBG.FillColor = theme.Color(theme.ColorNameHeaderBackground) + r.headColStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) r.headColStickyBG.Resize(fyne.NewSize(colWidth, r.cells.t.stuckHeight+rowHeight)) r.headRowBG.Hidden = !r.cells.t.ShowHeaderRow - r.headRowBG.FillColor = theme.Color(theme.ColorNameHeaderBackground) + r.headRowBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) r.headRowBG.Resize(fyne.NewSize(r.cells.t.Size().Width, rowHeight)) r.headRowStickyBG.Hidden = !r.cells.t.ShowHeaderRow - r.headRowStickyBG.FillColor = theme.Color(theme.ColorNameHeaderBackground) + r.headRowStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) r.headRowStickyBG.Resize(fyne.NewSize(r.cells.t.stuckWidth+colWidth, rowHeight)) r.cells.t.corner.Content.(*fyne.Container).Objects = corner r.cells.t.corner.Content.Refresh() diff --git a/widget/table_test.go b/widget/table_internal_test.go similarity index 99% rename from widget/table_test.go rename to widget/table_internal_test.go index 0c23bb2447..9de625d14d 100644 --- a/widget/table_test.go +++ b/widget/table_internal_test.go @@ -69,6 +69,7 @@ func TestTable_ChangeTheme(t *testing.T) { table.Resize(fyne.NewSize(50, 30)) content := test.TempWidgetRenderer(t, table.content.Content.(*tableCells)).(*tableCellsRenderer) w := test.NewWindow(table) + w.SetPadded(false) defer w.Close() w.Resize(fyne.NewSize(180, 180)) test.AssertImageMatches(t, "table/theme_initial.png", w.Canvas().Capture()) @@ -335,6 +336,7 @@ func TestTable_Unselect(t *testing.T) { } table.selectedCell = &TableCellID{1, 1} w := test.NewWindow(table) + w.SetPadded(false) defer w.Close() w.Resize(fyne.NewSize(180, 180)) diff --git a/widget/testdata/list/list_initial.png b/widget/testdata/list/list_initial.png index 5fe500bf03..bbff4d0d1a 100644 Binary files a/widget/testdata/list/list_initial.png and b/widget/testdata/list/list_initial.png differ diff --git a/widget/testdata/list/list_theme_changed.png b/widget/testdata/list/list_theme_changed.png index df5118eb91..fcbba863dc 100644 Binary files a/widget/testdata/list/list_theme_changed.png and b/widget/testdata/list/list_theme_changed.png differ diff --git a/widget/testdata/table/theme_changed.png b/widget/testdata/table/theme_changed.png index be346b0f68..efbf071742 100644 Binary files a/widget/testdata/table/theme_changed.png and b/widget/testdata/table/theme_changed.png differ diff --git a/widget/testdata/table/theme_initial.png b/widget/testdata/table/theme_initial.png index febd337ce3..36922f822e 100644 Binary files a/widget/testdata/table/theme_initial.png and b/widget/testdata/table/theme_initial.png differ diff --git a/widget/testdata/tree/theme_changed.png b/widget/testdata/tree/theme_changed.png index 864d0d60fc..b82dc3dfc2 100644 Binary files a/widget/testdata/tree/theme_changed.png and b/widget/testdata/tree/theme_changed.png differ diff --git a/widget/testdata/tree/theme_initial.png b/widget/testdata/tree/theme_initial.png index c2d568fe13..b103435304 100644 Binary files a/widget/testdata/tree/theme_initial.png and b/widget/testdata/tree/theme_initial.png differ diff --git a/widget/tree.go b/widget/tree.go index f8fe931ed8..db4ff58d3f 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -440,7 +440,7 @@ func (t *Tree) ensureOpenMap() { } func (t *Tree) findBottom() (y float32, size fyne.Size) { - sep := theme.Padding() + sep := t.Theme().Size(theme.SizeNamePadding) t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) { size = t.leafMinSize if branch { @@ -467,6 +467,8 @@ func (t *Tree) findBottom() (y float32, size fyne.Size) { } func (t *Tree) offsetAndSize(uid TreeNodeID) (y float32, size fyne.Size, found bool) { + pad := t.Theme().Size(theme.SizeNamePadding) + t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) { m := t.leafMinSize if branch { @@ -483,7 +485,7 @@ func (t *Tree) offsetAndSize(uid TreeNodeID) (y float32, size fyne.Size, found b } // If this is not the first item, add a separator if y > 0 { - y += theme.Padding() + y += pad } y += m.Height @@ -558,8 +560,11 @@ func (r *treeRenderer) Refresh() { func (r *treeRenderer) updateMinSizes() { if f := r.tree.CreateNode; f != nil { - r.tree.branchMinSize = newBranch(r.tree, f(true)).MinSize() - r.tree.leafMinSize = newLeaf(r.tree, f(false)).MinSize() + branch := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.tree) + r.tree.branchMinSize = newBranch(r.tree, branch).MinSize() + + leaf := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.tree) + r.tree.leafMinSize = newLeaf(r.tree, leaf).MinSize() } } @@ -614,6 +619,7 @@ type treeContentRenderer struct { } func (r *treeContentRenderer) Layout(size fyne.Size) { + th := r.treeContent.Theme() r.treeContent.propertyLock.Lock() defer r.treeContent.propertyLock.Unlock() @@ -621,12 +627,12 @@ func (r *treeContentRenderer) Layout(size fyne.Size) { branches := make(map[string]*branch) leaves := make(map[string]*leaf) - pad := theme.Padding() + pad := th.Size(theme.SizeNamePadding) offsetY := r.treeContent.tree.offset.Y viewport := r.treeContent.viewport width := fyne.Max(size.Width, viewport.Width) separatorCount := 0 - separatorThickness := theme.SeparatorThicknessSize() + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) separatorSize := fyne.NewSize(width, separatorThickness) separatorOff := (pad + separatorThickness) / 2 hideSeparators := r.treeContent.tree.HideSeparators @@ -735,8 +741,11 @@ func (r *treeContentRenderer) Layout(size fyne.Size) { } func (r *treeContentRenderer) MinSize() (min fyne.Size) { + th := r.treeContent.Theme() r.treeContent.propertyLock.Lock() defer r.treeContent.propertyLock.Unlock() + pad := th.Size(theme.SizeNamePadding) + iconSize := th.Size(theme.SizeNameInlineIcon) r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) { // Root node is not rendered unless it has been customized @@ -750,14 +759,14 @@ func (r *treeContentRenderer) MinSize() (min fyne.Size) { // If this is not the first item, add a separator if min.Height > 0 { - min.Height += theme.Padding() + min.Height += pad } m := r.treeContent.tree.leafMinSize if isBranch { m = r.treeContent.tree.branchMinSize } - m.Width += float32(depth) * (theme.IconInlineSize() + theme.Padding()) + m.Width += float32(depth) * (iconSize + pad) min.Width = fyne.Max(min.Width, m.Width) min.Height += m.Height }) @@ -805,7 +814,7 @@ func (r *treeContentRenderer) getBranch() (b *branch) { } else { var content fyne.CanvasObject if f := r.treeContent.tree.CreateNode; f != nil { - content = f(true) + content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.treeContent.tree) } b = newBranch(r.treeContent.tree, content) } @@ -819,7 +828,7 @@ func (r *treeContentRenderer) getLeaf() (l *leaf) { } else { var content fyne.CanvasObject if f := r.treeContent.tree.CreateNode; f != nil { - content = f(false) + content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.treeContent.tree) } l = newLeaf(r.treeContent.tree, content) } @@ -846,8 +855,11 @@ func (n *treeNode) Content() fyne.CanvasObject { } func (n *treeNode) CreateRenderer() fyne.WidgetRenderer { - background := canvas.NewRectangle(theme.Color(theme.ColorNameHover)) - background.CornerRadius = theme.SelectionRadiusSize() + th := n.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) background.Hide() return &treeNodeRenderer{ BaseRenderer: widget.BaseRenderer{}, @@ -857,7 +869,8 @@ func (n *treeNode) CreateRenderer() fyne.WidgetRenderer { } func (n *treeNode) Indent() float32 { - return float32(n.depth) * (theme.IconInlineSize() + theme.Padding()) + th := n.Theme() + return float32(n.depth) * (th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNamePadding)) } // MouseIn is called when a desktop pointer enters the widget @@ -916,15 +929,18 @@ type treeNodeRenderer struct { } func (r *treeNodeRenderer) Layout(size fyne.Size) { - x := theme.Padding() + r.treeNode.Indent() + th := r.treeNode.Theme() + pad := th.Size(theme.SizeNamePadding) + iconSize := th.Size(theme.SizeNameInlineIcon) + x := pad + r.treeNode.Indent() y := float32(0) r.background.Resize(size) if r.treeNode.icon != nil { r.treeNode.icon.Move(fyne.NewPos(x, y)) - r.treeNode.icon.Resize(fyne.NewSize(theme.IconInlineSize(), size.Height)) + r.treeNode.icon.Resize(fyne.NewSize(iconSize, size.Height)) } - x += theme.IconInlineSize() - x += theme.Padding() + x += iconSize + x += pad if r.treeNode.content != nil { r.treeNode.content.Move(fyne.NewPos(x, y)) r.treeNode.content.Resize(fyne.NewSize(size.Width-x, size.Height)) @@ -935,8 +951,11 @@ func (r *treeNodeRenderer) MinSize() (min fyne.Size) { if r.treeNode.content != nil { min = r.treeNode.content.MinSize() } - min.Width += theme.InnerPadding() + r.treeNode.Indent() + theme.IconInlineSize() - min.Height = fyne.Max(min.Height, theme.IconInlineSize()) + th := r.treeNode.Theme() + iconSize := th.Size(theme.SizeNameInlineIcon) + + min.Width += th.Size(theme.SizeNameInnerPadding) + r.treeNode.Indent() + iconSize + min.Height = fyne.Max(min.Height, iconSize) return } @@ -961,15 +980,18 @@ func (r *treeNodeRenderer) Refresh() { } func (r *treeNodeRenderer) partialRefresh() { + th := r.treeNode.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + if r.treeNode.icon != nil { r.treeNode.icon.Refresh() } - r.background.CornerRadius = theme.SelectionRadiusSize() + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) if len(r.treeNode.tree.selected) > 0 && r.treeNode.uid == r.treeNode.tree.selected[0] { - r.background.FillColor = theme.Color(theme.ColorNameSelection) + r.background.FillColor = th.Color(theme.ColorNameSelection, v) r.background.Show() } else if r.treeNode.hovered || (r.treeNode.tree.focused && r.treeNode.tree.currentFocus == r.treeNode.uid) { - r.background.FillColor = theme.Color(theme.ColorNameHover) + r.background.FillColor = th.Color(theme.ColorNameHover, v) r.background.Show() } else { r.background.Hide() @@ -995,6 +1017,10 @@ func newBranch(tree *Tree, content fyne.CanvasObject) (b *branch) { }, } b.ExtendBaseWidget(b) + + if cache.OverrideThemeMatchingScope(b, tree) { + b.Refresh() + } return } @@ -1052,5 +1078,9 @@ func newLeaf(tree *Tree, content fyne.CanvasObject) (l *leaf) { }, } l.ExtendBaseWidget(l) + + if cache.OverrideThemeMatchingScope(l, tree) { + l.Refresh() + } return } diff --git a/widget/tree_test.go b/widget/tree_test.go index 1817233731..d38a9dd408 100644 --- a/widget/tree_test.go +++ b/widget/tree_test.go @@ -6,8 +6,12 @@ import ( "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/stretchr/testify/assert" @@ -292,6 +296,7 @@ func TestTree_ChangeTheme(t *testing.T) { window := test.NewWindow(tree) defer window.Close() + window.SetPadded(false) window.Resize(fyne.NewSize(220, 220)) tree.Refresh() // Force layout @@ -305,6 +310,26 @@ func TestTree_ChangeTheme(t *testing.T) { }) } +func TestTree_OverrideTheme(t *testing.T) { + test.NewTempApp(t) + + tree := widget.NewTreeWithStrings(treeData) + tree.OpenBranch("foo") + + window := test.NewWindow(tree) + defer window.Close() + window.SetPadded(false) + window.Resize(fyne.NewSize(220, 220)) + test.ApplyTheme(t, test.NewTheme()) + + normal := test.Theme() + bg := canvas.NewRectangle(normal.Color(theme.ColorNameBackground, theme.VariantDark)) + window.SetContent(&fyne.Container{Layout: layout.NewStackLayout(), + Objects: []fyne.CanvasObject{bg, container.NewThemeOverride(tree, normal)}}) + window.Resize(fyne.NewSize(220, 220)) + test.AssertImageMatches(t, "tree/theme_initial.png", window.Canvas().Capture()) +} + func TestTree_Move(t *testing.T) { test.NewTempApp(t)