Compare commits
1
Commits
v0.5.2
..
bc22a9bf00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc22a9bf00 |
No files matched your search
@@ -226,9 +226,11 @@ division approximation; `SoftLight` uses init-time LUTs replacing `math.Sqrt`.
|
|||||||
`Key == KeyRune` with `Rune` set for printable input, `Modifiers` bitmask
|
`Key == KeyRune` with `Rune` set for printable input, `Modifiers` bitmask
|
||||||
(`ModShift`, `ModAlt`, `ModCtrl`), `Action` (`KeyPress`, and `KeyRepeat` and
|
(`ModShift`, `ModAlt`, `ModCtrl`), `Action` (`KeyPress`, and `KeyRepeat` and
|
||||||
`KeyRelease` while releases are reported; see below)
|
`KeyRelease` while releases are reported; see below)
|
||||||
- `EventMouse` — 0-indexed `MouseX/Y`, `MouseBtn` (buttons, wheel),
|
- `EventMouse` — 0-indexed `MouseX/Y`, `MouseBtn` (the three buttons, the
|
||||||
`MouseAction` (press/release/move/drag), modifiers. Enable via
|
wheel's four directions, back and forward), `MouseAction`
|
||||||
`SetMouseMode(MouseModeClick | MouseModeDrag)`; SGR protocol only.
|
(press/release/move/drag; a wheel step is a press with no release),
|
||||||
|
modifiers. Enable via `SetMouseMode(MouseModeClick | MouseModeDrag)`; SGR
|
||||||
|
protocol only.
|
||||||
- `EventResize` — new `Width`/`Height`
|
- `EventResize` — new `Width`/`Height`
|
||||||
- `EventPaste` — `Text` holds a whole paste, as sent, control characters
|
- `EventPaste` — `Text` holds a whole paste, as sent, control characters
|
||||||
included. Enable via `SetPasteMode(true)` (bracketed paste); off, a paste
|
included. Enable via `SetPasteMode(true)` (bracketed paste); off, a paste
|
||||||
|
|||||||
@@ -623,7 +623,8 @@ func (r *inputReader) parseControl(b byte) Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseSGRMouse parses mouse SGR sequences, ESC [ < Btn ; X ; Y M/m, from
|
// parseSGRMouse parses mouse SGR sequences, ESC [ < Btn ; X ; Y M/m, from
|
||||||
// the parameters and final byte; a malformed one is dropped as KeyNone
|
// the parameters and final byte; a malformed one, or a button past 9, is
|
||||||
|
// dropped as KeyNone
|
||||||
func parseSGRMouse(params []byte, final byte) Event {
|
func parseSGRMouse(params []byte, final byte) Event {
|
||||||
btn, x, y, ok := parseSGRParams(params)
|
btn, x, y, ok := parseSGRParams(params)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -632,48 +633,34 @@ func parseSGRMouse(params []byte, final byte) Event {
|
|||||||
|
|
||||||
ev := Event{Type: EventMouse, MouseX: x - 1, MouseY: y - 1} // Convert to 0-indexed
|
ev := Event{Type: EventMouse, MouseX: x - 1, MouseY: y - 1} // Convert to 0-indexed
|
||||||
|
|
||||||
// Decode button and action
|
// Bits 0-1 number a button within its bank: the main three and none, the
|
||||||
// Bits 0-1: button (0=left, 1=middle, 2=right, 3=release)
|
// wheel's four directions (bit 6), or buttons 8 to 11 (bit 7); bit 5 is motion
|
||||||
// Bit 5 (32): motion
|
id := btn & 0x03
|
||||||
// Bit 6 (64): scroll
|
switch btn & (64 | 128) {
|
||||||
buttonID := btn & 0x03
|
case 0:
|
||||||
isMotion := btn&32 != 0
|
ev.MouseBtn = [...]MouseButton{MouseBtnLeft, MouseBtnMiddle, MouseBtnRight, MouseBtnNone}[id]
|
||||||
isScroll := btn&64 != 0
|
case 64:
|
||||||
|
ev.MouseBtn = [...]MouseButton{MouseBtnWheelUp, MouseBtnWheelDown, MouseBtnWheelLeft, MouseBtnWheelRight}[id]
|
||||||
|
case 128:
|
||||||
|
if id > 1 {
|
||||||
|
return Event{Type: EventKey, Key: KeyNone}
|
||||||
|
}
|
||||||
|
ev.MouseBtn = MouseBtnBack + MouseButton(id)
|
||||||
|
default:
|
||||||
|
return Event{Type: EventKey, Key: KeyNone}
|
||||||
|
}
|
||||||
|
|
||||||
if isScroll {
|
switch {
|
||||||
// Scroll: buttonID 0=up, 1=down
|
case btn&64 != 0:
|
||||||
if buttonID == 0 {
|
ev.MouseAction = MouseActionPress // A wheel step is its press
|
||||||
ev.MouseBtn = MouseBtnWheelUp
|
case final == 'm':
|
||||||
} else {
|
ev.MouseAction = MouseActionRelease
|
||||||
ev.MouseBtn = MouseBtnWheelDown
|
case btn&32 == 0:
|
||||||
}
|
ev.MouseAction = MouseActionPress
|
||||||
ev.MouseAction = MouseActionPress // Scroll is instantaneous
|
case ev.MouseBtn != MouseBtnNone:
|
||||||
} else {
|
ev.MouseAction = MouseActionDrag
|
||||||
// Regular button
|
default:
|
||||||
switch buttonID {
|
ev.MouseAction = MouseActionMove
|
||||||
case 0:
|
|
||||||
ev.MouseBtn = MouseBtnLeft
|
|
||||||
case 1:
|
|
||||||
ev.MouseBtn = MouseBtnMiddle
|
|
||||||
case 2:
|
|
||||||
ev.MouseBtn = MouseBtnRight
|
|
||||||
case 3:
|
|
||||||
ev.MouseBtn = MouseBtnNone // Release with no specific button
|
|
||||||
}
|
|
||||||
|
|
||||||
if final == 'M' {
|
|
||||||
if isMotion {
|
|
||||||
if ev.MouseBtn != MouseBtnNone {
|
|
||||||
ev.MouseAction = MouseActionDrag
|
|
||||||
} else {
|
|
||||||
ev.MouseAction = MouseActionMove
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ev.MouseAction = MouseActionPress
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ev.MouseAction = MouseActionRelease
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract modifiers from button byte
|
// Extract modifiers from button byte
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ const (
|
|||||||
MouseBtnRight
|
MouseBtnRight
|
||||||
MouseBtnWheelUp
|
MouseBtnWheelUp
|
||||||
MouseBtnWheelDown
|
MouseBtnWheelDown
|
||||||
MouseBtnBack // Button 4 (if supported)
|
MouseBtnBack // Button 8, a mouse's back side button
|
||||||
MouseBtnForward // Button 5 (if supported)
|
MouseBtnForward // Button 9, its forward side button
|
||||||
|
MouseBtnWheelLeft // A wheel tilted left, or a touchpad swiped so
|
||||||
|
MouseBtnWheelRight // A wheel tilted right, or a touchpad swiped so
|
||||||
)
|
)
|
||||||
|
|
||||||
// MouseAction represents the type of mouse event
|
// MouseAction represents the type of mouse event
|
||||||
@@ -52,6 +54,10 @@ func (b MouseButton) String() string {
|
|||||||
return "Back"
|
return "Back"
|
||||||
case MouseBtnForward:
|
case MouseBtnForward:
|
||||||
return "Forward"
|
return "Forward"
|
||||||
|
case MouseBtnWheelLeft:
|
||||||
|
return "WheelLeft"
|
||||||
|
case MouseBtnWheelRight:
|
||||||
|
return "WheelRight"
|
||||||
default:
|
default:
|
||||||
return "None"
|
return "None"
|
||||||
}
|
}
|
||||||
@@ -71,4 +77,4 @@ func (a MouseAction) String() string {
|
|||||||
default:
|
default:
|
||||||
return "None"
|
return "None"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
// 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
|
// 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
|
// nothing once four DEL, which end any sequence or character, and a paste
|
||||||
|
|||||||
+25
-2
@@ -187,6 +187,28 @@ r.List(items, state.Selection, state.Offset, tui.ListOpts{CursorBg: color.DarkSl
|
|||||||
r.ScrollBar(r.W-1, state.Offset, state.Visible, state.Total, color.IronGray)
|
r.ScrollBar(r.W-1, state.Offset, state.Visible, state.Total, color.IronGray)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ScrollThumb` is the thumb a bar draws. `ScrollDrag` drives one by the
|
||||||
|
pointer, rows counted from the track's top: a press on the thumb takes hold
|
||||||
|
of it, a press on the track moves the thumb's middle there and takes hold, and
|
||||||
|
a drag keeps the held row under the pointer, each returning the offset to show.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var drag tui.ScrollDrag // kept across events
|
||||||
|
bar := r.Sub(r.W-1, 0, 1, r.H) // the column ScrollBar draws, in screen cells at bar.X, bar.Y
|
||||||
|
|
||||||
|
// per left button event:
|
||||||
|
switch ev.MouseAction {
|
||||||
|
case terminal.MouseActionPress:
|
||||||
|
if ev.MouseX == bar.X {
|
||||||
|
state.ScrollTo(drag.Press(ev.MouseY-bar.Y, bar.H, state.Offset, state.Visible, state.Total))
|
||||||
|
}
|
||||||
|
case terminal.MouseActionDrag:
|
||||||
|
state.ScrollTo(drag.Drag(ev.MouseY-bar.Y, bar.H, state.Offset, state.Visible, state.Total))
|
||||||
|
case terminal.MouseActionRelease:
|
||||||
|
drag.Release()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Modal dialog
|
### Modal dialog
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -265,5 +287,6 @@ Pure logic, no rendering — usable independently:
|
|||||||
(widget default or row background) — check specific widget docs. A zero
|
(widget default or row background) — check specific widget docs. A zero
|
||||||
background with no Bg color bits is transparent: the cell keeps the
|
background with no Bg color bits is transparent: the cell keeps the
|
||||||
background beneath it, bits and all.
|
background beneath it, bits and all.
|
||||||
- Mouse hit testing: `TabBar` returns `[]TabBounds`; other widgets require
|
- Mouse hit testing: `TabBar` returns `[]TabBounds`, and `ScrollDrag` maps a
|
||||||
application-side geometry from the regions used.
|
scrollbar's rows to offsets; other widgets require application-side
|
||||||
|
geometry from the regions used.
|
||||||
+64
-10
@@ -35,8 +35,8 @@ func (r Region) ScrollBarStyled(x int, offset, visible, total int, opts ScrollBa
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackH := r.H
|
trackH := r.H
|
||||||
if total <= visible || trackH < 3 {
|
thumbY, thumbH := ScrollThumb(trackH, offset, visible, total)
|
||||||
// No scrolling needed or track too small
|
if thumbH == 0 {
|
||||||
if opts.HideIdle {
|
if opts.HideIdle {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -46,14 +46,6 @@ func (r Region) ScrollBarStyled(x int, offset, visible, total int, opts ScrollBa
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbH := min(max((visible*trackH)/total, 1), trackH)
|
|
||||||
maxScroll := total - visible
|
|
||||||
thumbY := 0
|
|
||||||
if maxScroll > 0 {
|
|
||||||
thumbY = (offset * (trackH - thumbH)) / maxScroll
|
|
||||||
}
|
|
||||||
thumbY = min(max(thumbY, 0), trackH-thumbH)
|
|
||||||
|
|
||||||
for y := range trackH {
|
for y := range trackH {
|
||||||
if y >= thumbY && y < thumbY+thumbH {
|
if y >= thumbY && y < thumbY+thumbH {
|
||||||
r.Cell(x, y, thumbCh, opts.ThumbFg, opts.Bg, terminal.AttrNone)
|
r.Cell(x, y, thumbCh, opts.ThumbFg, opts.Bg, terminal.AttrNone)
|
||||||
@@ -63,6 +55,68 @@ func (r Region) ScrollBarStyled(x int, offset, visible, total int, opts ScrollBa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScrollThumb is the thumb a scrollbar trackH rows tall draws for offset: its
|
||||||
|
// first row and its height, 0 when content fits or the track is under 3 rows
|
||||||
|
func ScrollThumb(trackH, offset, visible, total int) (y, h int) {
|
||||||
|
if total <= visible || trackH < 3 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
h = min(max((visible*trackH)/total, 1), trackH)
|
||||||
|
y = (offset * (trackH - h)) / (total - visible)
|
||||||
|
return min(max(y, 0), trackH-h), h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollDrag holds a scrollbar's thumb under 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 first, and each drag keeps the row held
|
||||||
|
// under the pointer. Each call returns the offset to show.
|
||||||
|
type ScrollDrag struct {
|
||||||
|
Active bool // The thumb is held
|
||||||
|
Grab int // The thumb's row the pointer holds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press takes hold of the thumb at track row y; off the track it holds nothing
|
||||||
|
func (d *ScrollDrag) Press(y, trackH, offset, visible, total int) int {
|
||||||
|
top, h := ScrollThumb(trackH, offset, visible, total)
|
||||||
|
d.Active = h > 0 && y >= 0 && y < trackH
|
||||||
|
if !d.Active {
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
if y < top || y >= top+h {
|
||||||
|
offset = scrollOffsetAt(y-h/2, trackH, h, visible, total)
|
||||||
|
top, _ = ScrollThumb(trackH, offset, visible, total)
|
||||||
|
}
|
||||||
|
d.Grab = min(max(y-top, 0), h-1)
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag moves the held row to track row y, clamped at the track's ends
|
||||||
|
func (d *ScrollDrag) Drag(y, trackH, offset, visible, total int) int {
|
||||||
|
_, h := ScrollThumb(trackH, offset, visible, total)
|
||||||
|
if !d.Active || h == 0 {
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
return scrollOffsetAt(y-d.Grab, trackH, h, visible, total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release lets go of the thumb
|
||||||
|
func (d *ScrollDrag) Release() { *d = ScrollDrag{} }
|
||||||
|
|
||||||
|
// scrollOffsetAt is the offset whose thumb, h rows tall, starts nearest row top
|
||||||
|
func scrollOffsetAt(top, trackH, h, visible, total int) int {
|
||||||
|
span, maxScroll := trackH-h, total-visible
|
||||||
|
top = min(max(top, 0), span)
|
||||||
|
o := (top*maxScroll + span - 1) / span // The first offset whose thumb starts at or past top
|
||||||
|
if o > 0 {
|
||||||
|
at, _ := ScrollThumb(trackH, o, visible, total)
|
||||||
|
before, _ := ScrollThumb(trackH, o-1, visible, total)
|
||||||
|
if at-top > top-before {
|
||||||
|
return o - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
// ScrollIndicator draws compact indicator text (Top/Bot/XX%)
|
// ScrollIndicator draws compact indicator text (Top/Bot/XX%)
|
||||||
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg color.RGB) {
|
func (r Region) ScrollIndicator(y int, offset, visible, total int, fg color.RGB) {
|
||||||
if y < 0 || y >= r.H {
|
if y < 0 || y >= r.H {
|
||||||
|
|||||||
@@ -468,6 +468,60 @@ func TestWindowShowsTheRowsInView(t *testing.T) {
|
|||||||
r.Sub(8, 0, 4, 3).Window(10, &v, MonoTheme.Text, content) // no columns, past the right edge
|
r.Sub(8, 0, 4, 3).Window(10, &v, MonoTheme.Text, content) // no columns, past the right edge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A press on a scrollbar's track moves the thumb's middle to it and a press on
|
||||||
|
// the thumb leaves it, either taking hold; a drag keeps the held row under the
|
||||||
|
// pointer, clamped at the ends, or as near as an offset can put it when there are
|
||||||
|
// fewer offsets than rows. A bar with no thumb, or a row off it, holds nothing.
|
||||||
|
func TestScrollDragHoldsTheThumbUnderThePointer(t *testing.T) {
|
||||||
|
near := func(tops []int, want, got int) bool {
|
||||||
|
for _, top := range tops {
|
||||||
|
if max(top-want, want-top) < max(got-want, want-got) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for trackH := 3; trackH <= 10; trackH++ {
|
||||||
|
for total := 1; total <= 30; total++ {
|
||||||
|
for visible := 1; visible <= total+1; visible++ {
|
||||||
|
var tops []int
|
||||||
|
for o := 0; o <= total-visible; o++ {
|
||||||
|
top, _ := ScrollThumb(trackH, o, visible, total)
|
||||||
|
tops = append(tops, top)
|
||||||
|
}
|
||||||
|
for y := -1; y <= trackH; y++ {
|
||||||
|
var d ScrollDrag
|
||||||
|
from := (total - visible) / 3
|
||||||
|
fromTop, h := ScrollThumb(trackH, from, visible, total)
|
||||||
|
off := d.Press(y, trackH, from, visible, total)
|
||||||
|
top, _ := ScrollThumb(trackH, off, visible, total)
|
||||||
|
onThumb := func(top int) bool { return y >= top && y < top+h }
|
||||||
|
switch {
|
||||||
|
case h == 0 || y < 0 || y >= trackH:
|
||||||
|
if d.Active || off != from {
|
||||||
|
t.Fatalf("%d/%d/%d: a press at %d off the bar holds %v at %d", trackH, visible, total, y, d.Active, off)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case onThumb(fromTop):
|
||||||
|
if off != from {
|
||||||
|
t.Fatalf("%d/%d/%d: a press on the thumb moved it %d to %d", trackH, visible, total, from, off)
|
||||||
|
}
|
||||||
|
case !near(tops, min(max(y-h/2, 0), trackH-h), top) || !onThumb(top) && slices.ContainsFunc(tops, onThumb):
|
||||||
|
t.Fatalf("%d/%d/%d: a press at %d starts the thumb at %d", trackH, visible, total, y, top)
|
||||||
|
}
|
||||||
|
for to := -2; to <= trackH+1; to++ {
|
||||||
|
got, _ := ScrollThumb(trackH, d.Drag(to, trackH, off, visible, total), visible, total)
|
||||||
|
if want := min(max(to-d.Grab, 0), trackH-h); !near(tops, want, got) {
|
||||||
|
t.Fatalf("%d/%d/%d: held at %d and dragged to %d, the thumb starts at %d, want near %d",
|
||||||
|
trackH, visible, total, d.Grab, to, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A label is drawn a column a rune, so one holding runes of more than a byte
|
// A label is drawn a column a rune, so one holding runes of more than a byte
|
||||||
// keeps them together: each widget draws s as many times as it is given it
|
// keeps them together: each widget draws s as many times as it is given it
|
||||||
func TestLabelsKeepTheirRunesTogether(t *testing.T) {
|
func TestLabelsKeepTheirRunesTogether(t *testing.T) {
|
||||||
|
|||||||
Reference in new issue
Block a user