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/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") } diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go index 0ca0df260c..be3743da2d 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.NewAppTabs( + 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..23f130fa02 100644 --- a/cmd/fyne_demo/screens/container.go +++ b/cmd/fyne_demo/screens/container.go @@ -7,23 +7,23 @@ 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.NewAppTabs( // TODO not best use of tabs here either + container.NewTabItem("Accordion", makeAccordionTab()), + container.NewTabItem("Card", makeCardTab()), + container.NewTabItem("Split", makeSplitTab()), + container.NewTabItem("Scroll", makeScrollTab()), // 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()), ) } @@ -51,9 +51,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 { @@ -63,11 +61,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 { @@ -89,8 +85,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 { @@ -102,8 +98,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 { @@ -112,7 +107,7 @@ func makeGridLayout() *fyne.Container { box3 := makeCell() box4 := makeCell() - return fyne.NewContainerWithLayout(layout.NewGridLayout(2), + return container.NewGridWithColumns(2, box1, box2, box3, box4) } @@ -120,11 +115,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 +127,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 +137,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..c35e30a8da 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) @@ -289,6 +290,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") }), @@ -300,14 +330,15 @@ 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.NewAppTabs( // TODO move to something better suited to this content + container.NewTabItem("Buttons", makeButtonTab()), + container.NewTabItem("Text", makeTextTab()), + container.NewTabItem("Input", makeInputTab()), + container.NewTabItem("Progress", progress), + container.NewTabItem("Form", makeFormTab()), + container.NewTabItem("List", makeListTab()), ) - tabs.OnChanged = func(t *widget.TabItem) { + tabs.OnChanged = func(t *container.TabItem) { if t.Content == progress { startProgress() } else { diff --git a/container.go b/container.go index 62e1c1de02..ffff0e23c8 100644 --- a/container.go +++ b/container.go @@ -14,37 +14,48 @@ 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 - } +// 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...) } -// Size returns the current size of this container. -func (c *Container) Size() Size { - return c.size +// 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() + return ret } -// Resize sets a new size for the Container. -func (c *Container) Resize(size Size) { - if c.size == size { - return +// 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, } - c.size = size - c.layout() + ret.size = layout.MinSize(objects) + ret.layout() + return ret } -// Position gets the current position of this Container, relative to its parent. -func (c *Container) Position() Position { - return c.position +// 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() } -// 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. +// +// Deprecated: Use replacement Add() function +func (c *Container) AddObject(o CanvasObject) { + c.Add(o) } // MinSize calculates the minimum size of a Container. @@ -62,9 +73,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 +117,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 +133,32 @@ 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, +// 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 } - ret.size = ret.MinSize() - ret.layout() + for i, o := range c.Objects { + if o != rem { + continue + } - return ret + copy(c.Objects[i:], c.Objects[i+1:]) + c.Objects[len(c.Objects)-1] = nil + c.Objects = c.Objects[:len(c.Objects)-1] + return + } } -// 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, - } +// Visible returns true if the container is currently visible, false otherwise. +func (c *Container) Visible() bool { + return !c.Hidden +} - ret.size = layout.MinSize(objects) - ret.layout() - return ret +func (c *Container) layout() { + if c.Layout != nil { + c.Layout.Layout(c.Objects, c.size) + return + } } 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..4ef7103ca5 --- /dev/null +++ b/container/scroll.go @@ -0,0 +1,42 @@ +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 + +// 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 { + 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) +} + +// 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) +} + +// 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..1ef4aa57cc --- /dev/null +++ b/container/tabs.go @@ -0,0 +1,44 @@ +package container + +import ( + "fyne.io/fyne" + "fyne.io/fyne/widget" +) + +// 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. +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 + TabLocationBottom + TabLocationTrailing +) + +// 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...) +} + +// 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/container_test.go b/container_test.go index 7472368e3c..cc5d0a5bd7 100644 --- a/container_test.go +++ b/container_test.go @@ -6,18 +6,46 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMinSize(t *testing.T) { +func TestContainer_Add(t *testing.T) { box := new(dummyObject) - minSize := box.MinSize() + 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) + 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()) +} + +func TestContainer_Hide(t *testing.T) { + box := new(dummyObject) container := NewContainerWithoutLayout(box) - assert.Equal(t, minSize, container.MinSize()) - container.AddObject(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() + + container := NewContainerWithoutLayout(box) 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 +63,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) @@ -44,47 +72,15 @@ func TestNilLayout(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()) } -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) { +func TestContainer_Remove(t *testing.T) { box := new(dummyObject) container := NewContainerWithoutLayout(box) + assert.Equal(t, 1, len(container.Objects)) - assert.True(t, container.Visible()) - assert.True(t, box.Visible()) - container.Hide() - assert.False(t, container.Visible()) - assert.True(t, box.Visible()) + container.Remove(box) + assert.Equal(t, 0, len(container.Objects)) } func TestContainer_Show(t *testing.T) { @@ -100,6 +96,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 diff --git a/internal/driver/glfw/canvas.go b/internal/driver/glfw/canvas.go index 6eae2274d9..d91f230fd2 100644 --- a/internal/driver/glfw/canvas.go +++ b/internal/driver/glfw/canvas.go @@ -270,6 +270,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/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/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/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 diff --git a/widget/accordion.go b/widget/accordion.go index ba1e673d98..a8229049d5 100644 --- a/widget/accordion.go +++ b/widget/accordion.go @@ -7,14 +7,18 @@ import ( "fyne.io/fyne/theme" ) +const accordionDividerHeight = 1 + 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{ @@ -129,6 +133,7 @@ type accordionRenderer struct { widget.BaseRenderer container *Accordion headers []*Button + dividers []*canvas.Rectangle } func (r *accordionRenderer) Layout(size fyne.Size) { @@ -136,8 +141,13 @@ 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 } + y += theme.Padding() + h := r.headers[i] h.Move(fyne.NewPos(x, y)) min := h.MinSize().Height @@ -151,13 +161,16 @@ func (r *accordionRenderer) Layout(size fyne.Size) { d.Resize(fyne.NewSize(size.Width, min)) y += min } + + 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() + size.Height += accordionDividerHeight } min := r.headers[i].MinSize() size.Width = fyne.Max(size.Width, min.Width) @@ -173,6 +186,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) @@ -187,6 +204,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) @@ -194,6 +212,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 +243,21 @@ 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++ { + 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 + 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..f950897544 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) @@ -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(), 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+2*theme.Padding(), 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+theme.Padding(), 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+3*theme.Padding(), 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+2*theme.Padding(), 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+4*theme.Padding(), 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(), 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+2*theme.Padding(), 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+3*theme.Padding(), 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,35 +114,35 @@ 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+2*theme.Padding(), 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+4*theme.Padding(), 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+3*theme.Padding(), 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+5*theme.Padding(), 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) }) } -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) @@ -159,17 +159,17 @@ 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() + ac := NewAccordion() ac.Append(ai) ac.Close(0) ar := test.WidgetRenderer(ac).(*accordionRenderer) 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) { @@ -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) @@ -194,17 +194,19 @@ 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() + height += theme.Padding()*2 + 1 height += aih1.Height - height += theme.Padding() + 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) { - ac := &AccordionContainer{ + ac := &Accordion{ MultiOpen: true, } ac.Append(ai0) @@ -223,21 +225,23 @@ 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() + 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 + height += theme.Padding() assert.Equal(t, height, min.Height) }) t.Run("One_Closed", func(t *testing.T) { - ac := &AccordionContainer{ + ac := &Accordion{ MultiOpen: true, } ac.Append(ai0) @@ -257,19 +261,21 @@ 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() + 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() 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) @@ -283,12 +289,38 @@ 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 := theme.Padding() + height += aih0.Height + height += theme.Padding()*2 + 1 height += aih1.Height - height += theme.Padding() + height += theme.Padding()*2 + 1 height += aih2.Height + height += theme.Padding() assert.Equal(t, height, min.Height) }) }) } + +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"))) + 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()) +} diff --git a/widget/accordion_test.go b/widget/accordion_test.go index 3c1fc15057..989fabaa93 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" @@ -12,27 +13,50 @@ 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_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() + ac := widget.NewAccordion() ac.Append(&widget.AccordionItem{ Title: "foo", Detail: widget.NewLabel("foobar"), @@ -43,7 +67,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 +77,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 +88,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 +100,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 +111,7 @@ func TestAccordionContainer_Layout(t *testing.T) { }{ "single_open_one_item": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("11111"), }, @@ -95,7 +119,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 +128,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 +140,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 +154,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 +163,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 +173,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 +186,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 +199,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,13 +219,13 @@ 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()*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 @@ -223,103 +247,103 @@ func TestAccordionContainer_MinSize(t *testing.T) { }{ "single_open_one_item": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", 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{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, }, opened: []int{0}, - want: fyne.NewSize(minWidthA1, minHeightA1), + want: fyne.NewSize(minWidthA1, minHeightA1+theme.Padding()*2), }, "single_open_multiple_items": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*4+1), }, "single_open_multiple_items_opened": { items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, - &widget.AccordionItem{ + { Title: "B", Detail: widget.NewLabel("2222222222"), }, }, 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()*5+1), }, "multiple_open_one_item": { multiOpen: true, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", 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, items: []*widget.AccordionItem{ - &widget.AccordionItem{ + { Title: "A", Detail: widget.NewLabel("111111"), }, }, opened: []int{0}, - want: fyne.NewSize(minWidthA1, minHeightA1), + want: fyne.NewSize(minWidthA1, minHeightA1+theme.Padding()*2), }, "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"), }, }, - want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()), + want: fyne.NewSize(minWidthA1B2, minSizeA.Height+minSizeB.Height+theme.Padding()*4+1), }, "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"), }, }, 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()*6+1), }, } { t.Run(name, func(t *testing.T) { - accordion := &widget.AccordionContainer{ + accordion := &widget.Accordion{ MultiOpen: tt.multiOpen, } for _, ai := range tt.items { @@ -334,9 +358,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 +387,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 +402,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 +422,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 +439,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)) diff --git a/widget/list.go b/widget/list.go new file mode 100644 index 0000000000..931412c728 --- /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) > 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] + } + } + 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..373860221b --- /dev/null +++ b/widget/list_test.go @@ -0,0 +1,199 @@ +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(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, 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(1000) + 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(1000) + 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(1000) + 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(1000) + 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(1000) + 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(1000) + 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 TestList_SmallList(t *testing.T) { + var data []string + 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)) + } + + 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..7e4bceef27 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 ) @@ -208,6 +213,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() } @@ -331,12 +339,15 @@ 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 - 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 +458,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 } @@ -463,18 +477,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..e8b17fa6ab 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 ) @@ -291,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() @@ -298,7 +309,6 @@ func (r *tabContainerRenderer) Refresh() { o.Hide() } } - r.Layout(r.container.Size()) } for i, button := range r.tabBar.Objects { if i == current { diff --git a/widget/testdata/accordion_layout_multiple_open_multiple_items.png b/widget/testdata/accordion_layout_multiple_open_multiple_items.png index 2cef627ddd..a6f11793b8 100644 Binary files a/widget/testdata/accordion_layout_multiple_open_multiple_items.png and b/widget/testdata/accordion_layout_multiple_open_multiple_items.png differ 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 039cb0a1e6..f5162336d6 100644 Binary files a/widget/testdata/accordion_layout_multiple_open_multiple_items_opened.png and b/widget/testdata/accordion_layout_multiple_open_multiple_items_opened.png differ diff --git a/widget/testdata/accordion_layout_multiple_open_one_item.png b/widget/testdata/accordion_layout_multiple_open_one_item.png index ed3d50071c..ba219bbd68 100644 Binary files a/widget/testdata/accordion_layout_multiple_open_one_item.png and b/widget/testdata/accordion_layout_multiple_open_one_item.png differ 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 229c702a94..a2b9ed93c8 100644 Binary files a/widget/testdata/accordion_layout_multiple_open_one_item_opened.png and b/widget/testdata/accordion_layout_multiple_open_one_item_opened.png differ diff --git a/widget/testdata/accordion_layout_single_open_multiple_items.png b/widget/testdata/accordion_layout_single_open_multiple_items.png index 2cef627ddd..a6f11793b8 100644 Binary files a/widget/testdata/accordion_layout_single_open_multiple_items.png and b/widget/testdata/accordion_layout_single_open_multiple_items.png differ 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 c6965a946a..f9b4742ca7 100644 Binary files a/widget/testdata/accordion_layout_single_open_multiple_items_opened.png and b/widget/testdata/accordion_layout_single_open_multiple_items_opened.png differ diff --git a/widget/testdata/accordion_layout_single_open_one_item.png b/widget/testdata/accordion_layout_single_open_one_item.png index ed3d50071c..ba219bbd68 100644 Binary files a/widget/testdata/accordion_layout_single_open_one_item.png and b/widget/testdata/accordion_layout_single_open_one_item.png differ 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 229c702a94..a2b9ed93c8 100644 Binary files a/widget/testdata/accordion_layout_single_open_one_item_opened.png and b/widget/testdata/accordion_layout_single_open_one_item_opened.png differ diff --git a/widget/testdata/accordion_theme_changed.png b/widget/testdata/accordion_theme_changed.png new file mode 100644 index 0000000000..221fac6f6d Binary files /dev/null and b/widget/testdata/accordion_theme_changed.png differ diff --git a/widget/testdata/accordion_theme_initial.png b/widget/testdata/accordion_theme_initial.png new file mode 100644 index 0000000000..6dbf5deab4 Binary files /dev/null and b/widget/testdata/accordion_theme_initial.png differ diff --git a/widget/testdata/list/list_initial.png b/widget/testdata/list/list_initial.png new file mode 100644 index 0000000000..7c20c24812 Binary files /dev/null and b/widget/testdata/list/list_initial.png differ diff --git a/widget/testdata/list/list_new_data.png b/widget/testdata/list/list_new_data.png new file mode 100644 index 0000000000..20e2608f52 Binary files /dev/null and b/widget/testdata/list/list_new_data.png differ diff --git a/widget/testdata/list/list_offset_changed.png b/widget/testdata/list/list_offset_changed.png new file mode 100644 index 0000000000..be3098d29f Binary files /dev/null and b/widget/testdata/list/list_offset_changed.png differ diff --git a/widget/testdata/list/list_resized.png b/widget/testdata/list/list_resized.png new file mode 100644 index 0000000000..688ac8e49b Binary files /dev/null and b/widget/testdata/list/list_resized.png differ diff --git a/widget/testdata/list/list_theme_changed.png b/widget/testdata/list/list_theme_changed.png new file mode 100644 index 0000000000..4d33e14300 Binary files /dev/null and b/widget/testdata/list/list_theme_changed.png differ