v0.6.0 mouse: every wheel direction and side button named; tui: a scrollbar the pointer drives

* mouse: the wheel's four directions and the side buttons

parseSGRMouse read a report's low two bits alone, so a wheel tilted left or
right (66, 67) reported as WheelDown, and a mouse's back and forward buttons
(128, 129) as a left or middle press. It now decodes by bank: bits 0-1 number
a button within the main three and none, the wheel's four directions (bit 6),
or buttons 8 to 11 (bit 7). WheelLeft and WheelRight join MouseButton after
Forward, so every existing value keeps its number; Back and Forward are
reported for buttons 8 and 9; buttons 10 and 11, and a report with both bank
bits set, are dropped as KeyNone. A wheel step is a press with no release in
every direction.

* tui: ScrollThumb and ScrollDrag

ScrollThumb is the thumb ScrollBarStyled draws, its first row and height,
exported so an application hit-tests the bar it drew. ScrollDrag drives a bar
by the pointer, rows counted from the track's top: a press on the thumb takes
hold where it lands, a press on the track moves the thumb's middle there and
takes hold, and each drag keeps the held row under the pointer, clamped at the
track's ends, at the nearest offset when there are fewer offsets than rows.
Each call returns the offset to show.

A table of SGR reports covers each bank and drops the buttons past 9; a
property test over every track of 3 to 10 rows and content up to 30 checks
each press and drag against the thumb ScrollThumb draws.
This commit is contained in:
Lixen authored and GitHub committed 2026-10-26 20:42:04 -04:00
1 parent d4cc9f2407
commit bc22a9bf00
7 files changed
+215 -60

No files matched your search

+29
View File
@@ -228,6 +228,35 @@ func TestNoInputStopsTheReader(t *testing.T) {
}
}
// A mouse report names its button in each bank: the main three, the wheel's four
// directions, and back and forward among buttons 8 to 11, whose others, which no
// application could tell apart from the main three before, are dropped
func TestMouseReportsNameTheirButtons(t *testing.T) {
at := func(btn MouseButton, action MouseAction, mods Modifier) Event {
return Event{Type: EventMouse, MouseX: 4, MouseY: 2, MouseBtn: btn, MouseAction: action, Modifiers: mods}
}
for _, c := range []struct {
report string
want []Event
}{
{"\x1b[<20;5;3M", []Event{at(MouseBtnLeft, MouseActionPress, ModShift|ModCtrl)}},
{"\x1b[<34;5;3M", []Event{at(MouseBtnRight, MouseActionDrag, 0)}},
{"\x1b[<35;5;3M", []Event{at(MouseBtnNone, MouseActionMove, 0)}},
{"\x1b[<64;5;3M", []Event{at(MouseBtnWheelUp, MouseActionPress, 0)}},
{"\x1b[<65;5;3M", []Event{at(MouseBtnWheelDown, MouseActionPress, 0)}},
{"\x1b[<66;5;3M", []Event{at(MouseBtnWheelLeft, MouseActionPress, 0)}},
{"\x1b[<75;5;3M", []Event{at(MouseBtnWheelRight, MouseActionPress, ModAlt)}},
{"\x1b[<128;5;3M\x1b[<128;5;3m", []Event{at(MouseBtnBack, MouseActionPress, 0), at(MouseBtnBack, MouseActionRelease, 0)}},
{"\x1b[<129;5;3M", []Event{at(MouseBtnForward, MouseActionPress, 0)}},
{"\x1b[<130;5;3Mx", []Event{key('x')}},
{"\x1b[<192;5;3Mx", []Event{key('x')}},
} {
if got, _ := parsed(c.report); !slices.Equal(got, c.want) {
t.Errorf("%q: got %+v, want %+v", c.report, got, c.want)
}
}
}
// Whatever bytes arrive, however reads split them, the reader sends the same
// events, holds back no more than ESC and the longest sequence, and holds
// nothing once four DEL, which end any sequence or character, and a paste