Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Widget scrolling plus #4701

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
@@ -1 +1 @@
github: [fyne-io, andydotxyz, toaster, Jacalz, changkun]
github: [fyne-io, andydotxyz, toaster, Jacalz, changkun, dweymouth, lucor]
34 changes: 34 additions & 0 deletions widget/list.go
Expand Up @@ -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
Expand All @@ -274,6 +290,16 @@ func (l *List) ScrollToOffset(offset float32) {
l.Refresh()
}

// ScrollUpOnePage scrolls up one page (table height)
func (l *List) ScrollUpOnePage() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these need to be public APIs (you can do the equivalent by ScrollToOffset(l.GetScrollOffset() + l.Size().Height)) but if they are going to be new public APIs they need the // Since: 2.5 designation

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
Expand All @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions widget/list_test.go
Expand Up @@ -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))
}
38 changes: 38 additions & 0 deletions widget/table.go
Expand Up @@ -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:
Expand Down Expand Up @@ -512,6 +520,7 @@ func (t *Table) ScrollToBottom() {

t.content.Offset.Y = y
t.offset.Y = y
t.content.Refresh()
t.finishScroll()
}

Expand All @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to add some offset validation here - please test what happens if you try to keep scrolling when you're already at the top or bottom of the list

Copy link
Author

@m-rei m-rei Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not go out of bounds!

There is one bug in the tree widget, but its not introduced with this PR, it just became obvious after implementing it.

When you call tree.ScrollToBottom() twice, the last row's top border vanishes.

Other than that, there are no problems. Not sure if I should investigate it, since its not part of this PR.

I can share some example code (120 lines).

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
Expand Down
66 changes: 66 additions & 0 deletions widget/table_test.go
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
42 changes: 42 additions & 0 deletions widget/tree.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
74 changes: 74 additions & 0 deletions widget/tree_internal_test.go
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down