feat(ui): add Caps Lock and Num Lock modifier bits (#9)
* feat(ui): add Caps Lock and Num Lock modifier bits * test(ui): merge lock modifier tests into one table with a shared renderer helper
- commit
- 43c5a7c8ac56fa3481ae82f0d5af88d69e893903
- author
- bnema · · 2026-10-01 20:45 UTC
- committer
- GitHub · · 2026-10-01 20:45 UTC
- parent
- 657f658
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v0.6.0
4 files · +106 -16
M docs/runtime.md +1 -1
| @@ -5,7 +5,7 @@ | |||
| 5 | 5 | A client loop forwards events and presents frames: | |
| 6 | 6 | ||
| 7 | 7 | - `Resize(width, height, scale)` sets the logical size and scale; invalid values are ignored. Nothing is drawn before the first `Resize`. | |
| 8 | - | - `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild. | |
| 8 | + | - `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `Modifiers` is a bit set of `ModShift`, `ModCtrl`, `ModAlt`, `ModSuper` (held) and `ModCapsLock`, `ModNumLock` (latched). Only Shift and Ctrl affect shortcuts and editing; lock bits are ignored, and views do not see modifiers, so an application that shows a lock indicator keeps the lock bits of `Input.Modifiers` in its model. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild. | |
| 9 | 9 | - `Render(&out, &model, view)` builds the view when input or a redraw request is queued and draws it into a free buffer. It returns false, without touching the GPU, when nothing changed or no buffer is free. When it returns true, `out` describes the frame in physical pixels: DMA-BUF `Planes`, `FourCC`, `Modifier`, acquire and release timelines and points, `Damage`, `Cursor`, and `InputRects` with `InputRectsChanged`. | |
| 10 | 10 | - `Released(buffer)` returns a buffer to the pool when its `ReleaseFD` eventfd becomes readable. | |
| 11 | 11 | - `Pending()` reports a built frame waiting for the GPU rather than the compositor. No event announces this: arm a short timer (about 2 ms) and call `Render` again. `Wake()` is signaled when another goroutine, for example a finished paste, needs a `Render`. | |
| @@ -5,7 +5,7 @@ | |||||
| 5 | A client loop forwards events and presents frames: | 5 | A client loop forwards events and presents frames: | ||
| 6 | 6 | ||||
| 7 | - `Resize(width, height, scale)` sets the logical size and scale; invalid values are ignored. Nothing is drawn before the first `Resize`. | 7 | - `Resize(width, height, scale)` sets the logical size and scale; invalid values are ignored. Nothing is drawn before the first `Resize`. | ||
| 8 | - | - `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild. | 8 | + | - `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `Modifiers` is a bit set of `ModShift`, `ModCtrl`, `ModAlt`, `ModSuper` (held) and `ModCapsLock`, `ModNumLock` (latched). Only Shift and Ctrl affect shortcuts and editing; lock bits are ignored, and views do not see modifiers, so an application that shows a lock indicator keeps the lock bits of `Input.Modifiers` in its model. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild. |
| 9 | - `Render(&out, &model, view)` builds the view when input or a redraw request is queued and draws it into a free buffer. It returns false, without touching the GPU, when nothing changed or no buffer is free. When it returns true, `out` describes the frame in physical pixels: DMA-BUF `Planes`, `FourCC`, `Modifier`, acquire and release timelines and points, `Damage`, `Cursor`, and `InputRects` with `InputRectsChanged`. | 9 | - `Render(&out, &model, view)` builds the view when input or a redraw request is queued and draws it into a free buffer. It returns false, without touching the GPU, when nothing changed or no buffer is free. When it returns true, `out` describes the frame in physical pixels: DMA-BUF `Planes`, `FourCC`, `Modifier`, acquire and release timelines and points, `Damage`, `Cursor`, and `InputRects` with `InputRectsChanged`. | ||
| 10 | - `Released(buffer)` returns a buffer to the pool when its `ReleaseFD` eventfd becomes readable. | 10 | - `Released(buffer)` returns a buffer to the pool when its `ReleaseFD` eventfd becomes readable. | ||
| 11 | - `Pending()` reports a built frame waiting for the GPU rather than the compositor. No event announces this: arm a short timer (about 2 ms) and call `Render` again. `Wake()` is signaled when another goroutine, for example a finished paste, needs a `Render`. | 11 | - `Pending()` reports a built frame waiting for the GPU rather than the compositor. No event announces this: arm a short timer (about 2 ms) and call `Render` again. `Wake()` is signaled when another goroutine, for example a finished paste, needs a `Render`. | ||
M internal/ui/input.go +8 -1
| @@ -15,7 +15,12 @@ const ( | |||
| 15 | 15 | InputReset // platform dropped queued input; treat held state as released | |
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | - | // Modifiers is a bit set of keyboard modifiers held during a key event. | |
| 18 | + | // Modifiers is a bit set of keyboard modifiers held during a key or pointer | |
| 19 | + | // event. ModShift, ModCtrl, ModAlt and ModSuper report held keys; ModCapsLock | |
| 20 | + | // and ModNumLock report the latched lock state. The Renderer reads only | |
| 21 | + | // ModShift and ModCtrl, so lock bits never change shortcuts or text entry. | |
| 22 | + | // The Renderer does not expose modifiers to views: an application that shows a | |
| 23 | + | // lock indicator records the lock bits of Input.Modifiers in its own model. | |
| 19 | 24 | type Modifiers uint8 | |
| 20 | 25 | ||
| 21 | 26 | const ( | |
| @@ -23,4 +28,6 @@ const ( | |||
| 23 | 28 | ModCtrl | |
| 24 | 29 | ModAlt | |
| 25 | 30 | ModSuper | |
| 31 | + | ModCapsLock | |
| 32 | + | ModNumLock | |
| 26 | 33 | ) | |
| @@ -15,7 +15,12 @@ const ( | |||||
| 15 | InputReset // platform dropped queued input; treat held state as released | 15 | InputReset // platform dropped queued input; treat held state as released | ||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
| 18 | - | // Modifiers is a bit set of keyboard modifiers held during a key event. | 18 | + | // Modifiers is a bit set of keyboard modifiers held during a key or pointer |
| 19 | + | // event. ModShift, ModCtrl, ModAlt and ModSuper report held keys; ModCapsLock | |||
| 20 | + | // and ModNumLock report the latched lock state. The Renderer reads only | |||
| 21 | + | // ModShift and ModCtrl, so lock bits never change shortcuts or text entry. | |||
| 22 | + | // The Renderer does not expose modifiers to views: an application that shows a | |||
| 23 | + | // lock indicator records the lock bits of Input.Modifiers in its own model. | |||
| 19 | type Modifiers uint8 | 24 | type Modifiers uint8 | ||
| 20 | 25 | ||||
| 21 | const ( | 26 | const ( | ||
| @@ -23,4 +28,6 @@ const ( | |||||
| 23 | ModCtrl | 28 | ModCtrl | ||
| 24 | ModAlt | 29 | ModAlt | ||
| 25 | ModSuper | 30 | ModSuper | ||
| 31 | + | ModCapsLock | |||
| 32 | + | ModNumLock | |||
| 26 | ) | 33 | ) | ||
M internal/ui/renderer_test.go +92 -13
| @@ -4,6 +4,7 @@ package ui | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "errors" | |
| 7 | + | "fmt" | |
| 7 | 8 | "testing" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/nefergui/internal/layout" | |
| @@ -174,21 +175,9 @@ func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) { | |||
| 174 | 175 | } | |
| 175 | 176 | ||
| 176 | 177 | func TestRendererTextInputUsesTextBytes(t *testing.T) { | |
| 177 | - | r, tg := newTestRenderer(t) | |
| 178 | 178 | value := "" | |
| 179 | - | var out Output | |
| 180 | 179 | view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) } | |
| 181 | - | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | |
| 182 | - | r.Resize(200, 100, 1) | |
| 183 | - | drawOK(tg, nil).Maybe() | |
| 184 | - | settle := func() { | |
| 185 | - | for i := 0; i < 3; i++ { | |
| 186 | - | if _, err := r.Render(&out, &value, view); err != nil { | |
| 187 | - | t.Fatal(err) | |
| 188 | - | } | |
| 189 | - | } | |
| 190 | - | } | |
| 191 | - | settle() | |
| 180 | + | r, settle := settledRenderer(t, &value, view) | |
| 192 | 181 | r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true}) | |
| 193 | 182 | r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) | |
| 194 | 183 | settle() | |
| @@ -275,3 +264,93 @@ func TestTextIntAndMasked(t *testing.T) { | |||
| 275 | 264 | t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text) | |
| 276 | 265 | } | |
| 277 | 266 | } | |
| 267 | + | ||
| 268 | + | // settledRenderer returns a 200x100 renderer for view and a settle function | |
| 269 | + | // that renders until queued input has been applied. | |
| 270 | + | func settledRenderer[T any](t *testing.T, model *T, view func(*Frame, *T)) (*Renderer, func()) { | |
| 271 | + | t.Helper() | |
| 272 | + | r, tg := newTestRenderer(t) | |
| 273 | + | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | |
| 274 | + | r.Resize(200, 100, 1) | |
| 275 | + | drawOK(tg, nil).Maybe() | |
| 276 | + | var out Output | |
| 277 | + | settle := func() { | |
| 278 | + | for i := 0; i < 3; i++ { | |
| 279 | + | if _, err := r.Render(&out, model, view); err != nil { | |
| 280 | + | t.Fatal(err) | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | } | |
| 284 | + | settle() | |
| 285 | + | return r, settle | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | func TestModifierBits(t *testing.T) { | |
| 289 | + | // The values match neferclient's, so applications copy the bits as is. | |
| 290 | + | if ModShift != 1 || ModCtrl != 2 || ModAlt != 4 || ModSuper != 8 || ModCapsLock != 16 || ModNumLock != 32 { | |
| 291 | + | t.Fatal("modifier bit values changed") | |
| 292 | + | } | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // Caps Lock and Num Lock must not change shortcuts, selection or activation. | |
| 296 | + | func TestRendererLockModifiersDoNotChangeInput(t *testing.T) { | |
| 297 | + | inputView := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) } | |
| 298 | + | key := func(r *Renderer, keysym uint32, text string, mods Modifiers) { | |
| 299 | + | in := &Input{Kind: InputKey, Keysym: keysym, Pressed: true, Modifiers: mods} | |
| 300 | + | if text != "" { | |
| 301 | + | in.Text = []byte(text) | |
| 302 | + | } | |
| 303 | + | r.Input(in) | |
| 304 | + | } | |
| 305 | + | click := func(r *Renderer, x float64, mods Modifiers) { | |
| 306 | + | r.Input(&Input{Kind: InputPointerPress, X: x, Y: 5, Button: 0x110, Pressed: true, Modifiers: mods}) | |
| 307 | + | r.Input(&Input{Kind: InputPointerRelease, X: x, Y: 5, Button: 0x110, Modifiers: mods}) | |
| 308 | + | } | |
| 309 | + | for _, lock := range []Modifiers{0, ModCapsLock, ModNumLock, ModCapsLock | ModNumLock} { | |
| 310 | + | t.Run(fmt.Sprintf("lock=%#x", uint8(lock)), func(t *testing.T) { | |
| 311 | + | t.Run("CtrlA and ShiftLeft", func(t *testing.T) { | |
| 312 | + | value := "hello" | |
| 313 | + | r, settle := settledRenderer(t, &value, inputView) | |
| 314 | + | click(r, 5, lock) | |
| 315 | + | settle() | |
| 316 | + | key(r, 'a', "a", ModCtrl|lock) // select all | |
| 317 | + | settle() | |
| 318 | + | key(r, 'X', "X", lock) | |
| 319 | + | settle() | |
| 320 | + | if value != "X" { | |
| 321 | + | t.Fatalf("Ctrl+A then text: value=%q, want X", value) | |
| 322 | + | } | |
| 323 | + | key(r, 0xff51, "", ModShift|lock) // extend selection left | |
| 324 | + | settle() | |
| 325 | + | key(r, 'y', "y", lock) | |
| 326 | + | settle() | |
| 327 | + | if value != "y" { | |
| 328 | + | t.Fatalf("Shift+Left then text: value=%q, want y", value) | |
| 329 | + | } | |
| 330 | + | }) | |
| 331 | + | t.Run("ShiftClick", func(t *testing.T) { | |
| 332 | + | value := "hello" | |
| 333 | + | r, settle := settledRenderer(t, &value, inputView) | |
| 334 | + | click(r, 1, lock) | |
| 335 | + | settle() | |
| 336 | + | click(r, 95, ModShift|lock) // select to the end | |
| 337 | + | settle() | |
| 338 | + | key(r, 'Z', "Z", lock) | |
| 339 | + | settle() | |
| 340 | + | if value != "Z" { | |
| 341 | + | t.Fatalf("Shift+click then text: value=%q, want Z", value) | |
| 342 | + | } | |
| 343 | + | }) | |
| 344 | + | t.Run("TabReturn", func(t *testing.T) { | |
| 345 | + | m := testModel{} | |
| 346 | + | r, settle := settledRenderer(t, &m, testView) | |
| 347 | + | key(r, 0xff09, "", lock) | |
| 348 | + | key(r, 0xff0d, "", lock) | |
| 349 | + | settle() | |
| 350 | + | if m.clicks != 1 { | |
| 351 | + | t.Fatalf("Tab+Return clicks=%d, want 1", m.clicks) | |
| 352 | + | } | |
| 353 | + | }) | |
| 354 | + | }) | |
| 355 | + | } | |
| 356 | + | } | |
| @@ -4,6 +4,7 @@ package ui | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | + | "fmt" | |||
| 7 | "testing" | 8 | "testing" | ||
| 8 | 9 | ||||
| 9 | "github.com/bnema/nefergui/internal/layout" | 10 | "github.com/bnema/nefergui/internal/layout" | ||
| @@ -174,21 +175,9 @@ func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) { | |||||
| 174 | } | 175 | } | ||
| 175 | 176 | ||||
| 176 | func TestRendererTextInputUsesTextBytes(t *testing.T) { | 177 | func TestRendererTextInputUsesTextBytes(t *testing.T) { | ||
| 177 | - | r, tg := newTestRenderer(t) | |||
| 178 | value := "" | 178 | value := "" | ||
| 179 | - | var out Output | |||
| 180 | view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) } | 179 | view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) } | ||
| 181 | - | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | 180 | + | r, settle := settledRenderer(t, &value, view) |
| 182 | - | r.Resize(200, 100, 1) | |||
| 183 | - | drawOK(tg, nil).Maybe() | |||
| 184 | - | settle := func() { | |||
| 185 | - | for i := 0; i < 3; i++ { | |||
| 186 | - | if _, err := r.Render(&out, &value, view); err != nil { | |||
| 187 | - | t.Fatal(err) | |||
| 188 | - | } | |||
| 189 | - | } | |||
| 190 | - | } | |||
| 191 | - | settle() | |||
| 192 | r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true}) | 181 | r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true}) | ||
| 193 | r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) | 182 | r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) | ||
| 194 | settle() | 183 | settle() | ||
| @@ -275,3 +264,93 @@ func TestTextIntAndMasked(t *testing.T) { | |||||
| 275 | t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text) | 264 | t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text) | ||
| 276 | } | 265 | } | ||
| 277 | } | 266 | } | ||
| 267 | + | ||||
| 268 | + | // settledRenderer returns a 200x100 renderer for view and a settle function | |||
| 269 | + | // that renders until queued input has been applied. | |||
| 270 | + | func settledRenderer[T any](t *testing.T, model *T, view func(*Frame, *T)) (*Renderer, func()) { | |||
| 271 | + | t.Helper() | |||
| 272 | + | r, tg := newTestRenderer(t) | |||
| 273 | + | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | |||
| 274 | + | r.Resize(200, 100, 1) | |||
| 275 | + | drawOK(tg, nil).Maybe() | |||
| 276 | + | var out Output | |||
| 277 | + | settle := func() { | |||
| 278 | + | for i := 0; i < 3; i++ { | |||
| 279 | + | if _, err := r.Render(&out, model, view); err != nil { | |||
| 280 | + | t.Fatal(err) | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | } | |||
| 284 | + | settle() | |||
| 285 | + | return r, settle | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | func TestModifierBits(t *testing.T) { | |||
| 289 | + | // The values match neferclient's, so applications copy the bits as is. | |||
| 290 | + | if ModShift != 1 || ModCtrl != 2 || ModAlt != 4 || ModSuper != 8 || ModCapsLock != 16 || ModNumLock != 32 { | |||
| 291 | + | t.Fatal("modifier bit values changed") | |||
| 292 | + | } | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | // Caps Lock and Num Lock must not change shortcuts, selection or activation. | |||
| 296 | + | func TestRendererLockModifiersDoNotChangeInput(t *testing.T) { | |||
| 297 | + | inputView := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) } | |||
| 298 | + | key := func(r *Renderer, keysym uint32, text string, mods Modifiers) { | |||
| 299 | + | in := &Input{Kind: InputKey, Keysym: keysym, Pressed: true, Modifiers: mods} | |||
| 300 | + | if text != "" { | |||
| 301 | + | in.Text = []byte(text) | |||
| 302 | + | } | |||
| 303 | + | r.Input(in) | |||
| 304 | + | } | |||
| 305 | + | click := func(r *Renderer, x float64, mods Modifiers) { | |||
| 306 | + | r.Input(&Input{Kind: InputPointerPress, X: x, Y: 5, Button: 0x110, Pressed: true, Modifiers: mods}) | |||
| 307 | + | r.Input(&Input{Kind: InputPointerRelease, X: x, Y: 5, Button: 0x110, Modifiers: mods}) | |||
| 308 | + | } | |||
| 309 | + | for _, lock := range []Modifiers{0, ModCapsLock, ModNumLock, ModCapsLock | ModNumLock} { | |||
| 310 | + | t.Run(fmt.Sprintf("lock=%#x", uint8(lock)), func(t *testing.T) { | |||
| 311 | + | t.Run("CtrlA and ShiftLeft", func(t *testing.T) { | |||
| 312 | + | value := "hello" | |||
| 313 | + | r, settle := settledRenderer(t, &value, inputView) | |||
| 314 | + | click(r, 5, lock) | |||
| 315 | + | settle() | |||
| 316 | + | key(r, 'a', "a", ModCtrl|lock) // select all | |||
| 317 | + | settle() | |||
| 318 | + | key(r, 'X', "X", lock) | |||
| 319 | + | settle() | |||
| 320 | + | if value != "X" { | |||
| 321 | + | t.Fatalf("Ctrl+A then text: value=%q, want X", value) | |||
| 322 | + | } | |||
| 323 | + | key(r, 0xff51, "", ModShift|lock) // extend selection left | |||
| 324 | + | settle() | |||
| 325 | + | key(r, 'y', "y", lock) | |||
| 326 | + | settle() | |||
| 327 | + | if value != "y" { | |||
| 328 | + | t.Fatalf("Shift+Left then text: value=%q, want y", value) | |||
| 329 | + | } | |||
| 330 | + | }) | |||
| 331 | + | t.Run("ShiftClick", func(t *testing.T) { | |||
| 332 | + | value := "hello" | |||
| 333 | + | r, settle := settledRenderer(t, &value, inputView) | |||
| 334 | + | click(r, 1, lock) | |||
| 335 | + | settle() | |||
| 336 | + | click(r, 95, ModShift|lock) // select to the end | |||
| 337 | + | settle() | |||
| 338 | + | key(r, 'Z', "Z", lock) | |||
| 339 | + | settle() | |||
| 340 | + | if value != "Z" { | |||
| 341 | + | t.Fatalf("Shift+click then text: value=%q, want Z", value) | |||
| 342 | + | } | |||
| 343 | + | }) | |||
| 344 | + | t.Run("TabReturn", func(t *testing.T) { | |||
| 345 | + | m := testModel{} | |||
| 346 | + | r, settle := settledRenderer(t, &m, testView) | |||
| 347 | + | key(r, 0xff09, "", lock) | |||
| 348 | + | key(r, 0xff0d, "", lock) | |||
| 349 | + | settle() | |||
| 350 | + | if m.clicks != 1 { | |||
| 351 | + | t.Fatalf("Tab+Return clicks=%d, want 1", m.clicks) | |||
| 352 | + | } | |||
| 353 | + | }) | |||
| 354 | + | }) | |||
| 355 | + | } | |||
| 356 | + | } | |||
M nefergui.go +5 -1
| @@ -92,7 +92,8 @@ func Password(v bool) EditOption { return ui.Password(v) } | |||
| 92 | 92 | // Frame.SetInputRects and Output.InputRects. | |
| 93 | 93 | type Rect = ui.Rect | |
| 94 | 94 | ||
| 95 | - | // InputKind classifies an Input; Modifiers is a bit set of held modifiers. | |
| 95 | + | // InputKind classifies an Input; Modifiers is a bit set of held modifiers and | |
| 96 | + | // latched Caps Lock and Num Lock state. | |
| 96 | 97 | type ( | |
| 97 | 98 | InputKind = ui.InputKind | |
| 98 | 99 | Modifiers = ui.Modifiers | |
| @@ -113,4 +114,7 @@ const ( | |||
| 113 | 114 | ModCtrl = ui.ModCtrl | |
| 114 | 115 | ModAlt = ui.ModAlt | |
| 115 | 116 | ModSuper = ui.ModSuper | |
| 117 | + | ||
| 118 | + | ModCapsLock = ui.ModCapsLock | |
| 119 | + | ModNumLock = ui.ModNumLock | |
| 116 | 120 | ) | |
| @@ -92,7 +92,8 @@ func Password(v bool) EditOption { return ui.Password(v) } | |||||
| 92 | // Frame.SetInputRects and Output.InputRects. | 92 | // Frame.SetInputRects and Output.InputRects. | ||
| 93 | type Rect = ui.Rect | 93 | type Rect = ui.Rect | ||
| 94 | 94 | ||||
| 95 | - | // InputKind classifies an Input; Modifiers is a bit set of held modifiers. | 95 | + | // InputKind classifies an Input; Modifiers is a bit set of held modifiers and |
| 96 | + | // latched Caps Lock and Num Lock state. | |||
| 96 | type ( | 97 | type ( | ||
| 97 | InputKind = ui.InputKind | 98 | InputKind = ui.InputKind | ||
| 98 | Modifiers = ui.Modifiers | 99 | Modifiers = ui.Modifiers | ||
| @@ -113,4 +114,7 @@ const ( | |||||
| 113 | ModCtrl = ui.ModCtrl | 114 | ModCtrl = ui.ModCtrl | ||
| 114 | ModAlt = ui.ModAlt | 115 | ModAlt = ui.ModAlt | ||
| 115 | ModSuper = ui.ModSuper | 116 | ModSuper = ui.ModSuper | ||
| 117 | + | ||||
| 118 | + | ModCapsLock = ui.ModCapsLock | |||
| 119 | + | ModNumLock = ui.ModNumLock | |||
| 116 | ) | 120 | ) | ||