From 1da49d51819893ab1b7ee57a56b12450fb796a04 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 16 Sep 2020 14:03:22 +0100 Subject: [PATCH 01/19] Collect containers in a new package moved unreleased layout.NewXxxContainer to container.NewXxx. Deprecate and alias widget.NewXxxContainer as container.NewXxx --- cmd/fyne_demo/main.go | 17 ++++--- cmd/fyne_demo/screens/container.go | 34 +++++++------ cmd/fyne_demo/screens/widget.go | 33 ++++++------ container/layouts.go | 80 ++++++++++++++++++++++++++++++ container/scroll.go | 35 +++++++++++++ container/split.go | 24 +++++++++ container/tabs.go | 42 ++++++++++++++++ layout/borderlayout.go | 20 -------- layout/borderlayout_test.go | 2 +- layout/boxlayout.go | 12 ----- layout/centerlayout.go | 5 -- layout/fixedgridlayout.go | 1 + layout/gridlayout.go | 19 ------- layout/gridwraplayout.go | 7 --- layout/maxlayout.go | 5 -- widget/scroller.go | 13 +++++ widget/splitcontainer.go | 6 +++ widget/tabcontainer.go | 12 ++++- 18 files changed, 257 insertions(+), 110 deletions(-) create mode 100644 container/layouts.go create mode 100644 container/scroll.go create mode 100644 container/split.go create mode 100644 container/tabs.go diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index 0ca0df260c..bb63e77e4b 100644 --- a/cmd/fyne_demo/main.go +++ b/cmd/fyne_demo/main.go @@ -10,6 +10,7 @@ import ( "fyne.io/fyne/canvas" "fyne.io/fyne/cmd/fyne_demo/data" "fyne.io/fyne/cmd/fyne_demo/screens" + "fyne.io/fyne/container" "fyne.io/fyne/layout" "fyne.io/fyne/theme" "fyne.io/fyne/widget" @@ -114,17 +115,17 @@ func main() { w.SetMainMenu(mainMenu) w.SetMaster() - tabs := widget.NewTabContainer( - widget.NewTabItemWithIcon("Welcome", theme.HomeIcon(), welcomeScreen(a)), - widget.NewTabItemWithIcon("Graphics", theme.DocumentCreateIcon(), screens.GraphicsScreen()), - widget.NewTabItemWithIcon("Widgets", theme.CheckButtonCheckedIcon(), screens.WidgetScreen()), - widget.NewTabItemWithIcon("Containers", theme.ViewRestoreIcon(), screens.ContainerScreen()), - widget.NewTabItemWithIcon("Windows", theme.ViewFullScreenIcon(), screens.DialogScreen(w))) + tabs := container.NewTabs( + container.NewTabItemWithIcon("Welcome", theme.HomeIcon(), welcomeScreen(a)), + container.NewTabItemWithIcon("Graphics", theme.DocumentCreateIcon(), screens.GraphicsScreen()), + container.NewTabItemWithIcon("Widgets", theme.CheckButtonCheckedIcon(), screens.WidgetScreen()), + container.NewTabItemWithIcon("Containers", theme.ViewRestoreIcon(), screens.ContainerScreen()), + container.NewTabItemWithIcon("Windows", theme.ViewFullScreenIcon(), screens.DialogScreen(w))) if !fyne.CurrentDevice().IsMobile() { - tabs.Append(widget.NewTabItemWithIcon("Advanced", theme.SettingsIcon(), screens.AdvancedScreen(w))) + tabs.Append(container.NewTabItemWithIcon("Advanced", theme.SettingsIcon(), screens.AdvancedScreen(w))) } - tabs.SetTabLocation(widget.TabLocationLeading) + tabs.SetTabLocation(container.TabLocationLeading) tabs.SelectTabIndex(a.Preferences().Int(preferenceCurrentTab)) w.SetContent(tabs) diff --git a/cmd/fyne_demo/screens/container.go b/cmd/fyne_demo/screens/container.go index d9a099299c..e23b12101a 100644 --- a/cmd/fyne_demo/screens/container.go +++ b/cmd/fyne_demo/screens/container.go @@ -8,22 +8,24 @@ import ( "fyne.io/fyne" "fyne.io/fyne/canvas" "fyne.io/fyne/layout" + "fyne.io/fyne/container" "fyne.io/fyne/theme" "fyne.io/fyne/widget" ) // ContainerScreen loads a tab panel for containers and layouts func ContainerScreen() fyne.CanvasObject { - return widget.NewTabContainer( - widget.NewTabItem("Accordion", makeAccordionTab()), - widget.NewTabItem("Card", makeCardTab()), - widget.NewTabItem("Split", makeSplitTab()), - widget.NewTabItem("Scroll", makeScrollTab()), + return container.NewTabs( + container.NewTabItem("Accordion", makeAccordionTab()), + container.NewTabItem("Card", makeCardTab()), + container.NewTabItem("Split", makeSplitTab()), + container.NewTabItem("Scroll", makeScrollTab()), + container.NewTabItem("Table", makeTableTab()), // layouts - widget.NewTabItem("Border", makeBorderLayout()), - widget.NewTabItem("Box", makeBoxLayout()), - widget.NewTabItem("Center", makeCenterLayout()), - widget.NewTabItem("Grid", makeGridLayout()), + container.NewTabItem("Border", makeBorderLayout()), + container.NewTabItem("Box", makeBoxLayout()), + container.NewTabItem("Center", makeCenterLayout()), + container.NewTabItem("Grid", makeGridLayout()), ) } @@ -120,11 +122,11 @@ func makeScrollTab() fyne.CanvasObject { hlist := makeButtonList(20) vlist := makeButtonList(50) - horiz := widget.NewHScrollContainer(widget.NewHBox(hlist...)) - vert := widget.NewVScrollContainer(widget.NewVBox(vlist...)) + horiz := container.NewHScroll(widget.NewHBox(hlist...)) + vert := container.NewVScroll(widget.NewVBox(vlist...)) - return fyne.NewContainerWithLayout(layout.NewAdaptiveGridLayout(2), - fyne.NewContainerWithLayout(layout.NewBorderLayout(horiz, nil, nil, nil), horiz, vert), + return container.NewAdaptiveGrid(2, + container.NewBorder(horiz, nil, nil, nil, vert), makeScrollBothTab()) } @@ -132,7 +134,7 @@ func makeScrollBothTab() fyne.CanvasObject { logo := canvas.NewImageFromResource(theme.FyneLogo()) logo.SetMinSize(fyne.NewSize(800, 800)) - scroll := widget.NewScrollContainer(logo) + scroll := container.NewScroll(logo) scroll.Resize(fyne.NewSize(400, 400)) return scroll @@ -142,9 +144,9 @@ func makeSplitTab() fyne.CanvasObject { left := widget.NewMultiLineEntry() left.Wrapping = fyne.TextWrapWord left.SetText("Long text is looooooooooooooong") - right := widget.NewVSplitContainer( + right := container.NewVSplit( widget.NewLabel("Label"), widget.NewButton("Button", func() { fmt.Println("button tapped!") }), ) - return widget.NewHSplitContainer(widget.NewVScrollContainer(left), right) + return container.NewHSplit(container.NewVScroll(left), right) } diff --git a/cmd/fyne_demo/screens/widget.go b/cmd/fyne_demo/screens/widget.go index 164b59cbfe..8c64534689 100644 --- a/cmd/fyne_demo/screens/widget.go +++ b/cmd/fyne_demo/screens/widget.go @@ -7,6 +7,7 @@ import ( "time" "fyne.io/fyne" + "fyne.io/fyne/container" "fyne.io/fyne/data/validation" "fyne.io/fyne/layout" "fyne.io/fyne/theme" @@ -33,7 +34,7 @@ var ( endProgress chan interface{} ) -func makeButtonTab() fyne.Widget { +func makeButtonTab() fyne.CanvasObject { disabled := widget.NewButton("Disabled", func() {}) disabled.Disable() @@ -47,7 +48,7 @@ func makeButtonTab() fyne.Widget { shareItem, )) - return widget.NewVBox( + return container.NewVBox( widget.NewButton("Button (text only)", func() { fmt.Println("tapped text button") }), widget.NewButtonWithIcon("Button (text & leading icon)", theme.ConfirmIcon(), func() { fmt.Println("tapped text & leading icon button") }), &widget.Button{ @@ -101,7 +102,7 @@ func makeTextTab() fyne.CanvasObject { entryLoremIpsum := widget.NewMultiLineEntry() entryLoremIpsum.SetText(loremIpsum) - entryLoremIpsumScroller := widget.NewVScrollContainer(entryLoremIpsum) + entryLoremIpsumScroller := container.NewVScroll(entryLoremIpsum) label.Alignment = fyne.TextAlignLeading hyperlink.Alignment = fyne.TextAlignLeading @@ -155,8 +156,8 @@ func makeTextTab() fyne.CanvasObject { }) radioWrap.SetSelected("Text Wrapping Word") - fixed := widget.NewVBox( - widget.NewHBox( + fixed := container.NewVBox( + container.NewHBox( radioAlign, layout.NewSpacer(), radioWrap, @@ -170,7 +171,7 @@ func makeTextTab() fyne.CanvasObject { fixed, entryLoremIpsumScroller, grid) } -func makeInputTab() fyne.Widget { +func makeInputTab() fyne.CanvasObject { entry := widget.NewEntry() entry.SetPlaceHolder("Entry") entryDisabled := widget.NewEntry() @@ -189,7 +190,7 @@ func makeInputTab() fyne.Widget { disabledRadio := widget.NewRadio([]string{"Disabled radio"}, func(string) {}) disabledRadio.Disable() - return widget.NewVBox( + return container.NewVBox( entry, entryDisabled, entryValidated, @@ -204,7 +205,7 @@ func makeInputTab() fyne.Widget { ) } -func makeProgressTab() fyne.Widget { +func makeProgressTab() fyne.CanvasObject { progress = widget.NewProgressBar() fprogress = widget.NewProgressBar() @@ -215,7 +216,7 @@ func makeProgressTab() fyne.Widget { infProgress = widget.NewProgressBarInfinite() endProgress = make(chan interface{}, 1) - return widget.NewVBox( + return container.NewVBox( widget.NewLabel("Percent"), progress, widget.NewLabel("Formatted"), fprogress, widget.NewLabel("Infinite"), infProgress) @@ -300,14 +301,14 @@ func WidgetScreen() fyne.CanvasObject { ) progress := makeProgressTab() - tabs := widget.NewTabContainer( - widget.NewTabItem("Buttons", makeButtonTab()), - widget.NewTabItem("Text", makeTextTab()), - widget.NewTabItem("Input", makeInputTab()), - widget.NewTabItem("Progress", progress), - widget.NewTabItem("Form", makeFormTab()), + tabs := container.NewTabs( + container.NewTabItem("Buttons", makeButtonTab()), + container.NewTabItem("Text", makeTextTab()), + container.NewTabItem("Input", makeInputTab()), + container.NewTabItem("Progress", progress), + container.NewTabItem("Form", makeFormTab()), ) - tabs.OnChanged = func(t *widget.TabItem) { + tabs.OnChanged = func(t *container.TabItem) { if t.Content == progress { startProgress() } else { diff --git a/container/layouts.go b/container/layouts.go new file mode 100644 index 0000000000..f8fe527637 --- /dev/null +++ b/container/layouts.go @@ -0,0 +1,80 @@ +// Package container provides container widgets that are used to lay out and organise applications +package container // import "fyne.io/fyne/container" + +import ( + "fyne.io/fyne" + "fyne.io/fyne/layout" +) + +// NewAdaptiveGrid creates a new container with the specified objects and using the grid layout. +// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical +// it will specify the rows. On mobile this will dynamically refresh when device is rotated. +func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewAdaptiveGridLayout(rowcols), objects...) +} + +// NewBorder creates a new container with the specified objects and using the border layout. +// The top, bottom, left and right parameters specify the items that should be placed around edges, +// the remaining elements will be in the center. Nil can be used to an edge if it should not be filled. +func NewBorder(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container { + all := objects + if top != nil { + all = append(all, top) + } + if bottom != nil { + all = append(all, bottom) + } + if left != nil { + all = append(all, left) + } + if right != nil { + all = append(all, right) + } + return fyne.NewContainerWithLayout(layout.NewBorderLayout(top, bottom, left, right), all...) +} + +// NewCenter creates a new container with the specified objects centered in the available space. +func NewCenter(objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewCenterLayout(), objects...) +} + +// NewGridWithColumns creates a new container with the specified objects and using the grid layout with +// a specified number of columns. The number of rows will depend on how many children are in the container. +func NewGridWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewGridLayoutWithColumns(cols), objects...) +} + +// NewGridWithRows creates a new container with the specified objects and using the grid layout with +// a specified number of columns. The number of columns will depend on how many children are in the container. +func NewGridWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewGridLayoutWithRows(rows), objects...) +} + +// NewGridWrap creates a new container with the specified objects and using the gridwrap layout. +// Every element will be resized to the size parameter and the content will arrange along a row and flow to a +// new row if the elements don't fit. +func NewGridWrap(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewGridWrapLayout(size), objects...) +} + +// NewHBox creates a new container with the specified objects and using the HBox layout. +// The objects will be placed in the container from left to right. +func NewHBox(objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewHBoxLayout(), objects...) +} + +// NewMax creates a new container with the specified objects filling the available space. +func NewMax(objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewMaxLayout(), objects...) +} + +// NewPadded creates a new container with the specified objects inset by standard padding size. +func NewPadded(objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewPaddedLayout(), objects...) +} + +// NewVBox creates a new container with the specified objects and using the VBox layout. +// The objects will be stacked in the container from top to bottom. +func NewVBox(objects ...fyne.CanvasObject) *fyne.Container { + return fyne.NewContainerWithLayout(layout.NewVBoxLayout(), objects...) +} diff --git a/container/scroll.go b/container/scroll.go new file mode 100644 index 0000000000..6c3a3fea94 --- /dev/null +++ b/container/scroll.go @@ -0,0 +1,35 @@ +package container + +import ( + "fyne.io/fyne" + "fyne.io/fyne/widget" +) + +// Scroll defines a container that is smaller than the Content. +// The Offset is used to determine the position of the child widgets within the container. +type Scroll = widget.ScrollContainer + +// ScrollDirection represents the directions in which a Scroll container can scroll its child content. +type ScrollDirection = widget.ScrollDirection + +// NewScroll creates a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize to be smaller than that of the passed object. +func NewScroll(content fyne.CanvasObject) *Scroll { + return widget.NewScrollContainer(content) +} + +// NewHScroll create a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Width to be smaller than that of the passed object. +func NewHScroll(content fyne.CanvasObject) *Scroll { + return widget.NewHScrollContainer(content) +} + +// NewVScrollcreate a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Height to be smaller than that of the passed object. +func NewVScroll(content fyne.CanvasObject) *Scroll { + return widget.NewVScrollContainer(content) +} + +// TODO move the implementation into internal/scroll.go in 2.0 when we delete the old API. +// we cannot do that right now due to the fact that this package depends on widgets and they depend on us. +// Once moving the bulk of scroller will go to an internal package, then this and the widget package can both depend. diff --git a/container/split.go b/container/split.go new file mode 100644 index 0000000000..7bddbe27e0 --- /dev/null +++ b/container/split.go @@ -0,0 +1,24 @@ +package container + +import ( + "fyne.io/fyne" + "fyne.io/fyne/widget" +) + +// Split defines a container whose size is split between two children. +type Split = widget.SplitContainer + +// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements. +// A vertical split bar that can be dragged will be added between the elements. +func NewHSplit(leading, trailing fyne.CanvasObject) *Split { + return widget.NewHSplitContainer(leading, trailing) +} + +// NewVSplit creates a vertically arranged container with the specified top and bottom elements. +// A horizontal split bar that can be dragged will be added between the elements. +func NewVSplit(top, bottom fyne.CanvasObject) *Split { + return widget.NewVSplitContainer(top, bottom) +} + +// TODO move the implementation into here in 2.0 when we delete the old API. +// we cannot do that right now due to Scroll dependency order. diff --git a/container/tabs.go b/container/tabs.go new file mode 100644 index 0000000000..799c9b25b1 --- /dev/null +++ b/container/tabs.go @@ -0,0 +1,42 @@ +package container + +import ( + "fyne.io/fyne" + "fyne.io/fyne/widget" +) + +// Tabs container allows switching visible content from a list of TabItems. +// Each item is represented by a button at the top of the widget. +type Tabs = widget.TabContainer + +// TabItem represents a single view in a TabContainer. +// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active. +type TabItem = widget.TabItem + +// TabLocation is the location where the tabs of a tab container should be rendered +type TabLocation = widget.TabLocation + +const ( + TabLocationTop TabLocation = iota + TabLocationLeading + TabLocationBottom + TabLocationTrailing +) + +// NewTabs creates a new tab bar widget that allows the user to choose between different visible containers +func NewTabs(items ...*TabItem) *Tabs { + return widget.NewTabContainer(items...) +} + +// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab. +func NewTabItem(text string, content fyne.CanvasObject) *TabItem { + return widget.NewTabItem(text, content) +} + +// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab. +func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem { + return widget.NewTabItemWithIcon(text, icon, content) +} + +// TODO move the implementation into here in 2.0 when we delete the old API. +// we cannot do that right now due to Scroll dependency order. diff --git a/layout/borderlayout.go b/layout/borderlayout.go index 7e52db8521..6c46127270 100644 --- a/layout/borderlayout.go +++ b/layout/borderlayout.go @@ -12,26 +12,6 @@ type borderLayout struct { top, bottom, left, right fyne.CanvasObject } -// NewBorderContainer creates a new container with the specified objects and using the border layout. -// The top, bottom, left and right parameters specify the items that should be placed around edges, -// the remaining elements will be in the center. Nil can be used to an edge if it should not be filled. -func NewBorderContainer(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container { - all := objects - if top != nil { - all = append(all, top) - } - if bottom != nil { - all = append(all, bottom) - } - if left != nil { - all = append(all, left) - } - if right != nil { - all = append(all, right) - } - return fyne.NewContainerWithLayout(NewBorderLayout(top, bottom, left, right), all...) -} - // NewBorderLayout creates a new BorderLayout instance with top, bottom, left // and right objects set. All other items in the container will fill the centre // space diff --git a/layout/borderlayout_test.go b/layout/borderlayout_test.go index e31223a3a6..2d6538868c 100644 --- a/layout/borderlayout_test.go +++ b/layout/borderlayout_test.go @@ -20,7 +20,7 @@ func TestNewBorderContainer(t *testing.T) { right.SetMinSize(fyne.NewSize(10, 10)) middle := canvas.NewRectangle(color.NRGBA{0, 0, 0, 0}) - c := layout.NewBorderContainer(top, nil, nil, right, []fyne.CanvasObject{middle}...) + c := fyne.NewContainerWithLayout(layout.NewBorderLayout(top, nil, nil, right), top, right, middle) assert.Equal(t, 3, len(c.Objects)) c.Resize(fyne.NewSize(100, 100)) diff --git a/layout/boxlayout.go b/layout/boxlayout.go index a03163f28f..ca99f04ee2 100644 --- a/layout/boxlayout.go +++ b/layout/boxlayout.go @@ -12,24 +12,12 @@ type boxLayout struct { horizontal bool } -// NewHBoxContainer creates a new container with the specified objects and using the HBox layout. -// The objects will be placed in the container from left to right. -func NewHBoxContainer(objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewHBoxLayout(), objects...) -} - // NewHBoxLayout returns a horizontal box layout for stacking a number of child // canvas objects or widgets left to right. func NewHBoxLayout() fyne.Layout { return &boxLayout{true} } -// NewVBoxContainer creates a new container with the specified objects and using the VBox layout. -// The objects will be stacked in the container from top to bottom. -func NewVBoxContainer(objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewHBoxLayout(), objects...) -} - // NewVBoxLayout returns a vertical box layout for stacking a number of child // canvas objects or widgets top to bottom. func NewVBoxLayout() fyne.Layout { diff --git a/layout/centerlayout.go b/layout/centerlayout.go index 507848663a..122260e554 100644 --- a/layout/centerlayout.go +++ b/layout/centerlayout.go @@ -8,11 +8,6 @@ var _ fyne.Layout = (*centerLayout)(nil) type centerLayout struct { } -// NewCenterContainer creates a new container with the specified objects centered in the available space. -func NewCenterContainer(objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewCenterLayout(), objects...) -} - // NewCenterLayout creates a new CenterLayout instance func NewCenterLayout() fyne.Layout { return ¢erLayout{} diff --git a/layout/fixedgridlayout.go b/layout/fixedgridlayout.go index fee6df5bda..7ebce336f4 100644 --- a/layout/fixedgridlayout.go +++ b/layout/fixedgridlayout.go @@ -5,6 +5,7 @@ import ( ) // NewFixedGridLayout returns a new FixedGridLayout instance +// // Deprecated: use the replacement NewGridWrapLayout. This method will be removed in 2.0. func NewFixedGridLayout(size fyne.Size) fyne.Layout { return NewGridWrapLayout(size) diff --git a/layout/gridlayout.go b/layout/gridlayout.go index c0dfc4d18e..1b8986672f 100644 --- a/layout/gridlayout.go +++ b/layout/gridlayout.go @@ -15,30 +15,11 @@ type gridLayout struct { vertical, adapt bool } -// NewAdaptiveGridContainer creates a new container with the specified objects and using the grid layout. -// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical -// it will specify the rows. On mobile this will dynamically refresh when device is rotated. -func NewAdaptiveGridContainer(rowcols int, objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewAdaptiveGridLayout(rowcols), objects...) -} - // NewAdaptiveGridLayout returns a new grid layout which uses columns when horizontal but rows when vertical. func NewAdaptiveGridLayout(rowcols int) fyne.Layout { return &gridLayout{Cols: rowcols, adapt: true} } -// NewGridContainerWithColumns creates a new container with the specified objects and using the grid layout with -// a specified number of columns. The number of rows will depend on how many children are in the container. -func NewGridContainerWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewGridLayoutWithColumns(cols), objects...) -} - -// NewGridContainerWithRows creates a new container with the specified objects and using the grid layout with -// a specified number of columns. The number of columns will depend on how many children are in the container. -func NewGridContainerWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewGridLayoutWithRows(rows), objects...) -} - // NewGridLayout returns a grid layout arranged in a specified number of columns. // The number of rows will depend on how many children are in the container that uses this layout. func NewGridLayout(cols int) fyne.Layout { diff --git a/layout/gridwraplayout.go b/layout/gridwraplayout.go index b85049ab7a..989a7f3a10 100644 --- a/layout/gridwraplayout.go +++ b/layout/gridwraplayout.go @@ -16,13 +16,6 @@ type gridWrapLayout struct { rowCount int } -// NewGridWrapContainer creates a new container with the specified objects and using the gridwrap layout. -// Every element will be resized to the size parameter and the content will arrange along a row and flow to a -// new row if the elements don't fit. -func NewGridWrapContainer(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewGridWrapLayout(size), objects...) -} - // NewGridWrapLayout returns a new GridWrapLayout instance func NewGridWrapLayout(size fyne.Size) fyne.Layout { return &gridWrapLayout{size, 1, 1} diff --git a/layout/maxlayout.go b/layout/maxlayout.go index ab8ec6c2d4..31bde0871a 100644 --- a/layout/maxlayout.go +++ b/layout/maxlayout.go @@ -9,11 +9,6 @@ var _ fyne.Layout = (*maxLayout)(nil) type maxLayout struct { } -// NewMaxContainer creates a new container with the specified objects filling the available space. -func NewMaxContainer(objects ...fyne.CanvasObject) *fyne.Container { - return fyne.NewContainerWithLayout(NewMaxLayout(), objects...) -} - // NewMaxLayout creates a new MaxLayout instance func NewMaxLayout() fyne.Layout { return &maxLayout{} diff --git a/widget/scroller.go b/widget/scroller.go index b48319c2b2..cf924d3f2c 100644 --- a/widget/scroller.go +++ b/widget/scroller.go @@ -11,12 +11,17 @@ import ( ) // ScrollDirection represents the directions in which a ScrollContainer can scroll its child content. +// +// Deprecated: use container.ScrollDirection instead. type ScrollDirection int // Constants for valid values of ScrollDirection. const ( + // Deprecated: use container.ScrollBoth instead ScrollBoth ScrollDirection = iota + // Deprecated: use container.ScrollHorizontalOnly instead ScrollHorizontalOnly + // Deprecated: use container.ScrollVerticalOnly instead ScrollVerticalOnly ) @@ -331,6 +336,8 @@ func (r *scrollContainerRenderer) updatePosition() { // ScrollContainer defines a container that is smaller than the Content. // The Offset is used to determine the position of the child widgets within the container. +// +// Deprecated: use container.Scroll instead. type ScrollContainer struct { BaseWidget minSize fyne.Size @@ -463,18 +470,24 @@ func computeOffset(start, delta, outerWidth, innerWidth int) int { // NewScrollContainer creates a scrollable parent wrapping the specified content. // Note that this may cause the MinSize to be smaller than that of the passed object. +// +// Deprecated: use container.NewScroll instead. func NewScrollContainer(content fyne.CanvasObject) *ScrollContainer { return newScrollContainerWithDirection(ScrollBoth, content) } // NewHScrollContainer create a scrollable parent wrapping the specified content. // Note that this may cause the MinSize.Width to be smaller than that of the passed object. +// +// Deprecated: use container.NewHScroll instead. func NewHScrollContainer(content fyne.CanvasObject) *ScrollContainer { return newScrollContainerWithDirection(ScrollHorizontalOnly, content) } // NewVScrollContainer create a scrollable parent wrapping the specified content. // Note that this may cause the MinSize.Height to be smaller than that of the passed object. +// +// Deprecated: use container.NewVScroll instead. func NewVScrollContainer(content fyne.CanvasObject) *ScrollContainer { return newScrollContainerWithDirection(ScrollVerticalOnly, content) } diff --git a/widget/splitcontainer.go b/widget/splitcontainer.go index e95719a252..0b4a6e7927 100644 --- a/widget/splitcontainer.go +++ b/widget/splitcontainer.go @@ -13,6 +13,8 @@ import ( var _ fyne.CanvasObject = (*SplitContainer)(nil) // SplitContainer defines a container whose size is split between two children. +// +// Deprecated: use container.Split instead. type SplitContainer struct { BaseWidget Offset float64 @@ -23,12 +25,16 @@ type SplitContainer struct { // NewHSplitContainer creates a horizontally arranged container with the specified leading and trailing elements. // A vertical split bar that can be dragged will be added between the elements. +// +// Deprecated: use container.NewHSplit instead. func NewHSplitContainer(leading, trailing fyne.CanvasObject) *SplitContainer { return newSplitContainer(true, leading, trailing) } // NewVSplitContainer creates a vertically arranged container with the specified top and bottom elements. // A horizontal split bar that can be dragged will be added between the elements. +// +// Deprecated: use container.NewVSplit instead. func NewVSplitContainer(top, bottom fyne.CanvasObject) *SplitContainer { return newSplitContainer(false, top, bottom) } diff --git a/widget/tabcontainer.go b/widget/tabcontainer.go index 0fb1f3526d..f079d76b82 100644 --- a/widget/tabcontainer.go +++ b/widget/tabcontainer.go @@ -13,6 +13,8 @@ import ( // TabContainer widget allows switching visible content from a list of TabItems. // Each item is represented by a button at the top of the widget. +// +// Deprecated: use container.Tabs instead. type TabContainer struct { BaseWidget @@ -24,20 +26,28 @@ type TabContainer struct { // TabItem represents a single view in a TabContainer. // The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active. +// +// Deprecated: use container.TabItem instead. type TabItem struct { Text string Icon fyne.Resource Content fyne.CanvasObject } -// TabLocation is the location where the tabs of a tab container should be rendered +// TabLocation is the location where the tabs of a tab container should be rendered. +// +// Deprecated: use container.TabLocation instead. type TabLocation int // TabLocation values const ( + // Deprecated: use container.TabLocationTop TabLocationTop TabLocation = iota + // Deprecated: use container.TabLocationLeading TabLocationLeading + // Deprecated: use container.TabLocationBottom TabLocationBottom + // Deprecated: use container.TabLocationTrailing TabLocationTrailing ) From 3b8a32813a0b5dec5ccd60bf712eaeb9e9cd6966 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 16 Sep 2020 14:03:56 +0100 Subject: [PATCH 02/19] Use new container API, no layout needed --- cmd/fyne_demo/screens/container.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/cmd/fyne_demo/screens/container.go b/cmd/fyne_demo/screens/container.go index e23b12101a..530bc93431 100644 --- a/cmd/fyne_demo/screens/container.go +++ b/cmd/fyne_demo/screens/container.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne" "fyne.io/fyne/canvas" - "fyne.io/fyne/layout" "fyne.io/fyne/container" "fyne.io/fyne/theme" "fyne.io/fyne/widget" @@ -53,9 +52,7 @@ func makeBorderLayout() *fyne.Container { right := makeCell() middle := widget.NewLabelWithStyle("BorderLayout", fyne.TextAlignCenter, fyne.TextStyle{}) - borderLayout := layout.NewBorderLayout(top, bottom, left, right) - return fyne.NewContainerWithLayout(borderLayout, - top, bottom, left, right, middle) + return container.NewBorder(top, bottom, left, right, middle) } func makeBoxLayout() *fyne.Container { @@ -65,11 +62,9 @@ func makeBoxLayout() *fyne.Container { center := makeCell() right := makeCell() - col := fyne.NewContainerWithLayout(layout.NewVBoxLayout(), - top, middle, bottom) + col := container.NewVBox(top, middle, bottom) - return fyne.NewContainerWithLayout(layout.NewHBoxLayout(), - col, center, right) + return container.NewHBox(col, center, right) } func makeButtonList(count int) []fyne.CanvasObject { @@ -91,8 +86,8 @@ func makeCardTab() fyne.CanvasObject { card2.Image = canvas.NewImageFromResource(theme.FyneLogo()) card3 := widget.NewCard("Title 3", "Subtitle", widget.NewCheck("Check me", func(bool) {})) card4 := widget.NewCard("Title 4", "Another card", widget.NewLabel("Content")) - return fyne.NewContainerWithLayout(layout.NewGridLayout(3), widget.NewVBox(card1, card4), - widget.NewVBox(card2), widget.NewVBox(card3)) + return container.NewGridWithColumns(3, container.NewVBox(card1, card4), + container.NewVBox(card2), container.NewVBox(card3)) } func makeCell() fyne.CanvasObject { @@ -104,8 +99,7 @@ func makeCell() fyne.CanvasObject { func makeCenterLayout() *fyne.Container { middle := widget.NewButton("CenterLayout", func() {}) - return fyne.NewContainerWithLayout(layout.NewCenterLayout(), - middle) + return container.NewCenter(middle) } func makeGridLayout() *fyne.Container { @@ -114,7 +108,7 @@ func makeGridLayout() *fyne.Container { box3 := makeCell() box4 := makeCell() - return fyne.NewContainerWithLayout(layout.NewGridLayout(2), + return container.NewGridWithColumns(2, box1, box2, box3, box4) } From b005623b66baac004b441a8e0ccebd23d4dff942 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 16 Sep 2020 14:10:34 +0100 Subject: [PATCH 03/19] Missed some lint issues --- container/scroll.go | 9 ++++++++- container/tabs.go | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/container/scroll.go b/container/scroll.go index 6c3a3fea94..4ef7103ca5 100644 --- a/container/scroll.go +++ b/container/scroll.go @@ -12,6 +12,13 @@ type Scroll = widget.ScrollContainer // ScrollDirection represents the directions in which a Scroll container can scroll its child content. type ScrollDirection = widget.ScrollDirection +// Constants for valid values of ScrollDirection. +const ( + ScrollBoth ScrollDirection = iota + ScrollHorizontalOnly + ScrollVerticalOnly +) + // NewScroll creates a scrollable parent wrapping the specified content. // Note that this may cause the MinSize to be smaller than that of the passed object. func NewScroll(content fyne.CanvasObject) *Scroll { @@ -24,7 +31,7 @@ func NewHScroll(content fyne.CanvasObject) *Scroll { return widget.NewHScrollContainer(content) } -// NewVScrollcreate a scrollable parent wrapping the specified content. +// NewVScroll a scrollable parent wrapping the specified content. // Note that this may cause the MinSize.Height to be smaller than that of the passed object. func NewVScroll(content fyne.CanvasObject) *Scroll { return widget.NewVScrollContainer(content) diff --git a/container/tabs.go b/container/tabs.go index 799c9b25b1..c7d8513eb5 100644 --- a/container/tabs.go +++ b/container/tabs.go @@ -16,6 +16,7 @@ type TabItem = widget.TabItem // TabLocation is the location where the tabs of a tab container should be rendered type TabLocation = widget.TabLocation +// TabLocation values const ( TabLocationTop TabLocation = iota TabLocationLeading From 34e1afd463e2ce44afb055fba094fa0de406b883 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 16 Sep 2020 14:18:41 +0100 Subject: [PATCH 04/19] Remove accidental line --- cmd/fyne_demo/screens/container.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/fyne_demo/screens/container.go b/cmd/fyne_demo/screens/container.go index 530bc93431..8723053192 100644 --- a/cmd/fyne_demo/screens/container.go +++ b/cmd/fyne_demo/screens/container.go @@ -19,7 +19,6 @@ func ContainerScreen() fyne.CanvasObject { container.NewTabItem("Card", makeCardTab()), container.NewTabItem("Split", makeSplitTab()), container.NewTabItem("Scroll", makeScrollTab()), - container.NewTabItem("Table", makeTableTab()), // layouts container.NewTabItem("Border", makeBorderLayout()), container.NewTabItem("Box", makeBoxLayout()), From fefe293bd05e4f76b584ea57216083d22a69d70e Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 16 Sep 2020 21:52:35 +0100 Subject: [PATCH 05/19] Fixing style of accordion to use dividers (#1305) * Revert "Update accordion to use button style" This reverts commit 571a27a43eb13becec955d8b71fb441ea1586254. * Add full-width divider to indicate expansion areas in accordion --- widget/accordion.go | 26 ++++++++++- widget/accordion_internal_test.go | 42 +++++++++--------- widget/accordion_test.go | 16 +++---- ...on_layout_multiple_open_multiple_items.png | Bin 1449 -> 1290 bytes ...ut_multiple_open_multiple_items_opened.png | Bin 2444 -> 2188 bytes ...ccordion_layout_multiple_open_one_item.png | Bin 1084 -> 964 bytes ...n_layout_multiple_open_one_item_opened.png | Bin 1266 -> 1127 bytes ...dion_layout_single_open_multiple_items.png | Bin 1449 -> 1290 bytes ...yout_single_open_multiple_items_opened.png | Bin 2218 -> 2066 bytes .../accordion_layout_single_open_one_item.png | Bin 1084 -> 964 bytes ...ion_layout_single_open_one_item_opened.png | Bin 1266 -> 1127 bytes 11 files changed, 53 insertions(+), 31 deletions(-) diff --git a/widget/accordion.go b/widget/accordion.go index ba1e673d98..5f807f8f28 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -7,6 +7,8 @@ import ( "fyne.io/fyne/theme" ) +const accordionDividerHeight = 1 + var _ fyne.Widget = (*Accordion)(nil) // AccordionContainer displays a list of AccordionItems. @@ -129,6 +131,7 @@ type accordionRenderer struct { widget.BaseRenderer container *Accordion headers []*Button + dividers []*canvas.Rectangle } func (r *accordionRenderer) Layout(size fyne.Size) { @@ -136,8 +139,12 @@ func (r *accordionRenderer) Layout(size fyne.Size) { y := 0 for i, ai := range r.container.Items { if i != 0 { - y += theme.Padding() + div := r.dividers[i-1] + div.Move(fyne.NewPos(x, y)) + div.Resize(fyne.NewSize(size.Width, accordionDividerHeight)) + y += accordionDividerHeight + theme.Padding() } + h := r.headers[i] h.Move(fyne.NewPos(x, y)) min := h.MinSize().Height @@ -151,13 +158,17 @@ func (r *accordionRenderer) Layout(size fyne.Size) { d.Resize(fyne.NewSize(size.Width, min)) y += min } + + if i < len(r.container.Items)-1 { + y += theme.Padding() + } } } func (r *accordionRenderer) MinSize() (size fyne.Size) { for i, ai := range r.container.Items { if i != 0 { - size.Height += theme.Padding() + size.Height += theme.Padding()*2 + accordionDividerHeight } min := r.headers[i].MinSize() size.Width = fyne.Max(size.Width, min.Width) @@ -194,6 +205,7 @@ func (r *accordionRenderer) updateObjects() { h.Alignment = ButtonAlignLeading h.IconPlacement = ButtonIconLeadingText h.Hidden = false + h.HideShadow = true h.Text = ai.Title index := i // capture h.OnTapped = func() { @@ -224,6 +236,16 @@ func (r *accordionRenderer) updateObjects() { for _, i := range r.container.Items { objects = append(objects, i.Detail) } + // add dividers + for i = 0; i < len(r.dividers); i++ { + objects = append(objects, r.dividers[i]) + } + // make new dividers + for ; i < is-1; i++ { + div := canvas.NewRectangle(theme.ShadowColor()) + r.dividers = append(r.dividers, div) + objects = append(objects, div) + } r.SetObjects(objects) } diff --git a/widget/accordion_internal_test.go b/widget/accordion_internal_test.go index 7bcefd8979..e84c827250 100644 --- a/widget/accordion_internal_test.go +++ b/widget/accordion_internal_test.go @@ -49,11 +49,11 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+theme.Padding(), aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+theme.Padding()*2+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+2*theme.Padding(), aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) }) @@ -69,19 +69,19 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+theme.Padding(), aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+2*theme.Padding()+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+3*theme.Padding(), aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+5*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+2*theme.Padding(), aid1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+3*theme.Padding()+1, aid1.Position().Y) assert.Equal(t, min.Width, aid1.Size().Width) assert.Equal(t, aid1.MinSize().Height, aid1.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+4*theme.Padding(), aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+6*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -96,15 +96,15 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+1*theme.Padding(), aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+1*theme.Padding()*2+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+2*theme.Padding(), aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aih2.MinSize().Height+3*theme.Padding(), aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aih2.MinSize().Height+5*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -118,11 +118,11 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+2*theme.Padding(), aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+3*theme.Padding()+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+4*theme.Padding(), aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+6*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid0.Position().X) @@ -130,11 +130,11 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { assert.Equal(t, min.Width, aid0.Size().Width) assert.Equal(t, aid0.MinSize().Height, aid0.Size().Height) assert.Equal(t, 0, aid1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+3*theme.Padding(), aid1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+1, aid1.Position().Y) assert.Equal(t, min.Width, aid1.Size().Width) assert.Equal(t, aid1.MinSize().Height, aid1.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+5*theme.Padding(), aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+7*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -197,9 +197,9 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { height := aih0.Height height += theme.Padding() height += aid0.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih1.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih2.Height assert.Equal(t, height, min.Height) }) @@ -226,11 +226,11 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { height := aih0.Height height += theme.Padding() height += aid0.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih1.Height height += theme.Padding() height += aid1.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih2.Height height += theme.Padding() height += aid2.Height @@ -260,11 +260,11 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { height := aih0.Height height += theme.Padding() height += aid0.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih1.Height height += theme.Padding() height += aid1.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih2.Height assert.Equal(t, height, min.Height) }) @@ -284,9 +284,9 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { width = fyne.Max(width, aih2.Width) assert.Equal(t, width, min.Width) height := aih0.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih1.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih2.Height assert.Equal(t, height, min.Height) }) diff --git a/widget/accordion_test.go b/widget/accordion_test.go index 3c1fc15057..cb4e23f626 100644 --- a/widget/accordion_test.go +++ b/widget/accordion_test.go @@ -197,11 +197,11 @@ func TestAccordionContainer_Layout(t *testing.T) { func TestAccordionContainer_MinSize(t *testing.T) { minSizeA := fyne.MeasureText("A", theme.TextSize(), fyne.TextStyle{}) - minSizeA.Width += theme.IconInlineSize() + theme.Padding()*7 - minSizeA.Height = fyne.Max(minSizeA.Height, theme.IconInlineSize()) + theme.Padding()*4 + minSizeA.Width += theme.IconInlineSize() + theme.Padding()*5 + minSizeA.Height = fyne.Max(minSizeA.Height, theme.IconInlineSize()) + theme.Padding()*2 minSizeB := fyne.MeasureText("B", theme.TextSize(), fyne.TextStyle{}) - minSizeB.Width += theme.IconInlineSize() + theme.Padding()*7 - minSizeB.Height = fyne.Max(minSizeB.Height, theme.IconInlineSize()) + theme.Padding()*4 + minSizeB.Width += theme.IconInlineSize() + theme.Padding()*5 + minSizeB.Height = fyne.Max(minSizeB.Height, theme.IconInlineSize()) + theme.Padding()*2 minSize1 := fyne.MeasureText("111111", theme.TextSize(), fyne.TextStyle{}) minSize1.Width += theme.Padding() * 2 minSize1.Height += theme.Padding() * 2 @@ -251,7 +251,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("2222222222"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*2+1), }, "single_open_multiple_items_opened": { items: []*widget.AccordionItem{ @@ -265,7 +265,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0, 1}, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize2.Height+theme.Padding()*2), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize2.Height+theme.Padding()*3+1), }, "multiple_open_one_item": { multiOpen: true, @@ -300,7 +300,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("2222222222"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*2+1), }, "multiple_open_multiple_items_opened": { multiOpen: true, @@ -315,7 +315,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0, 1}, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize1.Height+minSize2.Height+theme.Padding()*3), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize1.Height+minSize2.Height+theme.Padding()*4+1), }, } { t.Run(name, func(t *testing.T) { diff --git a/widget/testdata/accordion_layout_multiple_open_multiple_items.png b/widget/testdata/accordion_layout_multiple_open_multiple_items.png index 2cef627dddeec6d31cf6a4e6ecdf55d6101bef93..a6f11793b872a9e1a58316ce859f9f0ad3273662 100644 GIT binary patch literal 1290 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`%0R)5S5QV$Pd8`@N&HWsZMT z?_9!od*jkvr_2R^xr3ynsxpttzgX14(b>J&{vU&Sx}Z9HX=y-6K!CA^hfa<@0zh=*=t|6yBPTs+g~*wdoa%{OE8zQ4O`eOvgZyA}86 zpEX7^zf{=h>gp~H(v<7JJ?}+i#ih+3o;^z|DERQ>#}E1A_BB5)T)+Oj?EdH9TQ+S9 z(hxcHupm48^`S#fHgfM@y;}8T50C$>3vbKH%gWpqU-VH6wv*ss`Sttv`?u*mkufnQ z$F5wv)^_;ebp7~cK`TqF{{H;@oXh|39ow%?ufG3IPfPn#xBs)50Edsi|Ni>_|L*DA z*xKH`a|fvH`t|C)_vG#CB$BsGyR}nnnzq&7Ql_Vr3IoX=y_tj{Mbw6!RPEJ0UU?9R38y_D%_ngtO+J(hu zSFc?8^EdnTwl=opGdFME{PkCzTic|QOFrLv{dHgMZ!@9JBT0ty&!<(?D;1CZ4=DQ%-+3y3-o?zNr^-Js+5Gpjy-#PRD_Prh|md(jg37yk#XLys26Xh z2CVz>K~`U;{$5cWd-K6>-@bYAoo=}K@zBSwudjdn`0?V!i_4ZRJAC+XhRLk6Y1)=L z4RigM_#M4}*W{qY=U-p5(~r-bZ(pycuP@Hk`u6SHXV0E>`Enh&@$u2RPqvR%hp(5* zRMI+AyHI(eN6qhVZ(qH7_2|(diO(-DFZWPMs@6E2z{%Zu=;TSy(9o;L`{iH1dbMl! z?)W_wi9o3@^-C%?>_2xRcF5&TSiIM>GBEuA-{p67*$SmUH-KdvgQu&X%Q~loCIGN_ Blq3KE literal 1449 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`$DI>EaktG3U*l)85kAGRHoe zPxf~94PUxlWy-X;e~nJg&iq<&7kVE#dED9YWmCyRukA{^1P^ZIc8%r8a&Yx@5jfiV z$Vp{Fhn@*1W8eh|-h)pXm={J=eK~UG%EKlAS7iIQj>LVEV;6W4n!3Z~eDy`SRqAGft*-9e>>E^62jFayS1a^OqNhEel#1ppjxWTUuKB z^UoRu0fQ=~SkFSSXGJ^z{rmU$@nh@b_xIP|zk7GB#`itvr%j)3JhSKcbi96)~nAylQu>?TyXpCyO%F_Zl5oZl$Ynn5f~c!^kan#pSy)j zpWEX2eKi*=vLa6uDVFYx2@Vd{o9_Mh;@dKRKR>Iv=c?;JK6#Roob1dI7#y6u`R1w9 zr&|T;uWk*Q*0Mh=2-I}%V@=KQAzPq+;`SSaq|9m@H85s$~KEn;?osteMZ@`ww)_?hbV3|hO z)vR6@CDnMzSy!`aPnZjIC7gQxSv7vj`CI?}cBj8J_4KHEv=^FM?^t=L-?8G5+s@Z? zd~NfKeq5>e+hkv-j>g8u0F5R4w%yFhzrXKm)!w@O_w%+(+s^WPAgm_TnPC$3qcA^T zUxaJznl(MWy^>bb>*k$k-Lh_7UU_+Waq;G*OIo~I#M~F?Y!siic;~y;qq(w6e*OFV`^AeFCr)_O*8Y9? zaN+sq-NzpT*M=?d$&R*tN^b-d^0P=HH*2ufIy!PVt-PamYt)vX7ds*y(nD z`Bke|-@bi&zHK$gcbb2VMOTJs$@PDh&M9|M5?p@yWr2m)zD+LkJyu%aDk7rUU$#WE xAAEex@ryg2?1xJ`wiH35vgofg1H=FS78fd|=eE6G2P}daJYD@<);T3K0RY1Iqe1`x diff --git a/widget/testdata/accordion_layout_multiple_open_multiple_items_opened.png b/widget/testdata/accordion_layout_multiple_open_multiple_items_opened.png index 039cb0a1e6e0bedc0a0044278d48e6cbcdb459b1..f5162336d6789a1012df7db55ea99d1dab7ea9e4 100644 GIT binary patch literal 2188 zcmcImcTm$=9%uG$DN>YE;Z~`_CIO7FRHe5_O9-HpWtS#MM}kD8#{oR zHsAb2IYQ)JZ*PrmcDr^dEN}{%?>wYmS6Fj_(qKkNtd}Hy69Mzm-hfZNas1vxJ19+P z2%fo4o~Q-wQC0QoRkO4sbXeH> zC{`5=0ugmPJ3E!F(#`es^fWbBg(Ox?G5+c8dP-g0@>}u`yB}G*K4X)U-S49b1cJ(F z06o3BwDgf+zPF;Hf{r&{cS=_v5(vD#BUrA&t!h^+AtNKBu&@vyJy|KB`+;9@aIlY$ zkDniB5sBN?M2aw0$7tq>xMFNjP*D5Lm;Kgzac7meK8Yv1SCeVTt&`5E zaf4)JX4=`=bwA)}OWImnKXE1$N8|n;BC;_(shbnetZi)E@W#h-$#goswRMX>+rnb8 znsE{m5-$A#n00=Z6%BIqM(1>0VsnD3Camex0gktM4Jm;v5Woz0YcPGDwSIN>{(bWHqi|~ z*N4}*;nQj@puLv)m{jW$9}m!q8yy{`rygFDWKhas&b)o*8s< zv^PCH?SNHz6u_JhZLhY%Sl+mCLtkHCOY0?-I)@=5;P4;&5};aITGy`K8!v;xU@*Dt z6;+axOG)t%_n%m-E(G%Ljg1W+?^ePmpSyQUW+E@c;sOJ$n`X+&%jwB`jrH}xlP@d) zvp?c!gTY_}gWc_w(X_O*`}glhc2ZxyWFm>4o}S-oYR=PAx?5U=gP9UnjiY-|T!xV3 zgqX{!gA`z~ee|vmn^CjPwD|Rc1CNxHK3{T|&vlhim^&PSut?fzz{7Esm2Dq31%%?_ z;+~!!Och9ri79l&==R&zqN)!TT$~9zB$GJK2Fax8zOS|Qz~@=)B&xC@^$10zP~2S{(j0%~K#D=jZ2#hljb`APf$aQ#T#{Teu-`)sO53jU)z8t#*C9JS-)&n#onvHN1lG z^ei2XBe9?3a80v(8hxb=c@aiwYkRozOTLALg#jL<6|q=lY-(CrR8;Hna^i&rKV66B zOvuU0`?R@ffm(Xv--16al2BJyZ}K9KjgLpg#L&>ZUl~uOIW7QH34MP15kTBn#(#v{ z*w_FHNQqb*D+SQ@W^0OGot}@l*wx(~78;tY^kZ8HlTAXpI6F7{(>5q2#l;V|mq)g? zP(pVo6snL5TZkyv4RA#u_D(xpf`Yg?mvz#OSYOU8`~DkB$3`Z9%8UAc9Cfe=d@Rn+ Yp6-opD~VY@1{PvYFjxC#+rW(f08s@*SpWb4 literal 2444 zcmb`JX*k>29>?8kJ7t)(Xw_b|L{W;govCfa7HSJhwGl;>SgN&zTJLSqDN5B=MA2HC z7MTc*CAM3Yv9(MP8YC!{L=cLG7kB19)91O*eR0lt&Utap?|jeq^L@@k7bjau zad~kO5fMo{n3XFq767gP$N`{FUCK5T5fO8eS*E(Ukv(iEfjdpjH ztHrm_)B-~iZ$oXz@ed`3ngbyotk6xK49O?ajbQ%>(|H19>n@&N5#bzSECy` zf|HlutG@PNkvqs_Gd;aw?ltEcnJ=P_{eEZ+Ye)Y6jnh)Sh$ zXFJ0~Ll>?0NVlmtq zP6FA%(XmQvg*DMYr_;^6OAEUM8i`*w>-c@qtntLXoe#1D>yneUsy!(Jk^gRsY(980 z1zKTs`orfatn*>V(>Z&)sUw|z`>fO*FaF&e{wvd*l>U-6#5b+QbL}bA_o=R_X=!e5 z<+esHWg+xx&{F}%j$^i2if43m=268eygr2u+{>5RIy!(r3k#JzOfDq~xFnfO*7qR~ z4i8`AuB@(_MX%(!Xi5C=19A@{wAs^g#{m*ifg49B6JOQV_6I>`+V749@Y`az8yg#X zK4k3U$BjGar0gB|)BOA@7cD3h8hW8*pq|NO&dtq%RLtIl&c3II5Fr03 zZQn~fzy2!zw1EM<&i(MggFA6(ZGel3~Oi`9qH!A8&O>15Q#+X{NHl*z3gDH{*jT3MGnr- zOwedFWq5cz2*O}6)IEn}+hJNV;u-1bCyab|Hx>Z|Wvn?4kic%rO#`(SQB7P&98BABoi!l_QU`X zS66kBZ5Ylo(|EuuNin7R!ruKwDZr=b;=Y(h()-U*KoLMPV4c6TL^6_({^@c5ouJwL zC0Oczpc1>$v*dFs!p>{&Rflmz8Sx$N!2JhH-;Uk&6d;BGIkPh|aw#YpjRr`X<;~%6 z?5iTisvXzY*TGKMa&$5M7DT=QL3)yxCnGJLTTtNP<1+2hZ44oTXUJgPa5Ld2Hr(43~Ufy>gWeE_-JZUeM_i4zp z0lCkEjP`4yPTstEvu~Y13!Q27?cL{Z(_^+aH?3@JR<_~_ITF37n>PWq3&3M+OdBl} z!+u#?3$wTXI)S!2)~Oav7G%KG{j%e?z5Vd9GbN2J#oL-@MVd#CV9kvd8Ib4gUvF2x ze(k6(#ra4YOZ0SebBl^<;)7Y};_+Z2{|jfH0Vx?8pPFiYkN(K($r&DHZL!7r5$k2+X#l_5NNSg1hS@PLjO4@@5re!w2T$GZ3WJ5wAU0iMuXb23N zc&*5x+5_j^Ed08K0{}mNKHAUkUijF_G&qiGSmc1C#+f}*WQqY+T!`2$G79`CbZ_wK zETEvGqoehv!!BI+^{s$b+!iWq?*eB92M0HY(hPt6ad{W=yq|nOEI3#j47SzIN4Cd) zSl6?!bC;KvHiAH+cGi4Tp^Sv0G9cB?hm6;{zpSsX51FVxd%bxOuXE|rROkhM!c<56 zwjSb*3j(32pun0W#%%@TukN*>4+@aN?`SEerl!D)>nZg1^Z?t#;qW{2eUgRVa&mHu zi;KJ@r%nRFtzm+XnF3HUGBTo?Z>s aE1?jnl@j^^vyuc%MC`1ctm>gR?)?jRhs8_) diff --git a/widget/testdata/accordion_layout_multiple_open_one_item.png b/widget/testdata/accordion_layout_multiple_open_one_item.png index ed3d50071c5980c93f69ab9360e0012aa42a02bf..ba219bbd685177ff4ac463b75b66a31855404c29 100644 GIT binary patch delta 528 zcmV+r0`L922*d}FBVYrINkluE*YGjndi5gx2FE}%sKD$7DW+6 zR-GA=!ovLge43`Ohkwmxb9Hs~-F_j2!^6W6 z!sO&+9LEC#1DBVVS65e~qod!v`g`$Sq>V;nXJ@C;Xp|qD&F1#@_VDoVyI*{GcsM^l z4lY9p!o6-`~H#zrVh|o@H5QB&-!hQT2vT zD&M6a>=#0qn}3_TxVZTDe0O&@ilVPx_j6!(;aeY%7ectXxheZCFE0-c4wi>oTU*!H z*GZC~6NaKYY;0__TCK6Mu{_UT4>vbATdh_UMbFRAm9A?G@-F;@^3I!>n0Wigb$WU_ zgz)(Icy@MH>AJRn??RQj$k-#V$yx#!;0RR86BtDnK S9bzQ_0000@3C7 z!iv4Jk-tH)P?(}9rIej4)sPKCN>*c{$;_=gw|U?0tvl~K?>qO*m+xoAQ>T8-_nhaP zPBU%gayf`BihBV4Dv{BQli&g`MAsJg0N)e}g;J>$_Y}27QPkPl8TS<5G)uxlq44zd zR4f+b9;CKJBGKF1+dLJUC1I&lDi({w!^3e8Qd>5gZK~bEdy@eH7k|EV!qd~!KmIa5 zKOgrj^=p=Qq_No9*~#T{EiEk%4-b!zk9~c8aZgl_uX`2d^ZApLlh)SOsi~=Qxtz&l z;-08Z;H$7yDy^=r-rwKoPQ@^a07A%y+?{Sdf+tq-NnVl z$jC^;CT)n%knpGCRZ?AEUWO1B78ZJXdMb;Xo0~T`H}ewU-8qkk_B z4i4Vl-o`P`uLk;xSK%jPlR*I#ExcxV!4^ePB9X{uvvChnTOyJ8k#7b3Z(7Rba@;fg z&EMMBr->4(K25YCs6LYs0ZfzN0&*IE1^@v6|Bo-lUhAKu&j0`b07*qoM6N<$f|uG~ A9{>OV diff --git a/widget/testdata/accordion_layout_multiple_open_one_item_opened.png b/widget/testdata/accordion_layout_multiple_open_one_item_opened.png index 229c702a94bebc6a8064a3a777ae43405efd7f0c..a2b9ed93c8e62a3e90ccd4497da6138ee0a7567f 100644 GIT binary patch literal 1127 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`&yC>EaktG3U)~LyzcanTC(q zQNO%iPH|e8a_Y)o)h(A7cW69X(4kwK@2&gD$i0`H^nI$AszBt5d(11s)Q%V{&g1;((~<6E zBm8Kl!1Kj@x`ufDK;&fK|c*R9)E{e9iy#fyVhF1h?tefGPb0m48X^XARFckdp1=$W+5 z;^N{TEB0*OY|OzVe9d=jyxn|z8=IPdqfehcRp6L4YgXOAKR^*l z`BCWJm(~A%{MQ!iKKk|RSD>QVv+Zkse0Y6*{q;h@#P!DE;o+M%Z?2Euz1Pm%e7c8< z)580IUDJ$a7VZ4=@$vDcOP9VX-Z;x(qKD1j2mUJ-2CckO-Ijjo>ea1Vx0cq|CMGJ1 zbze&UAwl`>EXGmg#j;Kzm}Glub*-`PUqP2oy(Uk zQ&U&>UV7=}%a?Dze*XM<;>3x)y}gegKR$i>^zG=KF?kso8X{b+hacYZuRh7Jy&x@3 zZK~JV^XKb-eR+BA+J_GX8zX=oD!*1qoImOqxiky!gN;v?LS^u*!F#P{-mwI9O2XE5}zygH9)78&qol`;+0KeWV Ap#T5? literal 1266 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`(N8)5S5QV$Pe}{u(*|<&S@S z98+9xel}}aN2Jy_`=qbFLMNM+962Rsy=)3=#HpdFs@uIl&h-IWNB~ zF`Bt%)v8U^6Sli94s?&zEzv&`>|~>gN$km=TTos7d%im}GjrMbN%qTY+_So;PMsQ| z6BZJZ5_#?DQP;}Kop*(wKJI>V=*HsmzbD_meS7WNwSIYfD=RCbnLY**IyXvMzlsVS zOWHVZ-n@JF?v;n~v>*Qd{rlFfTj$Q5+m@)YFn816q>VRz{`|RHYu(zll5H2STnUMf ze_ytnpO3GvuaC*M!_g@fyH z6gBPZ^?A3-OH1FrfB$@)zP|qTD_8dX+kP=)%c4bx-j<2y?E235{?HzvD^q{-^6^cZ zHZ4t@x$(fqkA(&jHO31s*Bo!TSv~LiYgswDx?T6~-M#DU>$`US`u^TtQSr%fV&9I> z{F5SVW@cu$KYrVmEfQ^owY9RHE;)1G-Sx=dety%=oilewL`GItRb?HwkpC)M{!n}| z_9W)_v+1mf@AT86y>4&ip5*FHfBm(6V*SkEdE&3Of2%AGa5~xFtj94)9!mtmBDJ{W z(B8d!zkU1G+uM7`xwyD^?b@}|Qujt|*IB)OeST5Vr{HsK52ieC-IEx5M zXU~5A>Y`+5WwmSf?&{LguOB{q_!FQaBq=3j0{v?viTD=*)^b!+9`dm&n1A6@wIW5@Bw zmX?;48{=YQW%=4`?cy~=e*L)VAT#SjegD7D7cTc(75}W+=Wiq3>a4%y)AK0;{+u6S r#Q%N~|J}#}2&qBNdVe2SeEt9bf;G0e>qO9PAcw)z)z4*}Q$iB}qRMPv diff --git a/widget/testdata/accordion_layout_single_open_multiple_items.png b/widget/testdata/accordion_layout_single_open_multiple_items.png index 2cef627dddeec6d31cf6a4e6ecdf55d6101bef93..a6f11793b872a9e1a58316ce859f9f0ad3273662 100644 GIT binary patch literal 1290 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`%0R)5S5QV$Pd8`@N&HWsZMT z?_9!od*jkvr_2R^xr3ynsxpttzgX14(b>J&{vU&Sx}Z9HX=y-6K!CA^hfa<@0zh=*=t|6yBPTs+g~*wdoa%{OE8zQ4O`eOvgZyA}86 zpEX7^zf{=h>gp~H(v<7JJ?}+i#ih+3o;^z|DERQ>#}E1A_BB5)T)+Oj?EdH9TQ+S9 z(hxcHupm48^`S#fHgfM@y;}8T50C$>3vbKH%gWpqU-VH6wv*ss`Sttv`?u*mkufnQ z$F5wv)^_;ebp7~cK`TqF{{H;@oXh|39ow%?ufG3IPfPn#xBs)50Edsi|Ni>_|L*DA z*xKH`a|fvH`t|C)_vG#CB$BsGyR}nnnzq&7Ql_Vr3IoX=y_tj{Mbw6!RPEJ0UU?9R38y_D%_ngtO+J(hu zSFc?8^EdnTwl=opGdFME{PkCzTic|QOFrLv{dHgMZ!@9JBT0ty&!<(?D;1CZ4=DQ%-+3y3-o?zNr^-Js+5Gpjy-#PRD_Prh|md(jg37yk#XLys26Xh z2CVz>K~`U;{$5cWd-K6>-@bYAoo=}K@zBSwudjdn`0?V!i_4ZRJAC+XhRLk6Y1)=L z4RigM_#M4}*W{qY=U-p5(~r-bZ(pycuP@Hk`u6SHXV0E>`Enh&@$u2RPqvR%hp(5* zRMI+AyHI(eN6qhVZ(qH7_2|(diO(-DFZWPMs@6E2z{%Zu=;TSy(9o;L`{iH1dbMl! z?)W_wi9o3@^-C%?>_2xRcF5&TSiIM>GBEuA-{p67*$SmUH-KdvgQu&X%Q~loCIGN_ Blq3KE literal 1449 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`$DI>EaktG3U*l)85kAGRHoe zPxf~94PUxlWy-X;e~nJg&iq<&7kVE#dED9YWmCyRukA{^1P^ZIc8%r8a&Yx@5jfiV z$Vp{Fhn@*1W8eh|-h)pXm={J=eK~UG%EKlAS7iIQj>LVEV;6W4n!3Z~eDy`SRqAGft*-9e>>E^62jFayS1a^OqNhEel#1ppjxWTUuKB z^UoRu0fQ=~SkFSSXGJ^z{rmU$@nh@b_xIP|zk7GB#`itvr%j)3JhSKcbi96)~nAylQu>?TyXpCyO%F_Zl5oZl$Ynn5f~c!^kan#pSy)j zpWEX2eKi*=vLa6uDVFYx2@Vd{o9_Mh;@dKRKR>Iv=c?;JK6#Roob1dI7#y6u`R1w9 zr&|T;uWk*Q*0Mh=2-I}%V@=KQAzPq+;`SSaq|9m@H85s$~KEn;?osteMZ@`ww)_?hbV3|hO z)vR6@CDnMzSy!`aPnZjIC7gQxSv7vj`CI?}cBj8J_4KHEv=^FM?^t=L-?8G5+s@Z? zd~NfKeq5>e+hkv-j>g8u0F5R4w%yFhzrXKm)!w@O_w%+(+s^WPAgm_TnPC$3qcA^T zUxaJznl(MWy^>bb>*k$k-Lh_7UU_+Waq;G*OIo~I#M~F?Y!siic;~y;qq(w6e*OFV`^AeFCr)_O*8Y9? zaN+sq-NzpT*M=?d$&R*tN^b-d^0P=HH*2ufIy!PVt-PamYt)vX7ds*y(nD z`Bke|-@bi&zHK$gcbb2VMOTJs$@PDh&M9|M5?p@yWr2m)zD+LkJyu%aDk7rUU$#WE xAAEex@ryg2?1xJ`wiH35vgofg1H=FS78fd|=eE6G2P}daJYD@<);T3K0RY1Iqe1`x diff --git a/widget/testdata/accordion_layout_single_open_multiple_items_opened.png b/widget/testdata/accordion_layout_single_open_multiple_items_opened.png index c6965a946a1af57f50c74cb3b4d4a9cc4a33d164..f9b4742ca7be6efa065efc1b0e2a499d388168fe 100644 GIT binary patch delta 1668 zcmV-~27CFc5t0y)BYy|kNklHm^T) z!SL_yJ14i-u6?>cuOt0k?0f%i-|d=HcTFcrk|1O;YHxu4DU&e>UXxG=H-86~u)DiE z`O(>q$vTRaI3nG1L5eYHx4%`~8`jnLBsxym#+jYisNN z{ri>NFNTOFjERZayLYeIY=3sU-ND7g#Kh|A>S_B4A@1M5FNDa=&CSfroI7`}!{Kl` zoyp0`3hz4FMfyaVkdScm=FNnJ1o_j%#Kfkirlm`lPJ2VI*W26MD}>mxWs6>~&&kOV zLhxLw&SPP)qTDZ-^Yez!MGRFZvilozZ?~2|M!Lc{r$5(^N*p#uQvIc3V1w`k)R%b!CWrk^y$-9 zt5yD3uh%bLyf`Z>%VM#_#>T3>HI>0692y#OI-S8!2L}hOR%=&RS3^UCs{2x(Y9~_n zFEhPWet!O&H*a3Pd|6UbB7}JU{Q2k4pOx7l40RH^-EOnlY_V8oT2ZUj#>K_O$H%W< zzg`HT(P;E~y<*$Mpu&lNRPHCovsf(QzuaC3;cz%=YiqxK`_|dnDc?LUE>7Y7M!QHS zoZL?c;c~fVRPn#R$R8UFhT`I4B{z#9(w7cuY;63`FO%1A+_+I$SsBiyXV$D)Q&v{? z{rmTa4EPLgnqyO z!-o$#oo@N^<(oHeK5*c`oH=vU-jvGtcck!89}6jnk-C;Ic``dYJ1s4(prD|+xjA~K{rK_Y>eZ_S z1qG?8sq5CQtE;Q?`~A~SQQOYHIrW`UVCDjvP6Xo}TXUcqB=3I-Mqy z>B^NWZ{NOu?d|Q|wr$(??c3#Bm6w;7mXU%q_VU@&xabVSLtrlzLc+}t~N z?%3`22M->sTD9ubsZ+n7rZmOhzi@19Ecmg{=SxmbzJ2?4=%JpT9)rQ)a=E6gOOjMv zTzu@+7?#v!|V=D8)~tlV1yeuhnYh`+evkolf_cH<54kmp4&m znnt6U{P*KwPE%Awzny7sZ`W$I8#ZhRJ@n|&qtw*Y&{r9HJRY0Pwsq^)D4Z4u1fD#3 zGSg|ZF9P>;nakypA7DZcb#-;6q@?us_fJ{(`Fy)~@7}j>UldN0B&n{hE-NeR%aiy#Oj%~L*^EYGLqo%q^`Ad~R#a5v<>d_z4@cWHNs<~G z8jMDx&1MUKnsO9RI6OR@pPzs9=+V&stJkl8UmK0arlzJT>w!R^qN1X(urTy1uKF}d zk{TNuSFc_@Ffb7QG({=<;KPhnx<H(vSpQ(mEGOlXV0FUKYxDkP-<$b&*xKp+O1o+PMkPVQ&W?ko*q1ul$4~?>Hcm} zngU^R}TJZ(b& O0000SrWbW?`9H zB;7DJ1B33AQ7Kt=BlAunl1ULnbffN=8D;-0v(%Z8S&*`%RM6;7+r-eyGTpN6{GbEF z?{;-HuXeWcve)C*Jcqse=+m98wQ~v-MNuGRDt|A4{uC3!W0TYeB$E&UCzId@7?ThI z2t$E#3>_aIcQ_n!Ova^BsbdY*=%-h zjEagXEiL_RKS2=g-Mc3ULV9|7a&mHLXlP$w-@w2?Y;3H|t5yo797rQ0Bd=e-9vK-a zeHj%MRa;vd5fSm*I}Q&IcXV_Jg0Ok>W{pOZmX;<6!e4bPRZ7CC6{UX8od53O!-sae zJvcb{&Ye5w&Yc?@8~fW5Du3cY>LgW^Zg_YX=_qQoTF;+9@7(zQ{rj%2uB4peLWKbkPevh=g&WU_^|VZ&$n;iDl01+ z8X8WVI1w0#{SA6TLC=N%fs}CL#*I^7xMpNz2!inOc%buPdgTc_++FD&*?bmhrj^Yla z=AWi}uguKM*RNl{c<~}HFHaDJXV0E}`t(Vj1uBlR5}M6sold9M>!({$tyYJGgoK5K zty{NF5QKn$0F6c?+kY~ZML`Ersh<>2uh)Bi-4a0%`uh6H%geuh{o2yfBHcYCBt+)* zDuq%KPVFZMg2`l>R>gNeNw;HSVsdkH?z6gS_Y}vAu zloWq2%135MLYvJtG&B?#7`SA~l1-a7?c29+)~s3nUX+h`|D1}ku`!Fq!cVZd=Rc4fXounFaP-_!nZ60Ra~eymXL{@X|r>c-GBO%a^ZOwd&x(gBFWL+CDryeDdVU)YR03goLcDteZD)DregF z@82(9zMPen6(1kJcJ11ViVB;}_SC%6jH*Y#RI<{`zx^3Gw>0X6}h57mUPo6yK?d`pI@nTF&OmlOyLZ;Q$ z)~2VY-@bjjx3~BH{rf9ctT=V*)W1)Yo4g4{QJk2VaNf3Bt+BDOw{G2X-__pU9upH| zGMQYqi=vpDn|t)=QH4x%I2=~1)w!#xswy=#^|#YxCHsGYbn5kA)M~Z#xOd+*^*^9@ zxnzu5t^UWm_+?r^K)}>N3 zMMX+V%IAO2pFN)@2OlN;@Zm#xdit?r$J}?leEBjlF|n?$&Sm@L<;oSUR@>X#>-jX9_#)x(@Ni~kW?^BW-EMc;Wi%SKT5WZ8wafM&KYkPy6=h^( zjEsyZZJH>G)z#Hnt=4EXdOl4)vL_rF8OhAdJaT{Ji2HZ->eVZ)R$E(J>$2T$w-*%^ zWoKu*|HS=1O%%nNnwpg>S9W)Idp=E8@_vGCbaXT?FHfV)aI|A8)l<{XXr+jT^_0A1^B_OH533 z?uu28jt&eA{M9rWey;yR-h}P#?fLonE*rzc!=FEYUR70f;lc%%T{$^9m6etMc%FW1 zYpcm*^6#{>XV2#5=DNS_QeIvj6&3ZXY3_fzcky$i@c1@2lkox*lkf)!lMn$Xli&v! nlMn$Xli&vk8~gtN00960M;GizJ_Ki800000NkvXXu0mjf^6l3w diff --git a/widget/testdata/accordion_layout_single_open_one_item.png b/widget/testdata/accordion_layout_single_open_one_item.png index ed3d50071c5980c93f69ab9360e0012aa42a02bf..ba219bbd685177ff4ac463b75b66a31855404c29 100644 GIT binary patch delta 528 zcmV+r0`L922*d}FBVYrINkluE*YGjndi5gx2FE}%sKD$7DW+6 zR-GA=!ovLge43`Ohkwmxb9Hs~-F_j2!^6W6 z!sO&+9LEC#1DBVVS65e~qod!v`g`$Sq>V;nXJ@C;Xp|qD&F1#@_VDoVyI*{GcsM^l z4lY9p!o6-`~H#zrVh|o@H5QB&-!hQT2vT zD&M6a>=#0qn}3_TxVZTDe0O&@ilVPx_j6!(;aeY%7ectXxheZCFE0-c4wi>oTU*!H z*GZC~6NaKYY;0__TCK6Mu{_UT4>vbATdh_UMbFRAm9A?G@-F;@^3I!>n0Wigb$WU_ zgz)(Icy@MH>AJRn??RQj$k-#V$yx#!;0RR86BtDnK S9bzQ_0000@3C7 z!iv4Jk-tH)P?(}9rIej4)sPKCN>*c{$;_=gw|U?0tvl~K?>qO*m+xoAQ>T8-_nhaP zPBU%gayf`BihBV4Dv{BQli&g`MAsJg0N)e}g;J>$_Y}27QPkPl8TS<5G)uxlq44zd zR4f+b9;CKJBGKF1+dLJUC1I&lDi({w!^3e8Qd>5gZK~bEdy@eH7k|EV!qd~!KmIa5 zKOgrj^=p=Qq_No9*~#T{EiEk%4-b!zk9~c8aZgl_uX`2d^ZApLlh)SOsi~=Qxtz&l z;-08Z;H$7yDy^=r-rwKoPQ@^a07A%y+?{Sdf+tq-NnVl z$jC^;CT)n%knpGCRZ?AEUWO1B78ZJXdMb;Xo0~T`H}ewU-8qkk_B z4i4Vl-o`P`uLk;xSK%jPlR*I#ExcxV!4^ePB9X{uvvChnTOyJ8k#7b3Z(7Rba@;fg z&EMMBr->4(K25YCs6LYs0ZfzN0&*IE1^@v6|Bo-lUhAKu&j0`b07*qoM6N<$f|uG~ A9{>OV diff --git a/widget/testdata/accordion_layout_single_open_one_item_opened.png b/widget/testdata/accordion_layout_single_open_one_item_opened.png index 229c702a94bebc6a8064a3a777ae43405efd7f0c..a2b9ed93c8e62a3e90ccd4497da6138ee0a7567f 100644 GIT binary patch literal 1127 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`&yC>EaktG3U)~LyzcanTC(q zQNO%iPH|e8a_Y)o)h(A7cW69X(4kwK@2&gD$i0`H^nI$AszBt5d(11s)Q%V{&g1;((~<6E zBm8Kl!1Kj@x`ufDK;&fK|c*R9)E{e9iy#fyVhF1h?tefGPb0m48X^XARFckdp1=$W+5 z;^N{TEB0*OY|OzVe9d=jyxn|z8=IPdqfehcRp6L4YgXOAKR^*l z`BCWJm(~A%{MQ!iKKk|RSD>QVv+Zkse0Y6*{q;h@#P!DE;o+M%Z?2Euz1Pm%e7c8< z)580IUDJ$a7VZ4=@$vDcOP9VX-Z;x(qKD1j2mUJ-2CckO-Ijjo>ea1Vx0cq|CMGJ1 zbze&UAwl`>EXGmg#j;Kzm}Glub*-`PUqP2oy(Uk zQ&U&>UV7=}%a?Dze*XM<;>3x)y}gegKR$i>^zG=KF?kso8X{b+hacYZuRh7Jy&x@3 zZK~JV^XKb-eR+BA+J_GX8zX=oD!*1qoImOqxiky!gN;v?LS^u*!F#P{-mwI9O2XE5}zygH9)78&qol`;+0KeWV Ap#T5? literal 1266 zcmeAS@N?(olHy`uVBq!ia0vp^(}4H{2NRHNeco@xz`(N8)5S5QV$Pe}{u(*|<&S@S z98+9xel}}aN2Jy_`=qbFLMNM+962Rsy=)3=#HpdFs@uIl&h-IWNB~ zF`Bt%)v8U^6Sli94s?&zEzv&`>|~>gN$km=TTos7d%im}GjrMbN%qTY+_So;PMsQ| z6BZJZ5_#?DQP;}Kop*(wKJI>V=*HsmzbD_meS7WNwSIYfD=RCbnLY**IyXvMzlsVS zOWHVZ-n@JF?v;n~v>*Qd{rlFfTj$Q5+m@)YFn816q>VRz{`|RHYu(zll5H2STnUMf ze_ytnpO3GvuaC*M!_g@fyH z6gBPZ^?A3-OH1FrfB$@)zP|qTD_8dX+kP=)%c4bx-j<2y?E235{?HzvD^q{-^6^cZ zHZ4t@x$(fqkA(&jHO31s*Bo!TSv~LiYgswDx?T6~-M#DU>$`US`u^TtQSr%fV&9I> z{F5SVW@cu$KYrVmEfQ^owY9RHE;)1G-Sx=dety%=oilewL`GItRb?HwkpC)M{!n}| z_9W)_v+1mf@AT86y>4&ip5*FHfBm(6V*SkEdE&3Of2%AGa5~xFtj94)9!mtmBDJ{W z(B8d!zkU1G+uM7`xwyD^?b@}|Qujt|*IB)OeST5Vr{HsK52ieC-IEx5M zXU~5A>Y`+5WwmSf?&{LguOB{q_!FQaBq=3j0{v?viTD=*)^b!+9`dm&n1A6@wIW5@Bw zmX?;48{=YQW%=4`?cy~=e*L)VAT#SjegD7D7cTc(75}W+=Wiq3>a4%y)AK0;{+u6S r#Q%N~|J}#}2&qBNdVe2SeEt9bf;G0e>qO9PAcw)z)z4*}Q$iB}qRMPv From 8ae1fec92c3e9b533d1eb881bd07979c9aee2281 Mon Sep 17 00:00:00 2001 From: Garrett Cornwell <484641-garrettcornwell@users.noreply.gitlab.com> Date: Wed, 16 Sep 2020 14:22:02 -0600 Subject: [PATCH 06/19] fix: get GOARCH from environment --- cmd/fyne/package.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/fyne/package.go b/cmd/fyne/package.go index 572d1f8936..803c98012f 100644 --- a/cmd/fyne/package.go +++ b/cmd/fyne/package.go @@ -285,7 +285,13 @@ func (p *packager) packageWindows() error { vi.Build() vi.Walk() - err = vi.WriteSyso(outPath, runtime.GOARCH) + + arch, ok := os.LookupEnv("GOARCH") + if !ok { + arch = runtime.GOARCH + } + + err = vi.WriteSyso(outPath, arch) if err != nil { return errors.Wrap(err, "Failed to write .syso file") } From d12ce856aa1e477c8492dd503fc30856caf1a8fa Mon Sep 17 00:00:00 2001 From: Stephen M Houston Date: Wed, 16 Sep 2020 16:48:32 -0500 Subject: [PATCH 07/19] Add a list widget that caches items for performance (#1253) * Add a list widget that caches items for performance Co-authored-by: Stuart Scott --- cmd/fyne_demo/screens/widget.go | 30 ++ widget/list.go | 426 +++++++++++++++++++ widget/list_test.go | 178 ++++++++ widget/scroller.go | 15 +- widget/testdata/list/list_initial.png | Bin 0 -> 13214 bytes widget/testdata/list/list_new_data.png | Bin 0 -> 9367 bytes widget/testdata/list/list_offset_changed.png | Bin 0 -> 13092 bytes widget/testdata/list/list_resized.png | Bin 0 -> 24877 bytes widget/testdata/list/list_theme_changed.png | Bin 0 -> 26380 bytes 9 files changed, 645 insertions(+), 4 deletions(-) create mode 100644 widget/list.go create mode 100644 widget/list_test.go create mode 100644 widget/testdata/list/list_initial.png create mode 100644 widget/testdata/list/list_new_data.png create mode 100644 widget/testdata/list/list_offset_changed.png create mode 100644 widget/testdata/list/list_resized.png create mode 100644 widget/testdata/list/list_theme_changed.png diff --git a/cmd/fyne_demo/screens/widget.go b/cmd/fyne_demo/screens/widget.go index 164b59cbfe..fc777b7e2c 100644 --- a/cmd/fyne_demo/screens/widget.go +++ b/cmd/fyne_demo/screens/widget.go @@ -289,6 +289,35 @@ func stopProgress() { endProgress <- struct{}{} } +func makeListTab() fyne.CanvasObject { + var data []string + for i := 0; i < 1000; i++ { + data = append(data, fmt.Sprintf("Test Item %d", i)) + } + + icon := widget.NewIcon(nil) + label := widget.NewLabel("Select An Item From The List") + hbox := fyne.NewContainerWithLayout(layout.NewHBoxLayout(), icon, label) + + list := widget.NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return fyne.NewContainerWithLayout(layout.NewHBoxLayout(), widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object")) + }, + func(index int, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*widget.Label).SetText(data[index]) + }, + ) + list.OnItemSelected = func(index int) { + label.SetText(data[index]) + icon.SetResource(theme.DocumentIcon()) + } + split := widget.NewHSplitContainer(list, fyne.NewContainerWithLayout(layout.NewCenterLayout(), hbox)) + return fyne.NewContainerWithLayout(layout.NewMaxLayout(), split) +} + // WidgetScreen shows a panel containing widget demos func WidgetScreen() fyne.CanvasObject { toolbar := widget.NewToolbar(widget.NewToolbarAction(theme.MailComposeIcon(), func() { fmt.Println("New") }), @@ -306,6 +335,7 @@ func WidgetScreen() fyne.CanvasObject { widget.NewTabItem("Input", makeInputTab()), widget.NewTabItem("Progress", progress), widget.NewTabItem("Form", makeFormTab()), + widget.NewTabItem("List", makeListTab()), ) tabs.OnChanged = func(t *widget.TabItem) { if t.Content == progress { diff --git a/widget/list.go b/widget/list.go new file mode 100644 index 0000000000..bf9cdbf9c1 --- /dev/null +++ b/widget/list.go @@ -0,0 +1,426 @@ +package widget + +import ( + "math" + + "fyne.io/fyne" + "fyne.io/fyne/canvas" + "fyne.io/fyne/driver/desktop" + "fyne.io/fyne/internal/widget" + "fyne.io/fyne/theme" +) + +// Declare conformity with Widget interface. +var _ fyne.Widget = (*List)(nil) + +// List is a widget that pools list items for performance and +// lays the items out in a vertical direction inside of a scroller. +// List requires that all items are the same size. +type List struct { + BaseWidget + + Length func() int + CreateItem func() fyne.CanvasObject + UpdateItem func(index int, item fyne.CanvasObject) + OnItemSelected func(index int) + selectedItem *listItem + selectedIndex int + itemMin fyne.Size + offsetY int +} + +// NewList creates and returns a list widget for displaying items in +// a vertical layout with scrolling and caching for performance. +func NewList(length func() int, createItem func() fyne.CanvasObject, updateItem func(index int, item fyne.CanvasObject)) *List { + list := &List{BaseWidget: BaseWidget{}, Length: length, CreateItem: createItem, UpdateItem: updateItem, selectedIndex: -1} + list.ExtendBaseWidget(list) + return list +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (l *List) CreateRenderer() fyne.WidgetRenderer { + l.ExtendBaseWidget(l) + + if f := l.CreateItem; f != nil { + if l.itemMin.IsZero() { + l.itemMin = newListItem(f(), nil).MinSize() + } + } + layout := fyne.NewContainerWithLayout(newListLayout(l)) + layout.Resize(layout.MinSize()) + scroller := NewVScrollContainer(layout) + objects := []fyne.CanvasObject{scroller} + return newListRenderer(objects, l, scroller, layout) +} + +// MinSize returns the size that this widget should not shrink below. +func (l *List) MinSize() fyne.Size { + l.ExtendBaseWidget(l) + + return l.BaseWidget.MinSize() +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*listRenderer)(nil) + +type listRenderer struct { + widget.BaseRenderer + + list *List + scroller *ScrollContainer + layout *fyne.Container + itemPool *syncPool + children []fyne.CanvasObject + visibleItemCount int + firstItemIndex int + lastItemIndex int + size fyne.Size + previousOffsetY int +} + +func newListRenderer(objects []fyne.CanvasObject, l *List, scroller *ScrollContainer, layout *fyne.Container) *listRenderer { + lr := &listRenderer{BaseRenderer: widget.NewBaseRenderer(objects), list: l, scroller: scroller, layout: layout} + lr.scroller.onOffsetChanged = func() { + if lr.list.offsetY == lr.scroller.Offset.Y { + return + } + lr.list.offsetY = lr.scroller.Offset.Y + lr.offsetChanged() + } + return lr +} + +func (l *listRenderer) Layout(size fyne.Size) { + if l.list.Length() == 0 { + if len(l.children) > 0 { + for _, child := range l.children { + l.itemPool.Release(child) + } + l.children = nil + l.list.Refresh() + } + return + } + if size != l.size { + if size.Width != l.size.Width { + for _, child := range l.children { + child.Resize(fyne.NewSize(size.Width, l.list.itemMin.Height)) + } + } + l.scroller.Resize(size) + l.size = size + } + if l.itemPool == nil { + l.itemPool = &syncPool{} + } + + // Relayout What Is Visible - no scroll change - initial layout or possibly from a resize. + l.visibleItemCount = int(math.Ceil(float64(l.scroller.size.Height) / float64(l.list.itemMin.Height))) + if len(l.children) > l.visibleItemCount { + for i := len(l.children); i > l.visibleItemCount; i-- { + l.itemPool.Release(l.children[len(l.children)-1]) + l.children = l.children[:len(l.children)-1] + } + } + if l.visibleItemCount > 0 && l.list.Length() < l.visibleItemCount && len(l.children) > 0 { + for i := l.visibleItemCount; i >= l.list.Length(); i-- { + l.itemPool.Release(l.children[len(l.children)-1]) + l.children = l.children[:len(l.children)-1] + } + } + for i := len(l.children) + l.firstItemIndex; len(l.children) <= l.visibleItemCount && i < l.list.Length(); i++ { + l.appendItem(i) + } + l.layout.Objects = l.children + l.layout.Layout.Layout(l.layout.Objects, l.list.itemMin) + l.lastItemIndex = l.firstItemIndex + len(l.children) - 1 + + i := l.firstItemIndex + for _, child := range l.children { + if f := l.list.UpdateItem; f != nil { + f(i, child.(*listItem).child) + } + i++ + } +} + +func (l *listRenderer) MinSize() fyne.Size { + return l.scroller.MinSize() +} + +func (l *listRenderer) Refresh() { + if f := l.list.CreateItem; f != nil { + l.list.itemMin = newListItem(f(), nil).MinSize() + } + l.Layout(l.list.Size()) + l.scroller.Refresh() + canvas.Refresh(l.list.super()) +} + +func (l *listRenderer) appendItem(index int) { + item := l.getItem() + l.children = append(l.children, item) + l.setupListItem(item, index) + l.layout.Objects = l.children + l.layout.Layout.(*listLayout).appendedItem(l.layout.Objects) +} + +func (l *listRenderer) getItem() fyne.CanvasObject { + item := l.itemPool.Obtain() + if item == nil { + if f := l.list.CreateItem; f != nil { + item = newListItem(f(), nil) + } + } + return item +} + +func (l *listRenderer) offsetChanged() { + offsetChange := int(math.Abs(float64(l.previousOffsetY - l.list.offsetY))) + + if l.previousOffsetY < l.list.offsetY { + // Scrolling Down. + l.scrollDown(offsetChange) + return + + } else if l.previousOffsetY > l.list.offsetY { + // Scrolling Up. + l.scrollUp(offsetChange) + return + } +} + +func (l *listRenderer) prependItem(index int) { + item := l.getItem() + l.children = append([]fyne.CanvasObject{item}, l.children...) + l.setupListItem(item, index) + l.layout.Objects = l.children + l.layout.Layout.(*listLayout).prependedItem(l.layout.Objects) +} + +func (l *listRenderer) scrollDown(offsetChange int) { + itemChange := 0 + layoutEndY := l.children[len(l.children)-1].Position().Y + l.list.itemMin.Height + scrollerEndY := l.scroller.Offset.Y + l.scroller.Size().Height + if layoutEndY < scrollerEndY { + itemChange = int(math.Ceil(float64(scrollerEndY-layoutEndY) / float64(l.list.itemMin.Height))) + } else if offsetChange < l.list.itemMin.Height { + return + } else { + itemChange = int(math.Floor(float64(offsetChange) / float64(l.list.itemMin.Height))) + } + l.previousOffsetY = l.list.offsetY + for i := 0; i < itemChange && l.lastItemIndex != l.list.Length()-1; i++ { + l.itemPool.Release(l.children[0]) + l.children = l.children[1:] + l.firstItemIndex++ + l.lastItemIndex++ + l.appendItem(l.lastItemIndex) + } +} + +func (l *listRenderer) scrollUp(offsetChange int) { + itemChange := 0 + layoutStartY := l.children[0].Position().Y + if layoutStartY > l.scroller.Offset.Y { + itemChange = int(math.Ceil(float64(layoutStartY-l.scroller.Offset.Y) / float64(l.list.itemMin.Height))) + } else if offsetChange < l.list.itemMin.Height { + return + } else { + itemChange = int(math.Floor(float64(offsetChange) / float64(l.list.itemMin.Height))) + } + l.previousOffsetY = l.list.offsetY + for i := 0; i < itemChange && l.firstItemIndex != 0; i++ { + l.itemPool.Release(l.children[len(l.children)-1]) + l.children = l.children[:len(l.children)-1] + l.firstItemIndex-- + l.lastItemIndex-- + l.prependItem(l.firstItemIndex) + } +} + +func (l *listRenderer) setupListItem(item fyne.CanvasObject, index int) { + previousIndicator := item.(*listItem).selected + if index != l.list.selectedIndex { + item.(*listItem).selected = false + } else { + item.(*listItem).selected = true + l.list.selectedItem = item.(*listItem) + } + if previousIndicator != item.(*listItem).selected { + item.Refresh() + } + if f := l.list.UpdateItem; f != nil { + f(index, item.(*listItem).child) + } + item.(*listItem).onTapped = func() { + if l.list.selectedItem != nil && l.list.selectedIndex >= l.firstItemIndex && l.list.selectedIndex <= l.lastItemIndex { + l.list.selectedItem.selected = false + l.list.selectedItem.Refresh() + } + l.list.selectedItem = item.(*listItem) + l.list.selectedIndex = index + l.list.selectedItem.selected = true + l.list.selectedItem.Refresh() + if f := l.list.OnItemSelected; f != nil { + f(index) + } + } +} + +// Declare conformity with interfaces. +var _ fyne.Widget = (*listItem)(nil) +var _ fyne.Tappable = (*listItem)(nil) +var _ desktop.Hoverable = (*listItem)(nil) + +type listItem struct { + BaseWidget + + onTapped func() + statusIndicator *canvas.Rectangle + child fyne.CanvasObject + divider *canvas.Rectangle + hovered, selected bool +} + +func newListItem(child fyne.CanvasObject, tapped func()) *listItem { + li := &listItem{ + child: child, + onTapped: tapped, + } + + li.ExtendBaseWidget(li) + return li +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (li *listItem) CreateRenderer() fyne.WidgetRenderer { + li.ExtendBaseWidget(li) + + li.statusIndicator = canvas.NewRectangle(theme.BackgroundColor()) + li.divider = canvas.NewRectangle(theme.ShadowColor()) + + objects := []fyne.CanvasObject{li.statusIndicator, li.child, li.divider} + + return &listItemRenderer{widget.NewBaseRenderer(objects), li} +} + +// MinSize returns the size that this widget should not shrink below. +func (li *listItem) MinSize() fyne.Size { + li.ExtendBaseWidget(li) + return li.BaseWidget.MinSize() +} + +// MouseIn is called when a desktop pointer enters the widget. +func (li *listItem) MouseIn(*desktop.MouseEvent) { + li.hovered = true + li.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget. +func (li *listItem) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget. +func (li *listItem) MouseOut() { + li.hovered = false + li.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any tap handler. +func (li *listItem) Tapped(*fyne.PointEvent) { + if li.onTapped != nil { + li.selected = true + li.Refresh() + li.onTapped() + } +} + +// Declare conformity with the WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*listItemRenderer)(nil) + +type listItemRenderer struct { + widget.BaseRenderer + + item *listItem +} + +// MinSize calculates the minimum size of a listItem. +// This is based on the size of the status indicator and the size of the child object. +func (li *listItemRenderer) MinSize() (size fyne.Size) { + itemSize := li.item.child.MinSize() + size = fyne.NewSize(itemSize.Width+theme.Padding()*3, + itemSize.Height+theme.Padding()*2) + return +} + +// Layout the components of the listItem widget. +func (li *listItemRenderer) Layout(size fyne.Size) { + li.item.statusIndicator.Move(fyne.NewPos(0, 0)) + s := fyne.NewSize(theme.Padding(), size.Height-1) + li.item.statusIndicator.SetMinSize(s) + li.item.statusIndicator.Resize(s) + + li.item.child.Move(fyne.NewPos(theme.Padding()*2, theme.Padding())) + li.item.child.Resize(fyne.NewSize(size.Width-theme.Padding()*3, size.Height-theme.Padding()*2)) + + li.item.divider.Move(fyne.NewPos(theme.Padding(), size.Height-1)) + s = fyne.NewSize(size.Width-theme.Padding()*2, 1) + li.item.divider.SetMinSize(s) + li.item.divider.Resize(s) +} + +func (li *listItemRenderer) Refresh() { + if li.item.selected { + li.item.statusIndicator.FillColor = theme.FocusColor() + } else if li.item.hovered { + li.item.statusIndicator.FillColor = theme.HoverColor() + } else { + li.item.statusIndicator.FillColor = theme.BackgroundColor() + } + canvas.Refresh(li.item.super()) +} + +// Declare conformity with Layout interface. +var _ fyne.Layout = (*listLayout)(nil) + +type listLayout struct { + list *List + layoutEndY int +} + +func newListLayout(list *List) fyne.Layout { + return &listLayout{list: list} +} + +func (l *listLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + if l.list.offsetY != 0 { + return + } + y := 0 + for _, child := range objects { + child.Move(fyne.NewPos(0, y)) + y += l.list.itemMin.Height + child.Resize(fyne.NewSize(l.list.size.Width, l.list.itemMin.Height)) + } + l.layoutEndY = y +} + +func (l *listLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + return fyne.NewSize(l.list.itemMin.Width, + l.list.itemMin.Height*l.list.Length()) +} + +func (l *listLayout) appendedItem(objects []fyne.CanvasObject) { + if len(objects) > 1 { + objects[len(objects)-1].Move(fyne.NewPos(0, objects[len(objects)-2].Position().Y+l.list.itemMin.Height)) + } else { + objects[len(objects)-1].Move(fyne.NewPos(0, 0)) + } + objects[len(objects)-1].Resize(fyne.NewSize(l.list.size.Width, l.list.itemMin.Height)) +} + +func (l *listLayout) prependedItem(objects []fyne.CanvasObject) { + objects[0].Move(fyne.NewPos(0, objects[1].Position().Y-l.list.itemMin.Height)) + objects[0].Resize(fyne.NewSize(l.list.size.Width, l.list.itemMin.Height)) +} diff --git a/widget/list_test.go b/widget/list_test.go new file mode 100644 index 0000000000..f9b478d7fe --- /dev/null +++ b/widget/list_test.go @@ -0,0 +1,178 @@ +package widget + +import ( + "fmt" + "math" + "testing" + "time" + + "fyne.io/fyne" + "fyne.io/fyne/driver/desktop" + "fyne.io/fyne/layout" + "fyne.io/fyne/test" + "fyne.io/fyne/theme" + "github.com/stretchr/testify/assert" +) + +func TestNewList(t *testing.T) { + list := createList() + + template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) + firstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex + lastItemIndex := test.WidgetRenderer(list).(*listRenderer).lastItemIndex + visibleCount := len(test.WidgetRenderer(list).(*listRenderer).children) + + assert.Equal(t, 1000, list.Length()) + assert.GreaterOrEqual(t, list.MinSize().Width, template.MinSize().Width) + assert.Equal(t, list.MinSize(), test.WidgetRenderer(list).(*listRenderer).scroller.MinSize()) + assert.Equal(t, 0, firstItemIndex) + assert.Equal(t, visibleCount, lastItemIndex-firstItemIndex+1) +} + +func TestList_Resize(t *testing.T) { + list := createList() + w := test.NewWindow(list) + w.Resize(fyne.NewSize(200, 1000)) + template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) + + firstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex + lastItemIndex := test.WidgetRenderer(list).(*listRenderer).lastItemIndex + visibleCount := len(test.WidgetRenderer(list).(*listRenderer).children) + assert.Equal(t, 0, firstItemIndex) + assert.Equal(t, visibleCount, lastItemIndex-firstItemIndex+1) + test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) + + w.Resize(fyne.NewSize(200, 2000)) + + indexChange := int(math.Floor(float64(1000) / float64(template.MinSize().Height))) + + newFirstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex + newLastItemIndex := test.WidgetRenderer(list).(*listRenderer).lastItemIndex + newVisibleCount := len(test.WidgetRenderer(list).(*listRenderer).children) + + assert.Equal(t, firstItemIndex, newFirstItemIndex) + assert.NotEqual(t, lastItemIndex, newLastItemIndex) + assert.Equal(t, newLastItemIndex, lastItemIndex+indexChange) + assert.NotEqual(t, visibleCount, newVisibleCount) + assert.Equal(t, newVisibleCount, newLastItemIndex-newFirstItemIndex+1) + test.AssertImageMatches(t, "list/list_resized.png", w.Canvas().Capture()) +} + +func TestList_OffsetChange(t *testing.T) { + list := createList() + w := test.NewWindow(list) + w.Resize(fyne.NewSize(200, 1000)) + template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) + + firstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex + lastItemIndex := test.WidgetRenderer(list).(*listRenderer).lastItemIndex + visibleCount := test.WidgetRenderer(list).(*listRenderer).visibleItemCount + + assert.Equal(t, 0, firstItemIndex) + assert.Equal(t, visibleCount, lastItemIndex-firstItemIndex) + test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) + + scroll := test.WidgetRenderer(list).(*listRenderer).scroller + scroll.Scrolled(&fyne.ScrollEvent{DeltaX: 0, DeltaY: -300}) + + indexChange := int(math.Floor(float64(300) / float64(template.MinSize().Height))) + + newFirstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex + newLastItemIndex := test.WidgetRenderer(list).(*listRenderer).lastItemIndex + newVisibleCount := test.WidgetRenderer(list).(*listRenderer).visibleItemCount + + assert.NotEqual(t, firstItemIndex, newFirstItemIndex) + assert.Equal(t, newFirstItemIndex, firstItemIndex+indexChange-1) + assert.NotEqual(t, lastItemIndex, newLastItemIndex) + assert.Equal(t, newLastItemIndex, lastItemIndex+indexChange-1) + assert.Equal(t, visibleCount, newVisibleCount) + assert.Equal(t, newVisibleCount, newLastItemIndex-newFirstItemIndex) + test.AssertImageMatches(t, "list/list_offset_changed.png", w.Canvas().Capture()) +} + +func TestList_Hover(t *testing.T) { + list := createList() + children := test.WidgetRenderer(list).(*listRenderer).children + + for i := 0; i < 2; i++ { + assert.Equal(t, children[i].(*listItem).statusIndicator.FillColor, theme.BackgroundColor()) + children[i].(*listItem).MouseIn(&desktop.MouseEvent{}) + assert.Equal(t, children[i].(*listItem).statusIndicator.FillColor, theme.HoverColor()) + children[i].(*listItem).MouseOut() + assert.Equal(t, children[i].(*listItem).statusIndicator.FillColor, theme.BackgroundColor()) + } +} + +func TestList_Selection(t *testing.T) { + list := createList() + children := test.WidgetRenderer(list).(*listRenderer).children + + assert.Equal(t, children[0].(*listItem).statusIndicator.FillColor, theme.BackgroundColor()) + children[0].(*listItem).Tapped(&fyne.PointEvent{}) + assert.Equal(t, children[0].(*listItem).statusIndicator.FillColor, theme.FocusColor()) + assert.Equal(t, list.selectedIndex, 0) + children[1].(*listItem).Tapped(&fyne.PointEvent{}) + assert.Equal(t, children[1].(*listItem).statusIndicator.FillColor, theme.FocusColor()) + assert.Equal(t, list.selectedIndex, 1) + assert.Equal(t, children[0].(*listItem).statusIndicator.FillColor, theme.BackgroundColor()) +} + +func TestList_DataChange(t *testing.T) { + list := createList() + w := test.NewWindow(list) + w.Resize(fyne.NewSize(200, 1000)) + children := test.WidgetRenderer(list).(*listRenderer).children + + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "Test Item 0") + test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) + changeData(list) + list.Refresh() + children = test.WidgetRenderer(list).(*listRenderer).children + assert.Equal(t, children[0].(*listItem).child.(*fyne.Container).Objects[1].(*Label).Text, "a") + test.AssertImageMatches(t, "list/list_new_data.png", w.Canvas().Capture()) +} + +func TestList_ThemeChange(t *testing.T) { + list := createList() + w := test.NewWindow(list) + w.Resize(fyne.NewSize(200, 1000)) + + test.AssertImageMatches(t, "list/list_initial.png", w.Canvas().Capture()) + + test.WithTestTheme(t, func() { + time.Sleep(100 * time.Millisecond) + list.Refresh() + test.AssertImageMatches(t, "list/list_theme_changed.png", w.Canvas().Capture()) + }) +} + +func createList() *List { + var data []string + for i := 0; i < 1000; i++ { + data = append(data, fmt.Sprintf("Test Item %d", i)) + } + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")) + }, + func(index int, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[index]) + }, + ) + 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(index int, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[index]) + } +} diff --git a/widget/scroller.go b/widget/scroller.go index b48319c2b2..c471f4e463 100644 --- a/widget/scroller.go +++ b/widget/scroller.go @@ -208,6 +208,9 @@ func (a *scrollBarArea) moveBar(offset int, barSize fyne.Size) { default: a.scroll.Offset.Y = a.computeScrollOffset(barSize.Height, offset, a.scroll.Size().Height, a.scroll.Content.Size().Height) } + if f := a.scroll.onOffsetChanged; f != nil { + f() + } a.scroll.refreshWithoutOffsetUpdate() } @@ -333,10 +336,11 @@ func (r *scrollContainerRenderer) updatePosition() { // The Offset is used to determine the position of the child widgets within the container. type ScrollContainer struct { BaseWidget - minSize fyne.Size - Direction ScrollDirection - Content fyne.CanvasObject - Offset fyne.Position + minSize fyne.Size + Direction ScrollDirection + Content fyne.CanvasObject + Offset fyne.Position + onOffsetChanged func() } // CreateRenderer is a private method to Fyne which links this widget to its renderer @@ -447,6 +451,9 @@ func (s *ScrollContainer) updateOffset(deltaX, deltaY int) bool { } s.Offset.X = computeOffset(s.Offset.X, -deltaX, s.Size().Width, s.Content.MinSize().Width) s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, s.Size().Height, s.Content.MinSize().Height) + if f := s.onOffsetChanged; f != nil { + f() + } return true } diff --git a/widget/testdata/list/list_initial.png b/widget/testdata/list/list_initial.png new file mode 100644 index 0000000000000000000000000000000000000000..7c20c248124bae8720f5241f2f96383fe450e110 GIT binary patch literal 13214 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 literal 0 HcmV?d00001 diff --git a/widget/testdata/list/list_new_data.png b/widget/testdata/list/list_new_data.png new file mode 100644 index 0000000000000000000000000000000000000000..20e2608f5256327b4e8fc7ddfa5057257bbbfe8d GIT binary patch literal 9367 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

-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�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 literal 0 HcmV?d00001 From 0d61e2e92ab2e3f264ba8e5a2329aa758e87fe0c Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 18 Sep 2020 16:14:10 +0100 Subject: [PATCH 08/19] Handle accordion removal (and re-adding) for dividers --- widget/accordion.go | 6 ++++++ widget/accordion_internal_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/widget/accordion.go b/widget/accordion.go index 5f807f8f28..c27758a850 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -198,6 +198,7 @@ func (r *accordionRenderer) updateObjects() { var h *Button if i < hs { h = r.headers[i] + h.Show() } else { h = &Button{} r.headers = append(r.headers, h) @@ -238,6 +239,11 @@ func (r *accordionRenderer) updateObjects() { } // add dividers for i = 0; i < len(r.dividers); i++ { + if i < len(r.container.Items)-1 { + r.dividers[i].Show() + } else { + r.dividers[i].Hide() + } objects = append(objects, r.dividers[i]) } // make new dividers diff --git a/widget/accordion_internal_test.go b/widget/accordion_internal_test.go index e84c827250..40a0fa6bc0 100644 --- a/widget/accordion_internal_test.go +++ b/widget/accordion_internal_test.go @@ -292,3 +292,27 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { }) }) } + +func TestAccordionContainerRenderer_AddRemove(t *testing.T) { + ac := NewAccordionContainer() + ar := test.WidgetRenderer(ac).(*accordionRenderer) + ac.Append(NewAccordionItem("foo0", NewLabel("foobar0"))) + ac.Append(NewAccordionItem("foo1", NewLabel("foobar1"))) + ac.Append(NewAccordionItem("foo2", NewLabel("foobar2"))) + + assert.Equal(t, 3, len(ac.Items)) + assert.Equal(t, 3, len(ar.headers)) + assert.Equal(t, 2, len(ar.dividers)) + assert.True(t, ar.headers[2].Visible()) + assert.True(t, ar.dividers[1].Visible()) + + ac.RemoveIndex(2) + assert.Equal(t, 2, len(ac.Items)) + assert.False(t, ar.headers[2].Visible()) + assert.False(t, ar.dividers[1].Visible()) + + ac.Append(NewAccordionItem("foo3", NewLabel("foobar3"))) + assert.Equal(t, 3, len(ac.Items)) + assert.True(t, ar.headers[2].Visible()) + assert.True(t, ar.dividers[1].Visible()) +} From a67383a53d046f989b8f916c8920d406a411b8c4 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 18 Sep 2020 16:28:20 +0100 Subject: [PATCH 09/19] Remove deprecation warnings in accordion tests --- widget/accordion.go | 2 + widget/accordion_internal_test.go | 28 ++++----- widget/accordion_test.go | 98 +++++++++++++++---------------- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/widget/accordion.go b/widget/accordion.go index c27758a850..764a353c31 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -13,10 +13,12 @@ var _ fyne.Widget = (*Accordion)(nil) // AccordionContainer displays a list of AccordionItems. // Each item is represented by a button that reveals a detailed view when tapped. +// // Deprecated: This has been renamed to Accordion type AccordionContainer = Accordion // NewAccordionContainer creates a new accordion widget. +// // Deprecated: Use NewAccordion instead func NewAccordionContainer(items ...*AccordionItem) *AccordionContainer { a := &Accordion{ diff --git a/widget/accordion_internal_test.go b/widget/accordion_internal_test.go index 40a0fa6bc0..116fc70bad 100644 --- a/widget/accordion_internal_test.go +++ b/widget/accordion_internal_test.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAccordionContainer_Toggle(t *testing.T) { +func TestAccordion_Toggle(t *testing.T) { ai := NewAccordionItem("foo", NewLabel("foobar")) - ac := NewAccordionContainer(ai) + ac := NewAccordion(ai) ar := test.WidgetRenderer(ac).(*accordionRenderer) aih := ar.headers[0] assert.False(t, ai.Open) @@ -23,14 +23,14 @@ func TestAccordionContainer_Toggle(t *testing.T) { assert.False(t, ai.Open) } -func TestAccordionContainerRenderer_Layout(t *testing.T) { +func TestAccordionRenderer_Layout(t *testing.T) { ai0 := NewAccordionItem("foo0", NewLabel("foobar0")) ai1 := NewAccordionItem("foo1", NewLabel("foobar1")) ai2 := NewAccordionItem("foo2", NewLabel("foobar2")) aid0 := ai0.Detail aid1 := ai1.Detail aid2 := ai2.Detail - ac := NewAccordionContainer() + ac := NewAccordion() ac.Append(ai0) ac.Append(ai1) ac.Append(ai2) @@ -140,9 +140,9 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { }) } -func TestAccordionContainerRenderer_MinSize(t *testing.T) { +func TestAccordionRenderer_MinSize(t *testing.T) { t.Run("Empty", func(t *testing.T) { - ac := NewAccordionContainer() + ac := NewAccordion() ar := test.WidgetRenderer(ac).(*accordionRenderer) min := ar.MinSize() assert.Equal(t, 0, min.Width) @@ -151,7 +151,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { t.Run("Single", func(t *testing.T) { ai := NewAccordionItem("foo", NewLabel("foobar")) t.Run("Open", func(t *testing.T) { - ac := NewAccordionContainer() + ac := NewAccordion() ac.Append(ai) ac.Open(0) ar := test.WidgetRenderer(ac).(*accordionRenderer) @@ -162,7 +162,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { assert.Equal(t, aih.Height+aid.Height+theme.Padding(), min.Height) }) t.Run("Closed", func(t *testing.T) { - ac := NewAccordionContainer() + ac := NewAccordion() ac.Append(ai) ac.Close(0) ar := test.WidgetRenderer(ac).(*accordionRenderer) @@ -177,7 +177,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { ai1 := NewAccordionItem("foo1", NewLabel("foobar1")) ai2 := NewAccordionItem("foo2", NewLabel("foobar2")) t.Run("One_Open", func(t *testing.T) { - ac := NewAccordionContainer() + ac := NewAccordion() ac.Append(ai0) ac.Append(ai1) ac.Append(ai2) @@ -204,7 +204,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { assert.Equal(t, height, min.Height) }) t.Run("All_Open", func(t *testing.T) { - ac := &AccordionContainer{ + ac := &Accordion{ MultiOpen: true, } ac.Append(ai0) @@ -237,7 +237,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { assert.Equal(t, height, min.Height) }) t.Run("One_Closed", func(t *testing.T) { - ac := &AccordionContainer{ + ac := &Accordion{ MultiOpen: true, } ac.Append(ai0) @@ -269,7 +269,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { assert.Equal(t, height, min.Height) }) t.Run("All_Closed", func(t *testing.T) { - ac := NewAccordionContainer() + ac := NewAccordion() ac.Append(ai0) ac.Append(ai1) ac.Append(ai2) @@ -293,8 +293,8 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { }) } -func TestAccordionContainerRenderer_AddRemove(t *testing.T) { - ac := NewAccordionContainer() +func TestAccordionRenderer_AddRemove(t *testing.T) { + ac := NewAccordion() ar := test.WidgetRenderer(ac).(*accordionRenderer) ac.Append(NewAccordionItem("foo0", NewLabel("foobar0"))) ac.Append(NewAccordionItem("foo1", NewLabel("foobar1"))) diff --git a/widget/accordion_test.go b/widget/accordion_test.go index cb4e23f626..9e05846359 100644 --- a/widget/accordion_test.go +++ b/widget/accordion_test.go @@ -12,27 +12,27 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAccordionContainer(t *testing.T) { +func TestAccordion(t *testing.T) { ai := widget.NewAccordionItem("foo", widget.NewLabel("foobar")) t.Run("Initializer", func(t *testing.T) { - ac := &widget.AccordionContainer{Items: []*widget.AccordionItem{ai}} + ac := &widget.Accordion{Items: []*widget.AccordionItem{ai}} assert.Equal(t, 1, len(ac.Items)) }) t.Run("Constructor", func(t *testing.T) { - ac := widget.NewAccordionContainer(ai) + ac := widget.NewAccordion(ai) assert.Equal(t, 1, len(ac.Items)) }) } -func TestAccordionContainer_Append(t *testing.T) { - ac := widget.NewAccordionContainer() +func TestAccordion_Append(t *testing.T) { + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo", widget.NewLabel("foobar"))) assert.Equal(t, 1, len(ac.Items)) } -func TestAccordionContainer_Close(t *testing.T) { +func TestAccordion_Close(t *testing.T) { t.Run("Exists", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(&widget.AccordionItem{ Title: "foo", Detail: widget.NewLabel("foobar"), @@ -43,7 +43,7 @@ func TestAccordionContainer_Close(t *testing.T) { assert.False(t, ac.Items[0].Detail.Visible()) }) t.Run("BelowBounds", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(&widget.AccordionItem{ Title: "foo", Detail: widget.NewLabel("foobar"), @@ -53,7 +53,7 @@ func TestAccordionContainer_Close(t *testing.T) { assert.True(t, ac.Items[0].Open) }) t.Run("AboveBounds", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(&widget.AccordionItem{ Title: "foo", Detail: widget.NewLabel("foobar"), @@ -64,8 +64,8 @@ func TestAccordionContainer_Close(t *testing.T) { }) } -func TestAccordionContainer_CloseAll(t *testing.T) { - ac := widget.NewAccordionContainer() +func TestAccordion_CloseAll(t *testing.T) { + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo0", widget.NewLabel("foobar0"))) ac.Append(widget.NewAccordionItem("foo1", widget.NewLabel("foobar1"))) ac.Append(widget.NewAccordionItem("foo2", widget.NewLabel("foobar2"))) @@ -76,7 +76,7 @@ func TestAccordionContainer_CloseAll(t *testing.T) { assert.False(t, ac.Items[2].Open) } -func TestAccordionContainer_Layout(t *testing.T) { +func TestAccordion_Layout(t *testing.T) { test.NewApp() test.ApplyTheme(t, theme.LightTheme()) @@ -87,7 +87,7 @@ func TestAccordionContainer_Layout(t *testing.T) { }{ "single_open_one_item": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, @@ -95,7 +95,7 @@ func TestAccordionContainer_Layout(t *testing.T) { }, "single_open_one_item_opened": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, @@ -104,11 +104,11 @@ func TestAccordionContainer_Layout(t *testing.T) { }, "single_open_multiple_items": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -116,11 +116,11 @@ func TestAccordionContainer_Layout(t *testing.T) { }, "single_open_multiple_items_opened": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -130,7 +130,7 @@ func TestAccordionContainer_Layout(t *testing.T) { "multiple_open_one_item": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, @@ -139,7 +139,7 @@ func TestAccordionContainer_Layout(t *testing.T) { "multiple_open_one_item_opened": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, @@ -149,11 +149,11 @@ func TestAccordionContainer_Layout(t *testing.T) { "multiple_open_multiple_items": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -162,11 +162,11 @@ func TestAccordionContainer_Layout(t *testing.T) { "multiple_open_multiple_items_opened": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -175,7 +175,7 @@ func TestAccordionContainer_Layout(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - accordion := &widget.AccordionContainer{ + accordion := &widget.Accordion{ MultiOpen: tt.multiOpen, } for _, ai := range tt.items { @@ -195,7 +195,7 @@ func TestAccordionContainer_Layout(t *testing.T) { } } -func TestAccordionContainer_MinSize(t *testing.T) { +func TestAccordion_MinSize(t *testing.T) { minSizeA := fyne.MeasureText("A", theme.TextSize(), fyne.TextStyle{}) minSizeA.Width += theme.IconInlineSize() + theme.Padding()*5 minSizeA.Height = fyne.Max(minSizeA.Height, theme.IconInlineSize()) + theme.Padding()*2 @@ -223,7 +223,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }{ "single_open_one_item": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, @@ -232,7 +232,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, "single_open_one_item_opened": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, @@ -242,11 +242,11 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, "single_open_multiple_items": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -255,11 +255,11 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, "single_open_multiple_items_opened": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -270,7 +270,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { "multiple_open_one_item": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, @@ -280,7 +280,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { "multiple_open_one_item_opened": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, @@ -291,11 +291,11 @@ func TestAccordionContainer_MinSize(t *testing.T) { "multiple_open_multiple_items": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -305,11 +305,11 @@ func TestAccordionContainer_MinSize(t *testing.T) { "multiple_open_multiple_items_opened": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, @@ -319,7 +319,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - accordion := &widget.AccordionContainer{ + accordion := &widget.Accordion{ MultiOpen: tt.multiOpen, } for _, ai := range tt.items { @@ -334,9 +334,9 @@ func TestAccordionContainer_MinSize(t *testing.T) { } } -func TestAccordionContainer_Open(t *testing.T) { +func TestAccordion_Open(t *testing.T) { t.Run("Exists", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo0", widget.NewLabel("foobar0"))) ac.Append(widget.NewAccordionItem("foo1", widget.NewLabel("foobar1"))) ac.Append(widget.NewAccordionItem("foo2", widget.NewLabel("foobar2"))) @@ -363,14 +363,14 @@ func TestAccordionContainer_Open(t *testing.T) { assert.True(t, ac.Items[2].Open) }) t.Run("BelowBounds", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo", widget.NewLabel("foobar"))) assert.False(t, ac.Items[0].Open) ac.Open(-1) assert.False(t, ac.Items[0].Open) }) t.Run("AboveBounds", func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo", widget.NewLabel("foobar"))) assert.False(t, ac.Items[0].Open) ac.Open(1) @@ -378,8 +378,8 @@ func TestAccordionContainer_Open(t *testing.T) { }) } -func TestAccordionContainer_OpenAll(t *testing.T) { - ac := widget.NewAccordionContainer() +func TestAccordion_OpenAll(t *testing.T) { + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo0", widget.NewLabel("foobar0"))) ac.Append(widget.NewAccordionItem("foo1", widget.NewLabel("foobar1"))) ac.Append(widget.NewAccordionItem("foo2", widget.NewLabel("foobar2"))) @@ -398,14 +398,14 @@ func TestAccordionContainer_OpenAll(t *testing.T) { assert.True(t, ac.Items[2].Open) } -func TestAccordionContainer_Remove(t *testing.T) { +func TestAccordion_Remove(t *testing.T) { ai := widget.NewAccordionItem("foo", widget.NewLabel("foobar")) - ac := widget.NewAccordionContainer(ai) + ac := widget.NewAccordion(ai) ac.Remove(ai) assert.Equal(t, 0, len(ac.Items)) } -func TestAccordionContainer_RemoveIndex(t *testing.T) { +func TestAccordion_RemoveIndex(t *testing.T) { for name, tt := range map[string]struct { index int length int @@ -415,7 +415,7 @@ func TestAccordionContainer_RemoveIndex(t *testing.T) { "AboveBounds": {index: 1, length: 1}, } { t.Run(name, func(t *testing.T) { - ac := widget.NewAccordionContainer() + ac := widget.NewAccordion() ac.Append(widget.NewAccordionItem("foo", widget.NewLabel("foobar"))) ac.RemoveIndex(tt.index) assert.Equal(t, tt.length, len(ac.Items)) From 1dbe0f181a797c7d5d8d479cb8c70fec54c8470c Mon Sep 17 00:00:00 2001 From: Stephen M Houston Date: Fri, 18 Sep 2020 15:45:37 -0500 Subject: [PATCH 10/19] Fix an error with list indexes being out of range (#1323) * Fix an error with list indexes being out of range * Make test case match the issue's provided replication case. --- widget/list.go | 2 +- widget/list_test.go | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/widget/list.go b/widget/list.go index bf9cdbf9c1..931412c728 100644 --- a/widget/list.go +++ b/widget/list.go @@ -122,7 +122,7 @@ func (l *listRenderer) Layout(size fyne.Size) { l.children = l.children[:len(l.children)-1] } } - if l.visibleItemCount > 0 && l.list.Length() < l.visibleItemCount && len(l.children) > 0 { + if l.visibleItemCount > 0 && l.list.Length() < l.visibleItemCount && len(l.children) > l.visibleItemCount-1 { for i := l.visibleItemCount; i >= l.list.Length(); i-- { l.itemPool.Release(l.children[len(l.children)-1]) l.children = l.children[:len(l.children)-1] diff --git a/widget/list_test.go b/widget/list_test.go index f9b478d7fe..373860221b 100644 --- a/widget/list_test.go +++ b/widget/list_test.go @@ -15,7 +15,7 @@ import ( ) func TestNewList(t *testing.T) { - list := createList() + list := createList(1000) template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) firstItemIndex := test.WidgetRenderer(list).(*listRenderer).firstItemIndex @@ -30,7 +30,7 @@ func TestNewList(t *testing.T) { } func TestList_Resize(t *testing.T) { - list := createList() + list := createList(1000) w := test.NewWindow(list) w.Resize(fyne.NewSize(200, 1000)) template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) @@ -59,7 +59,7 @@ func TestList_Resize(t *testing.T) { } func TestList_OffsetChange(t *testing.T) { - list := createList() + list := createList(1000) w := test.NewWindow(list) w.Resize(fyne.NewSize(200, 1000)) template := newListItem(fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")), nil) @@ -91,7 +91,7 @@ func TestList_OffsetChange(t *testing.T) { } func TestList_Hover(t *testing.T) { - list := createList() + list := createList(1000) children := test.WidgetRenderer(list).(*listRenderer).children for i := 0; i < 2; i++ { @@ -104,7 +104,7 @@ func TestList_Hover(t *testing.T) { } func TestList_Selection(t *testing.T) { - list := createList() + list := createList(1000) children := test.WidgetRenderer(list).(*listRenderer).children assert.Equal(t, children[0].(*listItem).statusIndicator.FillColor, theme.BackgroundColor()) @@ -118,7 +118,7 @@ func TestList_Selection(t *testing.T) { } func TestList_DataChange(t *testing.T) { - list := createList() + list := createList(1000) w := test.NewWindow(list) w.Resize(fyne.NewSize(200, 1000)) children := test.WidgetRenderer(list).(*listRenderer).children @@ -133,7 +133,7 @@ func TestList_DataChange(t *testing.T) { } func TestList_ThemeChange(t *testing.T) { - list := createList() + list := createList(1000) w := test.NewWindow(list) w.Resize(fyne.NewSize(200, 1000)) @@ -146,9 +146,30 @@ func TestList_ThemeChange(t *testing.T) { }) } -func createList() *List { +func TestList_SmallList(t *testing.T) { var data []string - for i := 0; i < 1000; i++ { + data = append(data, "Test Item 0") + + list := NewList( + func() int { + return len(data) + }, + func() fyne.CanvasObject { + return fyne.NewContainerWithLayout(layout.NewHBoxLayout(), NewIcon(theme.DocumentIcon()), NewLabel("Template Object")) + }, + func(index int, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[1].(*Label).SetText(data[index]) + }, + ) + list.Resize(fyne.NewSize(200, 1000)) + + data = append(data, "Test Item 1") + list.Refresh() +} + +func createList(items int) *List { + var data []string + for i := 0; i < items; i++ { data = append(data, fmt.Sprintf("Test Item %d", i)) } From d148fe77216ec9fd32326d58e420395ddb40c781 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 18 Sep 2020 22:42:04 +0100 Subject: [PATCH 11/19] Add padding to top and bottom to match other divided content widgets --- widget/accordion.go | 10 +++--- widget/accordion_internal_test.go | 56 ++++++++++++++++++------------- widget/accordion_test.go | 16 ++++----- 3 files changed, 45 insertions(+), 37 deletions(-) diff --git a/widget/accordion.go b/widget/accordion.go index 5f807f8f28..2cda9a320a 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -142,8 +142,9 @@ func (r *accordionRenderer) Layout(size fyne.Size) { div := r.dividers[i-1] div.Move(fyne.NewPos(x, y)) div.Resize(fyne.NewSize(size.Width, accordionDividerHeight)) - y += accordionDividerHeight + theme.Padding() + y += accordionDividerHeight } + y += theme.Padding() h := r.headers[i] h.Move(fyne.NewPos(x, y)) @@ -159,16 +160,15 @@ func (r *accordionRenderer) Layout(size fyne.Size) { y += min } - if i < len(r.container.Items)-1 { - y += theme.Padding() - } + y += theme.Padding() } } func (r *accordionRenderer) MinSize() (size fyne.Size) { for i, ai := range r.container.Items { + size.Height += theme.Padding()*2 if i != 0 { - size.Height += theme.Padding()*2 + accordionDividerHeight + size.Height += accordionDividerHeight } min := r.headers[i].MinSize() size.Width = fyne.Max(size.Width, min.Width) diff --git a/widget/accordion_internal_test.go b/widget/accordion_internal_test.go index e84c827250..204e44fc3f 100644 --- a/widget/accordion_internal_test.go +++ b/widget/accordion_internal_test.go @@ -45,15 +45,15 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { min := ac.MinSize() ar.Layout(min) assert.Equal(t, 0, aih0.Position().X) - assert.Equal(t, 0, aih0.Position().Y) + assert.Equal(t, theme.Padding(), aih0.Position().Y) assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+theme.Padding()*2+1, aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+theme.Padding()*3+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+2, aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+5*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) }) @@ -65,23 +65,23 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { min := ac.MinSize() ar.Layout(min) assert.Equal(t, 0, aih0.Position().X) - assert.Equal(t, 0, aih0.Position().Y) + assert.Equal(t, theme.Padding(), aih0.Position().Y) assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+2*theme.Padding()+1, aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+3*theme.Padding()+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+5*theme.Padding()+2, aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+6*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+3*theme.Padding()+1, aid1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+1, aid1.Position().Y) assert.Equal(t, min.Width, aid1.Size().Width) assert.Equal(t, aid1.MinSize().Height, aid1.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+6*theme.Padding()+2, aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+7*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -92,19 +92,19 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { min := ac.MinSize() ar.Layout(min) assert.Equal(t, 0, aih0.Position().X) - assert.Equal(t, 0, aih0.Position().Y) + assert.Equal(t, theme.Padding(), aih0.Position().Y) assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+1*theme.Padding()*2+1, aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+3*theme.Padding()+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+2, aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+5*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aih2.MinSize().Height+5*theme.Padding()+2, aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aih1.MinSize().Height+aih2.MinSize().Height+6*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -114,27 +114,27 @@ func TestAccordionContainerRenderer_Layout(t *testing.T) { min := ac.MinSize() ar.Layout(min) assert.Equal(t, 0, aih0.Position().X) - assert.Equal(t, 0, aih0.Position().Y) + assert.Equal(t, theme.Padding(), aih0.Position().Y) assert.Equal(t, min.Width, aih0.Size().Width) assert.Equal(t, aih0.MinSize().Height, aih0.Size().Height) assert.Equal(t, 0, aih1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+3*theme.Padding()+1, aih1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+4*theme.Padding()+1, aih1.Position().Y) assert.Equal(t, min.Width, aih1.Size().Width) assert.Equal(t, aih1.MinSize().Height, aih1.Size().Height) assert.Equal(t, 0, aih2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+6*theme.Padding()+2, aih2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+7*theme.Padding()+2, aih2.Position().Y) assert.Equal(t, min.Width, aih2.Size().Width) assert.Equal(t, aih2.MinSize().Height, aih2.Size().Height) assert.Equal(t, 0, aid0.Position().X) - assert.Equal(t, aih0.MinSize().Height+theme.Padding(), aid0.Position().Y) + assert.Equal(t, aih0.MinSize().Height+theme.Padding()*2, aid0.Position().Y) assert.Equal(t, min.Width, aid0.Size().Width) assert.Equal(t, aid0.MinSize().Height, aid0.Size().Height) assert.Equal(t, 0, aid1.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+4*theme.Padding()+1, aid1.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+5*theme.Padding()+1, aid1.Position().Y) assert.Equal(t, min.Width, aid1.Size().Width) assert.Equal(t, aid1.MinSize().Height, aid1.Size().Height) assert.Equal(t, 0, aid2.Position().X) - assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+7*theme.Padding()+2, aid2.Position().Y) + assert.Equal(t, aih0.MinSize().Height+aid0.MinSize().Height+aih1.MinSize().Height+aid1.MinSize().Height+aih2.MinSize().Height+8*theme.Padding()+2, aid2.Position().Y) assert.Equal(t, min.Width, aid2.Size().Width) assert.Equal(t, aid2.MinSize().Height, aid2.Size().Height) }) @@ -159,7 +159,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { aih := ar.headers[0].MinSize() aid := ai.Detail.MinSize() assert.Equal(t, fyne.Max(aih.Width, aid.Width), min.Width) - assert.Equal(t, aih.Height+aid.Height+theme.Padding(), min.Height) + assert.Equal(t, aih.Height+aid.Height+theme.Padding()*3, min.Height) }) t.Run("Closed", func(t *testing.T) { ac := NewAccordionContainer() @@ -169,7 +169,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { min := ar.MinSize() aih := ar.headers[0].MinSize() assert.Equal(t, aih.Width, min.Width) - assert.Equal(t, aih.Height, min.Height) + assert.Equal(t, aih.Height+theme.Padding()*2, min.Height) }) }) t.Run("Multiple", func(t *testing.T) { @@ -194,13 +194,15 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { width = fyne.Max(width, aih1.Width) width = fyne.Max(width, aih2.Width) assert.Equal(t, width, min.Width) - height := aih0.Height + height := theme.Padding() + height += aih0.Height height += theme.Padding() height += aid0.Height height += theme.Padding()*2 + 1 height += aih1.Height height += theme.Padding()*2 + 1 height += aih2.Height + height += theme.Padding() assert.Equal(t, height, min.Height) }) t.Run("All_Open", func(t *testing.T) { @@ -223,7 +225,8 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { width = fyne.Max(width, fyne.Max(aih1.Width, aid1.Width)) width = fyne.Max(width, fyne.Max(aih2.Width, aid2.Width)) assert.Equal(t, width, min.Width) - height := aih0.Height + height := theme.Padding() + height += aih0.Height height += theme.Padding() height += aid0.Height height += theme.Padding()*2 + 1 @@ -234,6 +237,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { height += aih2.Height height += theme.Padding() height += aid2.Height + height += theme.Padding() assert.Equal(t, height, min.Height) }) t.Run("One_Closed", func(t *testing.T) { @@ -257,7 +261,8 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { width = fyne.Max(width, fyne.Max(aih1.Width, aid1.Width)) width = fyne.Max(width, aih2.Width) assert.Equal(t, width, min.Width) - height := aih0.Height + height := theme.Padding() + height += aih0.Height height += theme.Padding() height += aid0.Height height += theme.Padding()*2 + 1 @@ -266,6 +271,7 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { height += aid1.Height height += theme.Padding()*2 + 1 height += aih2.Height + height += theme.Padding() assert.Equal(t, height, min.Height) }) t.Run("All_Closed", func(t *testing.T) { @@ -283,11 +289,13 @@ func TestAccordionContainerRenderer_MinSize(t *testing.T) { width = fyne.Max(width, aih1.Width) width = fyne.Max(width, aih2.Width) assert.Equal(t, width, min.Width) - height := aih0.Height + height := theme.Padding() + height += aih0.Height height += theme.Padding()*2 + 1 height += aih1.Height height += theme.Padding()*2 + 1 height += aih2.Height + height += theme.Padding() assert.Equal(t, height, min.Height) }) }) diff --git a/widget/accordion_test.go b/widget/accordion_test.go index cb4e23f626..0cc02a3607 100644 --- a/widget/accordion_test.go +++ b/widget/accordion_test.go @@ -228,7 +228,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("111111"), }, }, - want: fyne.NewSize(minWidthA1, minSizeA.Height), + want: fyne.NewSize(minWidthA1, minSizeA.Height+theme.Padding()*2), }, "single_open_one_item_opened": { items: []*widget.AccordionItem{ @@ -238,7 +238,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0}, - want: fyne.NewSize(minWidthA1, minHeightA1), + want: fyne.NewSize(minWidthA1, minHeightA1+theme.Padding()*2), }, "single_open_multiple_items": { items: []*widget.AccordionItem{ @@ -251,7 +251,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("2222222222"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*2+1), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*4+1), }, "single_open_multiple_items_opened": { items: []*widget.AccordionItem{ @@ -265,7 +265,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0, 1}, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize2.Height+theme.Padding()*3+1), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize2.Height+theme.Padding()*5+1), }, "multiple_open_one_item": { multiOpen: true, @@ -275,7 +275,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("111111"), }, }, - want: fyne.NewSize(minWidthA1, minSizeA.Height), + want: fyne.NewSize(minWidthA1, minSizeA.Height+theme.Padding()*2), }, "multiple_open_one_item_opened": { multiOpen: true, @@ -286,7 +286,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0}, - want: fyne.NewSize(minWidthA1, minHeightA1), + want: fyne.NewSize(minWidthA1, minHeightA1+theme.Padding()*2), }, "multiple_open_multiple_items": { multiOpen: true, @@ -300,7 +300,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { Detail: widget.NewLabel("2222222222"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*2+1), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*4+1), }, "multiple_open_multiple_items_opened": { multiOpen: true, @@ -315,7 +315,7 @@ func TestAccordionContainer_MinSize(t *testing.T) { }, }, opened: []int{0, 1}, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize1.Height+minSize2.Height+theme.Padding()*4+1), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+minSize1.Height+minSize2.Height+theme.Padding()*6+1), }, } { t.Run(name, func(t *testing.T) { From 51ae8d38274c4232097b94c072f0475fab4f3f9d Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 18 Sep 2020 22:47:12 +0100 Subject: [PATCH 12/19] Apply shadow colour to dialog divider --- widget/accordion.go | 6 ++++- widget/accordion_test.go | 24 ++++++++++++++++++++ widget/testdata/accordion_theme_changed.png | Bin 0 -> 1615 bytes widget/testdata/accordion_theme_initial.png | Bin 0 -> 1506 bytes 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 widget/testdata/accordion_theme_changed.png create mode 100644 widget/testdata/accordion_theme_initial.png diff --git a/widget/accordion.go b/widget/accordion.go index 2cda9a320a..4f78331d15 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -166,7 +166,7 @@ func (r *accordionRenderer) Layout(size fyne.Size) { func (r *accordionRenderer) MinSize() (size fyne.Size) { for i, ai := range r.container.Items { - size.Height += theme.Padding()*2 + size.Height += theme.Padding() * 2 if i != 0 { size.Height += accordionDividerHeight } @@ -184,6 +184,10 @@ func (r *accordionRenderer) MinSize() (size fyne.Size) { } func (r *accordionRenderer) Refresh() { + for _, d := range r.dividers { + d.FillColor = theme.ShadowColor() + } + r.updateObjects() r.Layout(r.container.Size()) canvas.Refresh(r.container) diff --git a/widget/accordion_test.go b/widget/accordion_test.go index 0cc02a3607..2f14925867 100644 --- a/widget/accordion_test.go +++ b/widget/accordion_test.go @@ -2,6 +2,7 @@ package widget_test import ( "testing" + "time" "fyne.io/fyne" "fyne.io/fyne/layout" @@ -30,6 +31,29 @@ func TestAccordionContainer_Append(t *testing.T) { assert.Equal(t, 1, len(ac.Items)) } +func TestAccordionContainer_ChangeTheme(t *testing.T) { + app := test.NewApp() + defer test.NewApp() + app.Settings().SetTheme(theme.LightTheme()) + + ac := widget.NewAccordionContainer() + ac.Append(widget.NewAccordionItem("foo0", widget.NewLabel("foobar0"))) + ac.Append(widget.NewAccordionItem("foo1", widget.NewLabel("foobar1"))) + + w := test.NewWindow(ac) + defer w.Close() + w.Resize(ac.MinSize().Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2))) + + test.AssertImageMatches(t, "accordion_theme_initial.png", w.Canvas().Capture()) + + test.WithTestTheme(t, func() { + ac.Resize(ac.MinSize()) + ac.Refresh() + time.Sleep(100 * time.Millisecond) + test.AssertImageMatches(t, "accordion_theme_changed.png", w.Canvas().Capture()) + }) +} + func TestAccordionContainer_Close(t *testing.T) { t.Run("Exists", func(t *testing.T) { ac := widget.NewAccordionContainer() diff --git a/widget/testdata/accordion_theme_changed.png b/widget/testdata/accordion_theme_changed.png new file mode 100644 index 0000000000000000000000000000000000000000..221fac6f6d44cad5f814260df634d1dccd45a9b7 GIT binary patch literal 1615 zcmaKtc`zGT9LH^IU3J&?vO!l%GtPC4y6dVVf~Km}5`@w^;wW_l<0!Qn$5!czGZk?Z z8!Dlwk`j`XP+?GKTDNE_l*F;wPG@Jk)7?Mbd-LAB_vU>+^ZR~ZhLeM}n8+CsK0ZD% zTN_IVZ}jH1bfF`>J|m_qkdIH`gsr8Sb9B*K5jfyD_!LPwtPcMZzZKHwdz?!IP$Yq$ zb*x9u=sw#ej+R-5j0U<(z2;rXvYzXJ^-AOH16a{!M30YX5GU%pB zrDbXUy#(`hF=FUR=n*6TXVaH+y_hUfVn9ep0(Gq`%)@gv3z52w9=nVX#n_(#6sXo*0>$QcIB#6KlrfIfxfhPo|cu8X6qc%P^Sp zG9v4fgu$+^u?A?Vy;9vwYdm*5RrvUtVGAvHY;~W-NcVGNMYl^bOc6Uy?LX>=SK#s6 zT`7W)DSR+0UXY+)sB;SfgFUh5@E&xy=R-^r3m@6|BySwcW=^$fc(%7hu8z(T)3J`F zCA!`3`X5@9J6T(kHWvqBI9Ct|q##MNuC1x@{otyqq^7H@J3Krz+hoTaNKJh`bBD#y>Lb8s(4DTW;eKvz<8600*wPncKKlCm zW1XQLvASXNmdn$lqe@8lbh~~$huxET68z?wYG8D9^bp$0t~y0f!tT-qnPFx6AiQ%1 zuB~j%Y*q!w9FrC$=7gCbcFJT|S5_);wY4C*db#XH&erNM4}lA2?V8bObCP|r!8MWn{ry&K6!ps7no#UECpHff%h1!&(IG$P z5wdG;Y=n@g#fH~`x<*DpZIhwPZ8qx0#>NoW=G%`#M)YPW1sdA0Nag%It%E?$}eeh{XXwHFLa)lwgjTw*k^=GzIR4 zY9R_0NuBS>18oqOzuX~2T$VH+3@)SO?lb0)o&%o*J1Ji1o%N&elj3zamMeYfjsJusPze+qe%=F zN)LW;mfDX{6mN{@MgfZq@75Ct1T=d74c2j>#Ajo=1M5(%C90^coMEi~i}k$$l8dKj z!d&+~Y9!BO4AaO^)D!bX*k){pf$h|LN4Am)s$EclI4=?`p%6kXz)ky{(X2D3|y5&)eyb@8^3y=X}p|KF{-fpHH5vvz?5TvJ?OSG7k3E z7r`3?9(TxZU}a(VCIG;mCl1!d%D$qn2FOHzKT2oVF?S|0P9Zevx`E&RcVQ*ascPsr|TMdYWVT0=Jsv@9nhL;2q5)wQ;J%KlG-y+S;&C%#q2;P_8^yrc34aJJ$YH#M_ z=?TK$>XwnSgh`UPA!n4lORhA8%K+sj3B1Mi==aYlG#WNNJv}%$I40(KQ&Zyh_cc{+ zZ%+>*plv+7p*y&)zu%468H6k~L8F&?yKz7uN?BrRevCDeTT!LHfJ;kLP0Am*%tk?X$BwK)E@g zg~4EOIJ;jzb^=pz&48xj&5sP_^~-K<62K%UBS4kyVfCe^c1M204P?91wT}_NQMA#Q z&jT!+oSb~^4|S4jy?uOqN-lFunSoYotfGD5c@lR$^LtpJ{JA5=Zt6^o_eZOJ;V6F6k)8y9dU?#c^gcFIb)yZb>|AA}-njGp_#KRPuP zEjvHnd<};?oqE;jz`Bx-gS;e#N~PAop<1^hD5!-6%Mi@+~a9Fj#yjle3j` zLtF&?HEUaw(*4E9UGj~{HdSqH{$@3fSN{*+&zFtJX_ikH4^l8T-vN|4g2cJKIGBP4sQohg^KCrP3BQQ(QaarOF zs6#%rk;&A`PMjY#Ff<&RoFu_U|BkI1{_Lp`mYl4l<>BXtMj~Ivj@LMq!7NYTkzW6{ zv;89!X>4GSvkwNwJ7SkP&-dsQ3Wb4Z?mE&M=lOT^0;bi4BGFRm(2!R)j2oLz1^=;T z_A`HWzLiK&X$Mg|8>7kW9AC4qEqL`i$Za{Zns>t#{*ns4WD9_PxA#}s(Y>&~VUobv zioWe1>ntX7qBY5dUhwMr^iyL&xp7Ft2nK^uH%*4g5u@1RErT=vyu5iG0y}PURrx)cEQaTQ700$dq>t~jM GS^ohZ(dn`P literal 0 HcmV?d00001 From 1a163ac6548ec261324e4a6ccbc15c1772aa0681 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Mon, 21 Sep 2020 05:12:10 -0400 Subject: [PATCH 13/19] implement RemoveShortcut (#1290) implement RemoveShortcut --- canvas.go | 1 + internal/driver/glfw/canvas.go | 4 ++++ internal/driver/gomobile/canvas.go | 4 ++++ shortcut.go | 11 +++++++++++ shortcut_test.go | 16 ++++++++++++++++ 5 files changed, 36 insertions(+) diff --git a/canvas.go b/canvas.go index 15ceb43167..cd0344cb33 100644 --- a/canvas.go +++ b/canvas.go @@ -42,6 +42,7 @@ type Canvas interface { OnTypedKey() func(*KeyEvent) SetOnTypedKey(func(*KeyEvent)) AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut)) + RemoveShortcut(shortcut Shortcut) Capture() image.Image diff --git a/internal/driver/glfw/canvas.go b/internal/driver/glfw/canvas.go index 62a8f5df6d..d913f5d4aa 100644 --- a/internal/driver/glfw/canvas.go +++ b/internal/driver/glfw/canvas.go @@ -275,6 +275,10 @@ func (c *glCanvas) canvasSize(contentSize fyne.Size) fyne.Size { return canvasSize } +func (c *glCanvas) RemoveShortcut(shortcut fyne.Shortcut) { + c.shortcut.RemoveShortcut(shortcut) +} + func (c *glCanvas) contentPos() fyne.Position { contentPos := fyne.NewPos(0, c.menuHeight()) if c.Padded() { diff --git a/internal/driver/gomobile/canvas.go b/internal/driver/gomobile/canvas.go index 5c15e7e7b6..039486cfc8 100644 --- a/internal/driver/gomobile/canvas.go +++ b/internal/driver/gomobile/canvas.go @@ -202,6 +202,10 @@ func (c *mobileCanvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut c.shortcut.AddShortcut(shortcut, handler) } +func (c *mobileCanvas) RemoveShortcut(shortcut fyne.Shortcut) { + c.shortcut.RemoveShortcut(shortcut) +} + func (c *mobileCanvas) Capture() image.Image { return c.painter.Capture(c) } diff --git a/shortcut.go b/shortcut.go index 38e8a983d5..43620b605a 100644 --- a/shortcut.go +++ b/shortcut.go @@ -30,6 +30,17 @@ func (sh *ShortcutHandler) AddShortcut(shortcut Shortcut, handler func(shortcut sh.entry[shortcut.ShortcutName()] = handler } +// RemoveShortcut removes a registered shortcut +func (sh *ShortcutHandler) RemoveShortcut(shortcut Shortcut) { + sh.mu.Lock() + defer sh.mu.Unlock() + if sh.entry == nil { + return + } + + delete(sh.entry, shortcut.ShortcutName()) +} + // Shortcut is the interface used to describe a shortcut action type Shortcut interface { ShortcutName() string diff --git a/shortcut_test.go b/shortcut_test.go index a1e173ecb2..7f4e827ec7 100644 --- a/shortcut_test.go +++ b/shortcut_test.go @@ -15,6 +15,22 @@ func TestShortcutHandler_AddShortcut(t *testing.T) { assert.Equal(t, 2, len(handle.entry)) } +func TestShortcutHandler_RemoveShortcut(t *testing.T) { + handler := &ShortcutHandler{} + handler.AddShortcut(&ShortcutCopy{}, func(shortcut Shortcut) {}) + handler.AddShortcut(&ShortcutPaste{}, func(shortcut Shortcut) {}) + + assert.Equal(t, 2, len(handler.entry)) + + handler.RemoveShortcut(&ShortcutCopy{}) + + assert.Equal(t, 1, len(handler.entry)) + + handler.RemoveShortcut(&ShortcutPaste{}) + + assert.Equal(t, 0, len(handler.entry)) +} + func TestShortcutHandler_HandleShortcut(t *testing.T) { handle := &ShortcutHandler{} cutCalled, copyCalled, pasteCalled := false, false, false From 279fcf8edc157873679ce521b4a871a22d58a545 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Mon, 21 Sep 2020 17:10:30 +0100 Subject: [PATCH 14/19] reorder canvas tests and fix names --- container.go | 119 +++++++++++++++++++++++----------------------- container_test.go | 82 ++++++++++++++++---------------- 2 files changed, 100 insertions(+), 101 deletions(-) diff --git a/container.go b/container.go index 62e1c1de02..03d8e788ea 100644 --- a/container.go +++ b/container.go @@ -14,37 +14,42 @@ type Container struct { Objects []CanvasObject // The set of CanvasObjects this container holds } -func (c *Container) layout() { - if c.Layout != nil { - c.Layout.Layout(c.Objects, c.size) - return - } -} - -// Size returns the current size of this container. -func (c *Container) Size() Size { - return c.size +// NewContainer returns a new Container instance holding the specified CanvasObjects. +// +// Deprecated: Use NewContainerWithoutLayout to create a container that uses manual layout. +func NewContainer(objects ...CanvasObject) *Container { + return NewContainerWithoutLayout(objects...) } -// Resize sets a new size for the Container. -func (c *Container) Resize(size Size) { - if c.size == size { - return +// NewContainerWithoutLayout returns a new Container instance holding the specified CanvasObjects +// that are manually arranged. +func NewContainerWithoutLayout(objects ...CanvasObject) *Container { + ret := &Container{ + Objects: objects, } - c.size = size - c.layout() + ret.size = ret.MinSize() + ret.layout() + + return ret } -// Position gets the current position of this Container, relative to its parent. -func (c *Container) Position() Position { - return c.position +// NewContainerWithLayout returns a new Container instance holding the specified +// CanvasObjects which will be laid out according to the specified Layout. +func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container { + ret := &Container{ + Objects: objects, + Layout: layout, + } + + ret.size = layout.MinSize(objects) + ret.layout() + return ret } -// Move the container (and all its children) to a new position, relative to its parent. -func (c *Container) Move(pos Position) { - c.position = pos - c.layout() +// AddObject adds another CanvasObject to the set this Container holds. +func (c *Container) AddObject(o CanvasObject) { + c.Add(o) } // MinSize calculates the minimum size of a Container. @@ -62,9 +67,30 @@ func (c *Container) MinSize() Size { return minSize } -// Visible returns true if the container is currently visible, false otherwise. -func (c *Container) Visible() bool { - return !c.Hidden +// Move the container (and all its children) to a new position, relative to its parent. +func (c *Container) Move(pos Position) { + c.position = pos + c.layout() +} + +// Position gets the current position of this Container, relative to its parent. +func (c *Container) Position() Position { + return c.position +} + +// Resize sets a new size for the Container. +func (c *Container) Resize(size Size) { + if c.size == size { + return + } + + c.size = size + c.layout() +} + +// Size returns the current size of this container. +func (c *Container) Size() Size { + return c.size } // Show sets this container, and all its children, to be visible. @@ -85,12 +111,6 @@ func (c *Container) Hide() { c.Hidden = true } -// AddObject adds another CanvasObject to the set this Container holds. -func (c *Container) AddObject(o CanvasObject) { - c.Objects = append(c.Objects, o) - c.layout() -} - // Refresh causes this object to be redrawn in it's current state func (c *Container) Refresh() { c.layout() @@ -107,35 +127,14 @@ func (c *Container) Refresh() { o.Refresh(c) } -// NewContainer returns a new Container instance holding the specified CanvasObjects. -// -// Deprecated: Use NewContainerWithoutLayout to create a container that uses manual layout. -func NewContainer(objects ...CanvasObject) *Container { - return NewContainerWithoutLayout(objects...) -} - -// NewContainerWithoutLayout returns a new Container instance holding the specified CanvasObjects -// that are manually arranged. -func NewContainerWithoutLayout(objects ...CanvasObject) *Container { - ret := &Container{ - Objects: objects, - } - - ret.size = ret.MinSize() - ret.layout() - - return ret +// Visible returns true if the container is currently visible, false otherwise. +func (c *Container) Visible() bool { + return !c.Hidden } -// NewContainerWithLayout returns a new Container instance holding the specified -// CanvasObjects which will be laid out according to the specified Layout. -func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container { - ret := &Container{ - Objects: objects, - Layout: layout, +func (c *Container) layout() { + if c.Layout != nil { + c.Layout.Layout(c.Objects, c.size) + return } - - ret.size = layout.MinSize(objects) - ret.layout() - return ret } diff --git a/container_test.go b/container_test.go index 7472368e3c..70941cf223 100644 --- a/container_test.go +++ b/container_test.go @@ -6,7 +6,32 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMinSize(t *testing.T) { +func TestContainer_CustomLayout(t *testing.T) { + box := new(dummyObject) + layout := new(customLayout) + container := NewContainerWithLayout(layout, box) + + size := layout.MinSize(container.Objects) + assert.Equal(t, size, container.MinSize()) + assert.Equal(t, size, container.Size()) + assert.Equal(t, size, box.Size()) + + container.AddObject(box) + assert.Equal(t, size, box.Size()) +} + +func TestContainer_Hide(t *testing.T) { + box := new(dummyObject) + container := NewContainerWithoutLayout(box) + + assert.True(t, container.Visible()) + assert.True(t, box.Visible()) + container.Hide() + assert.False(t, container.Visible()) + assert.True(t, box.Visible()) +} + +func TestContainer_MinSize(t *testing.T) { box := new(dummyObject) minSize := box.MinSize() @@ -17,7 +42,7 @@ func TestMinSize(t *testing.T) { assert.Equal(t, minSize, container.MinSize()) } -func TestMove(t *testing.T) { +func TestContainer_Move(t *testing.T) { box := new(dummyObject) container := NewContainerWithoutLayout(box) @@ -35,7 +60,7 @@ func TestMove(t *testing.T) { assert.Equal(t, pos, box.Position()) } -func TestNilLayout(t *testing.T) { +func TestContainer_NilLayout(t *testing.T) { box := new(dummyObject) boxSize := box.size container := NewContainerWithoutLayout(box) @@ -49,44 +74,6 @@ func TestNilLayout(t *testing.T) { assert.Equal(t, boxSize, box.Size()) } -type customLayout struct { -} - -func (c *customLayout) Layout(objs []CanvasObject, size Size) { - for _, child := range objs { - child.Resize(size) - } -} - -func (c *customLayout) MinSize(_ []CanvasObject) Size { - return NewSize(10, 10) -} - -func TestCustomLayout(t *testing.T) { - box := new(dummyObject) - layout := new(customLayout) - container := NewContainerWithLayout(layout, box) - - size := layout.MinSize(container.Objects) - assert.Equal(t, size, container.MinSize()) - assert.Equal(t, size, container.Size()) - assert.Equal(t, size, box.Size()) - - container.AddObject(box) - assert.Equal(t, size, box.Size()) -} - -func TestContainer_Hide(t *testing.T) { - box := new(dummyObject) - container := NewContainerWithoutLayout(box) - - assert.True(t, container.Visible()) - assert.True(t, box.Visible()) - container.Hide() - assert.False(t, container.Visible()) - assert.True(t, box.Visible()) -} - func TestContainer_Show(t *testing.T) { box := new(dummyObject) container := NewContainerWithoutLayout(box) @@ -100,6 +87,19 @@ func TestContainer_Show(t *testing.T) { assert.True(t, container.Visible()) } +type customLayout struct { +} + +func (c *customLayout) Layout(objs []CanvasObject, size Size) { + for _, child := range objs { + child.Resize(size) + } +} + +func (c *customLayout) MinSize(_ []CanvasObject) Size { + return NewSize(10, 10) +} + type dummyObject struct { size Size pos Position From e20c4ead018c371b271f31ac403a02531aa82960 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Mon, 21 Sep 2020 17:17:54 +0100 Subject: [PATCH 15/19] Add Container.Remove and move Container.AddObject to Container.Add --- container.go | 26 ++++++++++++++++++++++++++ container_test.go | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/container.go b/container.go index 03d8e788ea..8396a638be 100644 --- a/container.go +++ b/container.go @@ -47,7 +47,15 @@ func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container { return ret } +// Add appends the specified object to the items this container manages. +func (c *Container) Add(add CanvasObject) { + c.Objects = append(c.Objects, add) + c.layout() +} + // AddObject adds another CanvasObject to the set this Container holds. +// +// Deprecated: Use replacement Add() function func (c *Container) AddObject(o CanvasObject) { c.Add(o) } @@ -127,6 +135,24 @@ func (c *Container) Refresh() { o.Refresh(c) } +// Remove updates the contents of this container to no longer include the specified object. +func (c *Container) Remove(rem CanvasObject) { + if len(c.Objects) == 0 { + return + } + + for i, o := range c.Objects { + if o != rem { + continue + } + + copy(c.Objects[i:], c.Objects[i+1:]) + c.Objects[len(c.Objects)-1] = nil + c.Objects = c.Objects[:len(c.Objects)-1] + return + } +} + // Visible returns true if the container is currently visible, false otherwise. func (c *Container) Visible() bool { return !c.Hidden diff --git a/container_test.go b/container_test.go index 70941cf223..25a74cb125 100644 --- a/container_test.go +++ b/container_test.go @@ -6,6 +6,15 @@ import ( "github.com/stretchr/testify/assert" ) +func TestContainer_Add(t *testing.T) { + box := new(dummyObject) + container := NewContainerWithoutLayout() + assert.Equal(t, 0, len(container.Objects)) + + container.Add(box) + assert.Equal(t, 1, len(container.Objects)) +} + func TestContainer_CustomLayout(t *testing.T) { box := new(dummyObject) layout := new(customLayout) @@ -74,6 +83,15 @@ func TestContainer_NilLayout(t *testing.T) { assert.Equal(t, boxSize, box.Size()) } +func TestContainer_Remove(t *testing.T) { + box := new(dummyObject) + container := NewContainerWithoutLayout(box) + assert.Equal(t, 1, len(container.Objects)) + + container.Remove(box) + assert.Equal(t, 0, len(container.Objects)) +} + func TestContainer_Show(t *testing.T) { box := new(dummyObject) container := NewContainerWithoutLayout(box) From 00e3dcf17dd9fb5f10dcdf7e62dac08efd0ae211 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Mon, 21 Sep 2020 17:21:20 +0100 Subject: [PATCH 16/19] remove un-needed code from bad merge --- container_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/container_test.go b/container_test.go index 25a74cb125..cc5d0a5bd7 100644 --- a/container_test.go +++ b/container_test.go @@ -24,9 +24,6 @@ func TestContainer_CustomLayout(t *testing.T) { assert.Equal(t, size, container.MinSize()) assert.Equal(t, size, container.Size()) assert.Equal(t, size, box.Size()) - - container.AddObject(box) - assert.Equal(t, size, box.Size()) } func TestContainer_Hide(t *testing.T) { @@ -46,9 +43,6 @@ func TestContainer_MinSize(t *testing.T) { container := NewContainerWithoutLayout(box) assert.Equal(t, minSize, container.MinSize()) - - container.AddObject(box) - assert.Equal(t, minSize, container.MinSize()) } func TestContainer_Move(t *testing.T) { @@ -78,9 +72,6 @@ func TestContainer_NilLayout(t *testing.T) { container.Resize(size) assert.Equal(t, size, container.Size()) assert.Equal(t, boxSize, box.Size()) - - container.AddObject(box) - assert.Equal(t, boxSize, box.Size()) } func TestContainer_Remove(t *testing.T) { From adb1ef35976daf345c764b7f16a3be1882f21e65 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Mon, 21 Sep 2020 20:28:06 +0100 Subject: [PATCH 17/19] No need to layout a nil layout --- container.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/container.go b/container.go index 8396a638be..ffff0e23c8 100644 --- a/container.go +++ b/container.go @@ -29,8 +29,6 @@ func NewContainerWithoutLayout(objects ...CanvasObject) *Container { } ret.size = ret.MinSize() - ret.layout() - return ret } From 5ba08c1f4ed18d128a9d908f9547d5d6545a1d9b Mon Sep 17 00:00:00 2001 From: Stuart Scott Date: Mon, 21 Sep 2020 13:13:16 -0700 Subject: [PATCH 18/19] Ensure tab children are resized before being shown (#1331) --- widget/tabcontainer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/tabcontainer.go b/widget/tabcontainer.go index f079d76b82..e8b17fa6ab 100644 --- a/widget/tabcontainer.go +++ b/widget/tabcontainer.go @@ -301,6 +301,7 @@ func (r *tabContainerRenderer) Refresh() { } else { current := r.container.current if current >= 0 && current < len(r.objects) && !r.objects[current].Visible() { + r.Layout(r.container.Size()) for i, o := range r.objects { if i == current { o.Show() @@ -308,7 +309,6 @@ func (r *tabContainerRenderer) Refresh() { o.Hide() } } - r.Layout(r.container.Size()) } for i, button := range r.tabBar.Objects { if i == current { From 3207d863573e9a9922f8a00fd092ec57d56eab26 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Tue, 22 Sep 2020 16:54:00 +0100 Subject: [PATCH 19/19] new widget.TabContainer is now container.AppTabs As per #1310 --- cmd/fyne_demo/main.go | 2 +- cmd/fyne_demo/screens/container.go | 2 +- cmd/fyne_demo/screens/widget.go | 2 +- container/tabs.go | 11 ++++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index bb63e77e4b..be3743da2d 100644 --- a/cmd/fyne_demo/main.go +++ b/cmd/fyne_demo/main.go @@ -115,7 +115,7 @@ func main() { w.SetMainMenu(mainMenu) w.SetMaster() - tabs := container.NewTabs( + tabs := container.NewAppTabs( container.NewTabItemWithIcon("Welcome", theme.HomeIcon(), welcomeScreen(a)), container.NewTabItemWithIcon("Graphics", theme.DocumentCreateIcon(), screens.GraphicsScreen()), container.NewTabItemWithIcon("Widgets", theme.CheckButtonCheckedIcon(), screens.WidgetScreen()), diff --git a/cmd/fyne_demo/screens/container.go b/cmd/fyne_demo/screens/container.go index 8723053192..23f130fa02 100644 --- a/cmd/fyne_demo/screens/container.go +++ b/cmd/fyne_demo/screens/container.go @@ -14,7 +14,7 @@ import ( // ContainerScreen loads a tab panel for containers and layouts func ContainerScreen() fyne.CanvasObject { - return container.NewTabs( + return container.NewAppTabs( // TODO not best use of tabs here either container.NewTabItem("Accordion", makeAccordionTab()), container.NewTabItem("Card", makeCardTab()), container.NewTabItem("Split", makeSplitTab()), diff --git a/cmd/fyne_demo/screens/widget.go b/cmd/fyne_demo/screens/widget.go index f2c17838ee..c35e30a8da 100644 --- a/cmd/fyne_demo/screens/widget.go +++ b/cmd/fyne_demo/screens/widget.go @@ -330,7 +330,7 @@ func WidgetScreen() fyne.CanvasObject { ) progress := makeProgressTab() - tabs := container.NewTabs( + tabs := container.NewAppTabs( // TODO move to something better suited to this content container.NewTabItem("Buttons", makeButtonTab()), container.NewTabItem("Text", makeTextTab()), container.NewTabItem("Input", makeInputTab()), diff --git a/container/tabs.go b/container/tabs.go index c7d8513eb5..1ef4aa57cc 100644 --- a/container/tabs.go +++ b/container/tabs.go @@ -5,9 +5,10 @@ import ( "fyne.io/fyne/widget" ) -// Tabs container allows switching visible content from a list of TabItems. -// Each item is represented by a button at the top of the widget. -type Tabs = widget.TabContainer +// AppTabs container is used to split your application into various different areas identified by tabs. +// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem. +// Each item is represented by a button at the edge of the container. +type AppTabs = widget.TabContainer // TabItem represents a single view in a TabContainer. // The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active. @@ -24,8 +25,8 @@ const ( TabLocationTrailing ) -// NewTabs creates a new tab bar widget that allows the user to choose between different visible containers -func NewTabs(items ...*TabItem) *Tabs { +// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app. +func NewAppTabs(items ...*TabItem) *AppTabs { return widget.NewTabContainer(items...) }