diff --git a/components/textarea.go b/components/textarea.go index b5363bf4..25f4704d 100644 --- a/components/textarea.go +++ b/components/textarea.go @@ -553,6 +553,26 @@ func (layout *TextareaLayout) ScrollBy(dy float32) { layout.scroll = layout.clampScroll(layout.scroll + dy) } +// Wheel scrolls the box by a wheel event over the text it last painted, as ScrollBy does, and +// reports whether the box moved. A wheel the box cannot move with (it is at the end the wheel +// points to, or it shows all its rows) reports false, so the page scrolls instead. +func (layout *TextareaLayout) Wheel(event gfx.ScrollEvent) bool { + content := layout.content + if !layout.painted || event.X < content.X || event.X >= content.X+content.Width || + event.Y < content.Y || event.Y >= content.Y+content.Height { + return false + } + dy := event.DeltaY + if !event.Precise && dy > 0 && dy < 24 { + dy = 24 + } else if !event.Precise && dy < 0 && dy > -24 { + dy = -24 + } + before := layout.scroll + layout.ScrollBy(dy) + return layout.scroll != before +} + // RowCount is how many rows text takes in a box of outer width width: one for // each line that fits and as many as a longer one wraps to, and an empty one // after a final newline. The rows it wraps are the ones the next paint at that diff --git a/components/textarea_wheel_test.go b/components/textarea_wheel_test.go new file mode 100644 index 00000000..c9c2657b --- /dev/null +++ b/components/textarea_wheel_test.go @@ -0,0 +1,45 @@ +package components + +import ( + "strings" + "testing" + + "github.com/vivaui/viva/gfx" +) + +// A wheel over a TextareaLayout scrolls it while it can move in that direction, and reports +// nothing once it is at the end, so the page can scroll instead. +func TestTextareaWheelScrollsUntilItsEnd(t *testing.T) { + var lines []string + for index := range 40 { + lines = append(lines, "row "+string(rune('a'+index%26))) + } + text := strings.Join(lines, "\n") + box := newWrapBox(t, 100) + box.paint(TextareaSnapshot{Value: text, Caret: 0}) + center := gfx.Point{X: box.layout.content.X + 4, Y: box.layout.content.Y + 4} + + outside := gfx.ScrollEvent{X: box.layout.content.X - 10, Y: center.Y, DeltaY: 30} + if box.layout.Wheel(outside) { + t.Fatal("a wheel outside the text scrolled the box") + } + up := gfx.ScrollEvent{X: center.X, Y: center.Y, DeltaY: -30} + if box.layout.Wheel(up) { + t.Fatal("a wheel up at the top scrolled the box") + } + + down := gfx.ScrollEvent{X: center.X, Y: center.Y, DeltaY: 30} + if !box.layout.Wheel(down) { + t.Fatal("a wheel down over the text did not scroll the box") + } + for range 200 { + box.paint(TextareaSnapshot{Value: text, Caret: 0}) + if !box.layout.Wheel(down) { + break + } + } + box.paint(TextareaSnapshot{Value: text, Caret: 0}) + if box.layout.Wheel(down) { + t.Fatal("a wheel down at the end of the text still scrolled the box") + } +} diff --git a/ui/page.go b/ui/page.go index 87de1698..7a252fd4 100644 --- a/ui/page.go +++ b/ui/page.go @@ -1127,6 +1127,13 @@ func (p *Page) Wheel(event gfx.ScrollEvent) gfx.EventResult { return state.Wheel(event) } } + // A TextArea under the pointer scrolls its own rows first; at its end the wheel goes on + // to the page, as in a browser. + for _, entry := range p.fields { + if entry.field.rows && entry.field.area != nil && entry.field.area.Wheel(event) { + return gfx.EventResult{Handled: true, Repaint: true} + } + } if p.wheelRoute != nil { if result, ok := p.wheelRoute(p, event); ok { return result