diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 3eaff5b70d..779eb235a9 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [fyne-io, andydotxyz, toaster, Jacalz, changkun] +github: [fyne-io, andydotxyz, toaster, Jacalz, changkun, dweymouth, lucor] diff --git a/widget/list.go b/widget/list.go index f0479addec..1b23fa4054 100644 --- a/widget/list.go +++ b/widget/list.go @@ -257,6 +257,22 @@ func (l *List) ScrollToTop() { l.Refresh() } +// scrollByOnePage scrolls down or up by list height +func (l *List) scrollByOnePage(down bool) { + if l.scroller == nil { + return + } + + height := l.size.Load().Height + if down { + l.scroller.Offset.Y += height + } else { + l.scroller.Offset.Y -= height + } + l.offsetUpdated(l.scroller.Offset) + l.Refresh() +} + // ScrollToOffset scrolls the list to the given offset position. // // Since: 2.5 @@ -274,6 +290,16 @@ func (l *List) ScrollToOffset(offset float32) { l.Refresh() } +// ScrollUpOnePage scrolls up one page (table height) +func (l *List) ScrollUpOnePage() { + l.scrollByOnePage(false) +} + +// ScrollDownOnePage scrolls down one page (table height) +func (l *List) ScrollDownOnePage() { + l.scrollByOnePage(true) +} + // GetScrollOffset returns the current scroll offset position // // Since: 2.5 @@ -286,6 +312,14 @@ func (l *List) GetScrollOffset() float32 { // Implements: fyne.Focusable func (l *List) TypedKey(event *fyne.KeyEvent) { switch event.Name { + case fyne.KeyHome: + l.ScrollToTop() + case fyne.KeyPageUp: + l.ScrollUpOnePage() + case fyne.KeyPageDown: + l.ScrollDownOnePage() + case fyne.KeyEnd: + l.ScrollToBottom() case fyne.KeySpace: l.Select(l.currentFocus) case fyne.KeyDown: diff --git a/widget/list_test.go b/widget/list_test.go index 3c2f814b1c..fd3fc37e1b 100644 --- a/widget/list_test.go +++ b/widget/list_test.go @@ -666,3 +666,23 @@ func TestList_RefreshUpdatesAllItems(t *testing.T) { list.Refresh() assert.Equal(t, "0.0.", printOut) } + +func TestList_ScrollDownOnePage(t *testing.T) { + list := createList(1100) + + offset := 1000 + list.ScrollDownOnePage() + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) +} + +func TestList_ScrollUpOnePage(t *testing.T) { + list := createList(1100) + list.ScrollDownOnePage() + list.ScrollDownOnePage() + + offset := 1000 + list.ScrollUpOnePage() + assert.Equal(t, offset, int(list.offsetY)) + assert.Equal(t, offset, int(list.scroller.Offset.Y)) +} diff --git a/widget/table.go b/widget/table.go index 444341bc91..f451db2db8 100644 --- a/widget/table.go +++ b/widget/table.go @@ -344,6 +344,14 @@ func (t *Table) TouchCancel(*mobile.TouchEvent) { // Implements: fyne.Focusable func (t *Table) TypedKey(event *fyne.KeyEvent) { switch event.Name { + case fyne.KeyHome: + t.ScrollToTop() + case fyne.KeyPageUp: + t.ScrollUpOnePage() + case fyne.KeyPageDown: + t.ScrollDownOnePage() + case fyne.KeyEnd: + t.ScrollToBottom() case fyne.KeySpace: t.Select(t.currentFocus) case fyne.KeyDown: @@ -512,6 +520,7 @@ func (t *Table) ScrollToBottom() { t.content.Offset.Y = y t.offset.Y = y + t.content.Refresh() t.finishScroll() } @@ -538,9 +547,38 @@ func (t *Table) ScrollToTop() { t.content.Offset.Y = 0 t.offset.Y = 0 + t.content.Refresh() t.finishScroll() } +// scrollByOnePage scrolls down or up by table height +func (t *Table) scrollByOnePage(down bool) { + if t.Length == nil || t.content == nil { + return + } + + height := t.size.Load().Height + if down { + t.content.Offset.Y += height + } else { + t.content.Offset.Y -= height + } + t.offset.Y = t.content.Offset.Y + + t.content.Refresh() + t.finishScroll() +} + +// ScrollUpOnePage scrolls up one page (table height) +func (t *Table) ScrollUpOnePage() { + t.scrollByOnePage(false) +} + +// ScrollDownOnePage scrolls down one page (table height) +func (t *Table) ScrollDownOnePage() { + t.scrollByOnePage(true) +} + // ScrollToTrailing scrolls horizontally to the trailing edge of the table // // Since: 2.1 diff --git a/widget/table_test.go b/widget/table_test.go index b634f532a8..4a1d7ee7f2 100644 --- a/widget/table_test.go +++ b/widget/table_test.go @@ -516,6 +516,38 @@ func TestTable_ScrollToBottom(t *testing.T) { assert.Equal(t, want, table.content.Offset) } +func TestTable_ScrollDownOnePage(t *testing.T) { + test.NewApp() + defer test.NewApp() + test.ApplyTheme(t, test.NewTheme()) + + const ( + maxRows int = 20 + maxCols int = 5 + width float32 = 50 + height float32 = 50 + ) + + templ := canvas.NewRectangle(color.Gray16{}) + templ.SetMinSize(fyne.NewSize(width, height)) + + table := NewTable( + func() (int, int) { return maxRows, maxCols }, + func() fyne.CanvasObject { return templ }, + func(TableCellID, fyne.CanvasObject) {}) + + w := test.NewWindow(table) + defer w.Close() + + w.Resize(fyne.NewSize(200, 200)) + + want := fyne.Position{X: 0, Y: 180} + table.ScrollDownOnePage() + + assert.Equal(t, want, table.offset) + assert.Equal(t, want, table.content.Offset) +} + func TestTable_ScrollToLeading(t *testing.T) { test.NewApp() defer test.NewApp() @@ -574,6 +606,40 @@ func TestTable_ScrollToTop(t *testing.T) { assert.Equal(t, want, table.content.Offset) } +func TestTable_ScrollUpOnePage(t *testing.T) { + test.NewApp() + defer test.NewApp() + test.ApplyTheme(t, test.NewTheme()) + + const ( + maxRows int = 20 + maxCols int = 5 + width float32 = 50 + height float32 = 50 + ) + + templ := canvas.NewRectangle(color.Gray16{}) + templ.SetMinSize(fyne.NewSize(width, height)) + + table := NewTable( + func() (int, int) { return maxRows, maxCols }, + func() fyne.CanvasObject { return templ }, + func(TableCellID, fyne.CanvasObject) {}) + + w := test.NewWindow(table) + defer w.Close() + + w.Resize(fyne.NewSize(200, 200)) + table.ScrollDownOnePage() + table.ScrollDownOnePage() + + want := fyne.Position{X: 0, Y: 180} + table.ScrollUpOnePage() + + assert.Equal(t, want, table.offset) + assert.Equal(t, want, table.content.Offset) +} + func TestTable_ScrollToTrailing(t *testing.T) { test.NewApp() defer test.NewApp() diff --git a/widget/tree.go b/widget/tree.go index e81fb108e1..44b55e006a 100644 --- a/widget/tree.go +++ b/widget/tree.go @@ -294,6 +294,40 @@ func (t *Tree) ScrollToTop() { t.Refresh() } +// scrollByOnePage scrolls down or up by tree height +func (t *Tree) scrollByOnePage(down bool) { + if t.scroller == nil { + return + } + + height := t.size.Load().Height + if down { + t.scroller.Offset.Y += height + y, size := t.findBottom() + max := y + size.Height - t.scroller.Size().Height + if t.scroller.Offset.Y > max { + t.scroller.Offset.Y = max + } + } else { + t.scroller.Offset.Y -= height + if t.scroller.Offset.Y < 0 { + t.scroller.Offset.Y = 0 + } + } + t.offsetUpdated(t.scroller.Offset) + t.Refresh() +} + +// ScrollUpOnePage scrolls down or up by tree height +func (t *Tree) ScrollUpOnePage() { + t.scrollByOnePage(false) +} + +// ScrollDownOnePage scrolls down or up by tree height +func (t *Tree) ScrollDownOnePage() { + t.scrollByOnePage(true) +} + // Select marks the specified node to be selected. func (t *Tree) Select(uid TreeNodeID) { if len(t.selected) > 0 { @@ -325,6 +359,14 @@ func (t *Tree) ToggleBranch(uid string) { // Implements: fyne.Focusable func (t *Tree) TypedKey(event *fyne.KeyEvent) { switch event.Name { + case fyne.KeyHome: + t.ScrollToTop() + case fyne.KeyPageUp: + t.ScrollUpOnePage() + case fyne.KeyPageDown: + t.ScrollDownOnePage() + case fyne.KeyEnd: + t.ScrollToBottom() case fyne.KeySpace: t.Select(t.currentFocus) case fyne.KeyDown: diff --git a/widget/tree_internal_test.go b/widget/tree_internal_test.go index 2107b4c982..37dcf52ece 100644 --- a/widget/tree_internal_test.go +++ b/widget/tree_internal_test.go @@ -628,6 +628,42 @@ func TestTree_ScrollToBottom(t *testing.T) { assert.Equal(t, want, tree.scroller.Offset.Y) } +func TestTree_ScrollDownOnePage(t *testing.T) { + test.NewApp() + defer test.NewApp() + test.ApplyTheme(t, test.NewTheme()) + + data := make(map[string][]string) + addTreePath(data, "A") + addTreePath(data, "B", "C") + addTreePath(data, "D", "E", "F") + tree := NewTreeWithStrings(data) + tree.OpenBranch("B") + tree.OpenBranch("D") + tree.OpenBranch("E") + + w := test.NewWindow(tree) + defer w.Close() + + var ( + min = getLeaf(t, tree, "A").MinSize() + sep = theme.Padding() + ) + + // Resize tall enough to display two nodes and the separater between them + treeHeight := 2*(min.Height) + sep + w.Resize(fyne.Size{ + Width: 400, + Height: treeHeight + 2*theme.Padding(), + }) + + tree.ScrollDownOnePage() + + want := treeHeight + assert.Equal(t, want, tree.offset.Y) + assert.Equal(t, want, tree.scroller.Offset.Y) +} + func TestTree_ScrollToSelection(t *testing.T) { data := make(map[string][]string) addTreePath(data, "A") @@ -685,6 +721,44 @@ func TestTree_ScrollToTop(t *testing.T) { assert.Equal(t, float32(0), tree.scroller.Offset.Y) } +func TestTree_ScrollUpOnePage(t *testing.T) { + test.NewApp() + defer test.NewApp() + test.ApplyTheme(t, test.NewTheme()) + + data := make(map[string][]string) + addTreePath(data, "A") + addTreePath(data, "B", "C") + addTreePath(data, "D", "E", "F") + tree := NewTreeWithStrings(data) + tree.OpenBranch("B") + tree.OpenBranch("D") + tree.OpenBranch("E") + + w := test.NewWindow(tree) + defer w.Close() + + var ( + min = getLeaf(t, tree, "A").MinSize() + sep = theme.Padding() + ) + + // Resize tall enough to display two nodes and the separater between them + treeHeight := 2*(min.Height) + sep + w.Resize(fyne.Size{ + Width: 400, + Height: treeHeight + 2*theme.Padding(), + }) + + tree.ScrollDownOnePage() + tree.ScrollDownOnePage() + tree.ScrollUpOnePage() + + want := treeHeight + assert.Equal(t, want, tree.offset.Y) + assert.Equal(t, want, tree.scroller.Offset.Y) +} + func TestTree_Tap(t *testing.T) { t.Run("Branch", func(t *testing.T) { data := make(map[string][]string)