From 590f5dfd9045922a414465e6f6d789c94809ae55 Mon Sep 17 00:00:00 2001 From: Yao Jingxi <23722032@bjtu.edu.cn> Date: Sun, 15 Nov 2026 01:14:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20TextArea=20=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E6=BB=9A=E8=BD=AE=EF=BC=8C=E5=86=85=E5=AE=B9=E8=B6=85=E5=87=BA?= =?UTF-8?q?=E6=97=B6=E5=8F=AF=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 多行输入框此前不处理滚轮,只能随光标滚动,超出高度的内容无法用鼠标浏览。 现在指针位于 TextArea 上时,滚轮先滚动它自己的行;已经到达所需方向的末端时, 事件交还给页面,与浏览器行为一致。 - components: TextareaLayout.Wheel 报告是否真的移动,超出判断只看上次绘制的文本区域。 - ui: Page.Wheel 在下拉框之后、其他滚动路由之前检查 TextArea。 - 测试:components.TestTextareaWheelScrollsUntilItsEnd 覆盖区域外、顶端、末端和正常滚动。 Co-Authored-By: Claude Haiku 5.5 --- components/textarea.go | 20 ++++++++++++++ components/textarea_wheel_test.go | 45 +++++++++++++++++++++++++++++++ ui/page.go | 7 +++++ 3 files changed, 72 insertions(+) create mode 100644 components/textarea_wheel_test.go 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