diff --git a/app/internal/xkb/xkb_unix.go b/app/internal/xkb/xkb_unix.go index 54ba604a..9230e712 100644 --- a/app/internal/xkb/xkb_unix.go +++ b/app/internal/xkb/xkb_unix.go @@ -238,17 +238,17 @@ func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latched C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) } -func convertKeysym(s C.xkb_keysym_t) (string, bool) { +func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) { if 'a' <= s && s <= 'z' { - return string(rune(s - 'a' + 'A')), true + return key.Name(rune(s - 'a' + 'A')), true } if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 { - return string(rune(s - C.XKB_KEY_KP_0 + '0')), true + return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true } if ' ' < s && s <= '~' { - return string(rune(s)), true + return key.Name(rune(s)), true } - var n string + var n key.Name switch s { case C.XKB_KEY_Escape: n = key.NameEscape diff --git a/app/os_android.go b/app/os_android.go index f7962063..079c70ab 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -899,8 +899,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { f(env) } -func convertKeyCode(code C.jint) (string, bool) { - var n string +func convertKeyCode(code C.jint) (key.Name, bool) { + var n key.Name switch code { case C.AKEYCODE_FORWARD_DEL: n = key.NameDeleteForward diff --git a/app/os_ios.go b/app/os_ios.go index e9d8ea06..55ee8faa 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -310,7 +310,7 @@ func (w *window) SetCursor(cursor pointer.Cursor) { w.cursor = windowSetCursor(w.cursor, cursor) } -func (w *window) onKeyCommand(name string) { +func (w *window) onKeyCommand(name key.Name) { w.w.Event(key.Event{ Name: name, }) diff --git a/app/os_js.go b/app/os_js.go index e59e33da..29f38b53 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -752,8 +752,8 @@ func osMain() { select {} } -func translateKey(k string) (string, bool) { - var n string +func translateKey(k string) (key.Name, bool) { + var n key.Name switch k { case "ArrowUp": @@ -820,7 +820,7 @@ func translateKey(k string) (string, bool) { r, s := utf8.DecodeRuneInString(k) // If there is exactly one printable character, return that. if s == len(k) && unicode.IsPrint(r) { - return strings.ToUpper(k), true + return key.Name(strings.ToUpper(k)), true } return "", false } diff --git a/app/os_macos.go b/app/os_macos.go index 42327229..3b7982bd 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -920,8 +920,8 @@ func osMain() { C.gio_main() } -func convertKey(k rune) (string, bool) { - var n string +func convertKey(k rune) (key.Name, bool) { + var n key.Name switch k { case 0x1b: n = key.NameEscape @@ -982,7 +982,7 @@ func convertKey(k rune) (string, bool) { if !unicode.IsPrint(k) { return "", false } - n = string(k) + n = key.Name(k) } return n, true } diff --git a/app/os_windows.go b/app/os_windows.go index 625abc56..017c80c5 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -870,11 +870,11 @@ func (w *window) raise() { windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW) } -func convertKeyCode(code uintptr) (string, bool) { +func convertKeyCode(code uintptr) (key.Name, bool) { if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { - return string(rune(code)), true + return key.Name(rune(code)), true } - var r string + var r key.Name switch code { case windows.VK_ESCAPE: diff --git a/app/window.go b/app/window.go index 6fd72de3..aa99df69 100644 --- a/app/window.go +++ b/app/window.go @@ -903,11 +903,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool { handled = false } } - // As a special case, the top-most input handler receives all unhandled - // events. - if !handled { - handled = w.queue.QueueTopmost(e) - } } w.updateCursor(d) if handled { diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 3fa6081d..78c1da35 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -63,7 +63,6 @@ const ( TypePass TypePopPass TypeInput - TypeKeyInput TypeKeyInputHint TypeSave TypeLoad @@ -140,7 +139,6 @@ const ( TypePassLen = 1 TypePopPassLen = 1 TypeInputLen = 1 - TypeKeyInputLen = 1 TypeKeyInputHintLen = 1 + 1 TypeSaveLen = 1 + 4 TypeLoadLen = 1 + 4 @@ -415,7 +413,6 @@ var opProps = [0x100]opProp{ TypePass: {Size: TypePassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypeInput: {Size: TypeInputLen, NumRefs: 1}, - TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1}, TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, @@ -478,8 +475,6 @@ func (t OpType) String() string { return "PopPass" case TypeInput: return "Input" - case TypeKeyInput: - return "KeyInput" case TypeKeyInputHint: return "KeyInputHint" case TypeSave: diff --git a/io/input/key.go b/io/input/key.go index 2f217053..cdecb3f6 100644 --- a/io/input/key.go +++ b/io/input/key.go @@ -39,10 +39,11 @@ type keyHandler struct { visible bool new bool focusable bool + active bool hint key.InputHint order int dirOrder int - filter key.Set + filters []key.Filter trans f32.Affine2D } @@ -116,6 +117,7 @@ func (q *keyQueue) Frame(events *handlerEvents) { h.new = false h.visible = false h.focusable = false + h.active = false } q.updateFocusLayout() } @@ -255,7 +257,25 @@ func (q *keyQueue) AreaFor(t event.Tag) int { } func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool { - return q.handlers[t].filter.Contains(e.Name, e.Modifiers) + for _, f := range q.handlers[t].filters { + if keyFilterMatch(f, e) { + return true + } + } + return false +} + +func keyFilterMatch(f key.Filter, e key.Event) bool { + if f.Name != e.Name { + return false + } + if e.Modifiers&f.Required != f.Required { + return false + } + if e.Modifiers&^(f.Required|f.Optional) != 0 { + return false + } + return true } func (q *keyQueue) Focus(focus event.Tag, events *handlerEvents) { @@ -288,6 +308,15 @@ func (q *keyQueue) softKeyboard(show bool) { } } +func (q *keyQueue) filter(tag event.Tag, f key.Filter) { + h := q.handlerFor(tag) + if !h.active { + h.active = true + h.filters = h.filters[:0] + } + h.filters = append(h.filters, f) +} + func (q *keyQueue) focusable(tag event.Tag) { h := q.handlerFor(tag) h.focusable = true @@ -305,14 +334,13 @@ func (q *keyQueue) handlerFor(tag event.Tag) *keyHandler { return h } -func (q *keyQueue) inputOp(op key.InputOp, t f32.Affine2D, area int, bounds image.Rectangle) { - h := q.handlerFor(op.Tag) +func (q *keyQueue) inputOp(tag event.Tag, t f32.Affine2D, area int, bounds image.Rectangle) { + h := q.handlerFor(tag) if h.order == -1 { h.order = len(q.order) - q.order = append(q.order, op.Tag) - q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: op.Tag, area: area, bounds: bounds}) + q.order = append(q.order, tag) + q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds}) } - h.filter = op.Keys h.visible = true h.trans = t } diff --git a/io/input/key_test.go b/io/input/key_test.go index a40126df..12e277f0 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -15,10 +15,10 @@ import ( "gioui.org/op/clip" ) -func TestKeyWakeup(t *testing.T) { +func TestInputWakeup(t *testing.T) { handler := new(int) var ops op.Ops - key.InputOp{Tag: handler}.Add(&ops) + event.InputOp(&ops, handler) var r Router // Test that merely adding a handler doesn't trigger redraw. @@ -39,12 +39,12 @@ func TestKeyMultiples(t *testing.T) { r := new(Router) r.Source().Queue(key.SoftKeyboardCmd{Show: true}) - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) r.Source().Queue(key.FocusCmd{Tag: &handlers[2]}) - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) // The last one must be focused: - key.InputOp{Tag: &handlers[2]}.Add(ops) + event.InputOp(ops, &handlers[2]) for i := range handlers { r.Events(&handlers[i], key.FocusFilter{}) @@ -64,14 +64,14 @@ func TestKeyStacked(t *testing.T) { ops := new(op.Ops) r := new(Router) - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) r.Source().Queue(key.FocusCmd{}) r.Source().Queue(key.SoftKeyboardCmd{Show: false}) - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) r.Source().Queue(key.FocusCmd{Tag: &handlers[1]}) - key.InputOp{Tag: &handlers[2]}.Add(ops) + event.InputOp(ops, &handlers[2]) r.Source().Queue(key.SoftKeyboardCmd{Show: true}) - key.InputOp{Tag: &handlers[3]}.Add(ops) + event.InputOp(ops, &handlers[3]) for i := range handlers { r.Events(&handlers[i], key.FocusFilter{}) @@ -107,35 +107,39 @@ func TestKeyRemoveFocus(t *testing.T) { r := new(Router) // New InputOp with Focus and Keyboard: - key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops) + event.InputOp(ops, &handlers[0]) r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) r.Source().Queue(key.SoftKeyboardCmd{Show: true}) // New InputOp without any focus: - key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops) + event.InputOp(ops, &handlers[1]) + filters := []event.Filter{ + key.FocusFilter{}, + key.Filter{Name: key.NameTab, Required: key.ModShortcut}, + } for i := range handlers { - r.Events(&handlers[i], key.FocusFilter{}) + r.Events(&handlers[i], filters...) } r.Frame(ops) // Add some key events: - event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}) - r.Queue(event) + evt := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}) + r.Queue(evt) - assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), true, event) - assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[0], filters...), true, evt) + assertKeyEvent(t, r.Events(&handlers[1], filters...), false) assertFocus(t, r, &handlers[0]) assertKeyboard(t, r, TextInputOpen) ops.Reset() // Will get the focus removed: - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) // Unchanged: - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) // Remove focus by focusing on a tag that don't exist. r.Source().Queue(key.FocusCmd{Tag: new(int)}) @@ -148,9 +152,8 @@ func TestKeyRemoveFocus(t *testing.T) { ops.Reset() - key.InputOp{Tag: &handlers[0]}.Add(ops) - - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[0]) + event.InputOp(ops, &handlers[1]) r.Frame(ops) @@ -164,11 +167,11 @@ func TestKeyRemoveFocus(t *testing.T) { // Set focus to InputOp which already // exists in the previous frame: r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) r.Source().Queue(key.SoftKeyboardCmd{Show: true}) // Remove focus. - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) r.Source().Queue(key.FocusCmd{}) r.Frame(ops) @@ -185,11 +188,11 @@ func TestKeyFocusedInvisible(t *testing.T) { // Set new InputOp with focus: r.Source().Queue(key.FocusCmd{Tag: &handlers[0]}) - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) r.Source().Queue(key.SoftKeyboardCmd{Show: true}) // Set new InputOp without focus: - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) for i := range handlers { r.Events(&handlers[i], key.FocusFilter{}) @@ -209,7 +212,7 @@ func TestKeyFocusedInvisible(t *testing.T) { // // Unchanged: - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) r.Frame(ops) @@ -221,7 +224,7 @@ func TestKeyFocusedInvisible(t *testing.T) { r.Frame(ops) // Unchanged - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) r.Frame(ops) @@ -229,10 +232,10 @@ func TestKeyFocusedInvisible(t *testing.T) { // Respawn the first element: // It must receive one `Event{Focus: false}`. - key.InputOp{Tag: &handlers[0]}.Add(ops) + event.InputOp(ops, &handlers[0]) // Unchanged - key.InputOp{Tag: &handlers[1]}.Add(ops) + event.InputOp(ops, &handlers[1]) for i := range handlers { r.Events(&handlers[i], key.FocusFilter{}) @@ -263,7 +266,7 @@ func TestDirectionalFocus(t *testing.T) { for i, bounds := range handlers { cl := clip.Rect(bounds).Push(ops) - key.InputOp{Tag: &handlers[i]}.Add(ops) + event.InputOp(ops, &handlers[i]) cl.Pop() r.Events(&handlers[i], key.FocusFilter{}) } @@ -307,7 +310,6 @@ func TestFocusScroll(t *testing.T) { r.Events(h, filters...) parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops) cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops) - key.InputOp{Tag: h}.Add(ops) event.InputOp(ops, h) // Test that h is scrolled even if behind another handler. event.InputOp(ops, new(int)) @@ -334,7 +336,6 @@ func TestFocusClick(t *testing.T) { } assertEventPointerTypeSequence(t, r.Events(h, filters...), pointer.Cancel) cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops) - key.InputOp{Tag: h}.Add(ops) event.InputOp(ops, h) cl.Pop() r.Frame(ops) @@ -359,21 +360,31 @@ func TestKeyRouting(t *testing.T) { rect := clip.Rect{Max: image.Pt(10, 10)} macro := op.Record(macroOps) - key.InputOp{Tag: &handlers[0], Keys: "A"}.Add(ops) + event.InputOp(ops, &handlers[0]) cl1 := rect.Push(ops) - key.InputOp{Tag: &handlers[1], Keys: "B"}.Add(ops) - key.InputOp{Tag: &handlers[2], Keys: "A"}.Add(ops) + event.InputOp(ops, &handlers[1]) + event.InputOp(ops, &handlers[2]) cl1.Pop() cl2 := rect.Push(ops) - key.InputOp{Tag: &handlers[3]}.Add(ops) - key.InputOp{Tag: &handlers[4], Keys: "A"}.Add(ops) + event.InputOp(ops, &handlers[3]) + event.InputOp(ops, &handlers[4]) cl2.Pop() call := macro.Stop() call.Add(ops) - for i := range handlers { - r.Events(&handlers[i], key.FocusFilter{}) + fa := []event.Filter{ + key.FocusFilter{}, + key.Filter{Name: "A"}, } + fb := []event.Filter{ + key.FocusFilter{}, + key.Filter{Name: "B"}, + } + r.Events(&handlers[0], fa...) + r.Events(&handlers[1], fb...) + r.Events(&handlers[2], fa...) + r.Events(&handlers[3], key.FocusFilter{}) + r.Events(&handlers[4], fa...) r.Frame(ops) @@ -382,17 +393,19 @@ func TestKeyRouting(t *testing.T) { // With no focus, the events should traverse the final branch of the hit tree // searching for handlers. - assertKeyEvent(t, r.Events(&handlers[4], key.FocusFilter{}), false, A) + assertKeyEvent(t, r.Events(&handlers[4], fa...), false, A) assertKeyEvent(t, r.Events(&handlers[3], key.FocusFilter{}), false) - assertKeyEvent(t, r.Events(&handlers[2], key.FocusFilter{}), false) - assertKeyEvent(t, r.Events(&handlers[1], key.FocusFilter{}), false, B) - assertKeyEvent(t, r.Events(&handlers[0], key.FocusFilter{}), false) + assertKeyEvent(t, r.Events(&handlers[2], fa...), false) + assertKeyEvent(t, r.Events(&handlers[1], fb...), false, B) + assertKeyEvent(t, r.Events(&handlers[0], fa...), false) r2 := new(Router) - for i := range handlers { - r2.Events(&handlers[i], key.FocusFilter{}) - } + r2.Events(&handlers[0], fa...) + r2.Events(&handlers[1], fb...) + r2.Events(&handlers[2], fa...) + r2.Events(&handlers[3], key.FocusFilter{}) + r2.Events(&handlers[4], fa...) r2.Source().Queue(key.FocusCmd{Tag: &handlers[3]}) r2.Frame(ops) @@ -401,11 +414,11 @@ func TestKeyRouting(t *testing.T) { // With focus, the events should traverse the branch of the hit tree // containing the focused element. - assertKeyEvent(t, r2.Events(&handlers[4], key.FocusFilter{}), false) + assertKeyEvent(t, r2.Events(&handlers[4], fa...), false) assertKeyEvent(t, r2.Events(&handlers[3], key.FocusFilter{}), true) - assertKeyEvent(t, r2.Events(&handlers[2], key.FocusFilter{}), false) - assertKeyEvent(t, r2.Events(&handlers[1], key.FocusFilter{}), false) - assertKeyEvent(t, r2.Events(&handlers[0], key.FocusFilter{}), false, A) + assertKeyEvent(t, r2.Events(&handlers[2], fa...), false) + assertKeyEvent(t, r2.Events(&handlers[1], fb...), false) + assertKeyEvent(t, r2.Events(&handlers[0], fa...), false, A) } func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus bool, expectedInputs ...event.Event) { diff --git a/io/input/pointer.go b/io/input/pointer.go index 7e774e00..758cd5a4 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -10,7 +10,6 @@ import ( f32internal "gioui.org/internal/f32" "gioui.org/internal/ops" "gioui.org/io/event" - "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/semantic" "gioui.org/io/system" @@ -43,7 +42,6 @@ type hitNode struct { // For handler nodes. tag event.Tag - ktag event.Tag pass bool } @@ -262,15 +260,6 @@ func (q *pointerQueue) handlerFor(tag event.Tag, events *handlerEvents) *pointer return h } -func (c *pointerCollector) keyInputOp(op key.InputOp) { - areaID := c.currentArea() - c.addHitNode(hitNode{ - area: areaID, - ktag: op.Tag, - pass: true, - }) -} - func (c *pointerCollector) actionInputOp(act system.Action) { areaID := c.currentArea() area := &c.q.areas[areaID] diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index 1242949e..1af7b936 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -1008,7 +1008,7 @@ func TestDeferredInputOp(t *testing.T) { var r Router m := op.Record(&ops) - key.InputOp{Tag: new(int)}.Add(&ops) + event.InputOp(&ops, new(int)) call := m.Stop() op.Defer(&ops, call) diff --git a/io/input/router.go b/io/input/router.go index 9d242028..d5aafb9c 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -131,6 +131,8 @@ func (s Source) Events(k event.Tag, filters ...event.Filter) []event.Event { func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event { for _, f := range filters { switch f := f.(type) { + case key.Filter: + q.key.queue.filter(k, f) case key.FocusFilter: q.key.queue.focusable(k) case pointer.Filter: @@ -167,25 +169,6 @@ func (q *Router) Frame(frame *op.Ops) { } } -// Queue key events to the topmost handler. -func (q *Router) QueueTopmost(events ...key.Event) bool { - var topmost event.Tag - pq := &q.pointer.queue - for _, h := range pq.hitTree { - if h.ktag != nil { - topmost = h.ktag - break - } - } - if topmost == nil { - return false - } - for _, e := range events { - q.handlers.Add(topmost, e) - } - return q.handlers.HadEvents() -} - // Queue events and report whether at least one handler had an event queued. func (q *Router) Queue(events ...event.Event) bool { for _, e := range events { @@ -273,7 +256,7 @@ func (q *Router) queueKeyEvent(e key.Event) { if focused { // If there is a focused tag, traverse its ancestry through the // hit tree to search for handlers. - for ; pq.hitTree[idx].ktag != f; idx-- { + for ; pq.hitTree[idx].tag != f; idx-- { } } for idx != -1 { @@ -283,11 +266,11 @@ func (q *Router) queueKeyEvent(e key.Event) { } else { idx-- } - if n.ktag == nil { + if n.tag == nil { continue } - if kq.Accepts(n.ktag, e) { - q.handlers.Add(n.ktag, e) + if kq.Accepts(n.tag, e) { + q.handlers.Add(n.tag, e) break } } @@ -479,6 +462,9 @@ func (q *Router) collect() { case ops.TypeInput: tag := encOp.Refs[0].(event.Tag) pc.inputOp(tag, &q.handlers) + a := pc.currentArea() + b := pc.currentAreaBounds() + kq.inputOp(tag, t, a, b) // Pointer ops. case ops.TypePass: @@ -491,17 +477,6 @@ func (q *Router) collect() { case ops.TypeActionInput: act := system.Action(encOp.Data[1]) pc.actionInputOp(act) - - case ops.TypeKeyInput: - filter := key.Set(*encOp.Refs[1].(*string)) - op := key.InputOp{ - Tag: encOp.Refs[0].(event.Tag), - Keys: filter, - } - a := pc.currentArea() - b := pc.currentAreaBounds() - pc.keyInputOp(op) - kq.inputOp(op, t, a, b) case ops.TypeKeyInputHint: op := key.InputHintOp{ Tag: encOp.Refs[0].(event.Tag), @@ -583,6 +558,14 @@ func (h *handlerEvents) Events(k event.Tag, filters ...event.Filter) []event.Eve func filtersMatches(filters []event.Filter, e event.Event) bool { switch e := e.(type) { + case key.Event: + for _, f := range filters { + if f, ok := f.(key.Filter); ok { + if keyFilterMatch(f, e) { + return true + } + } + } case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: for _, f := range filters { if _, ok := f.(key.FocusFilter); ok { @@ -614,8 +597,6 @@ func filtersMatches(filters []event.Filter, e event.Event) bool { return true } } - default: - return true } return false } diff --git a/io/key/key.go b/io/key/key.go index 6e812481..7848eb74 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -18,16 +18,15 @@ import ( "gioui.org/op" ) -// InputOp declares a handler ready for key events. -// Key events are in general only delivered to the -// focused key handler. -type InputOp struct { - Tag event.Tag - // Keys is the set of keys Tag can handle. That is, Tag will only - // receive an Event if its key and modifiers are accepted by Keys.Contains. - // As a special case, the topmost (first added) InputOp handler receives all - // unhandled events. - Keys Set +// Filter matches [Event]s. +type Filter struct { + // Required is the set of modifiers that must be included in events matched. + Required Modifiers + // Optional is the set of modifiers that may be included in events matched. + Optional Modifiers + // Name of the key to be matched. As a special case, the empty + // Name matches every key not matched by any other filter. + Name Name } // InputHintOp describes the type of text expected by a tag. @@ -54,22 +53,6 @@ type SnippetCmd struct { Snippet } -// Set is an expression that describes a set of key combinations, in the form -// "-|...". Modifiers are separated by dashes, optional -// modifiers are enclosed by parentheses. A key set is either a literal key -// name or a list of key names separated by commas and enclosed in brackets. -// -// The "Short" modifier matches the shortcut modifier (ModShortcut) and -// "ShortAlt" matches the alternative modifier (ModShortcutAlt). -// -// Examples: -// -// - A|B matches the A and B keys -// - [A,B] also matches the A and B keys -// - Shift-A matches A key if shift is pressed, and no other modifier. -// - Shift-(Ctrl)-A matches A if shift is pressed, and optionally ctrl. -type Set string - // Range represents a range of text, such as an editor's selection. // Start and End are in runes. type Range struct { @@ -110,11 +93,8 @@ type FocusEvent struct { // An Event is generated when a key is pressed. For text input // use EditEvent. type Event struct { - // Name of the key. For letters, the upper case form is used, via - // unicode.ToUpper. The shift modifier is taken into account, all other - // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" - // combinations both give the Name "!" with the US keyboard layout. - Name string + // Name of the key. + Name Name // Modifiers is the set of active modifiers when the key was pressed. Modifiers Modifiers // State is the state of the key when the event was fired. @@ -184,41 +164,49 @@ const ( ModSuper ) +// Name is the identifier for a keyboard key. +// +// For letters, the upper case form is used, via unicode.ToUpper. +// The shift modifier is taken into account, all other +// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" +// combinations both give the Name "!" with the US keyboard layout. +type Name string + const ( // Names for special keys. - NameLeftArrow = "←" - NameRightArrow = "→" - NameUpArrow = "↑" - NameDownArrow = "↓" - NameReturn = "⏎" - NameEnter = "⌤" - NameEscape = "⎋" - NameHome = "⇱" - NameEnd = "⇲" - NameDeleteBackward = "⌫" - NameDeleteForward = "⌦" - NamePageUp = "⇞" - NamePageDown = "⇟" - NameTab = "Tab" - NameSpace = "Space" - NameCtrl = "Ctrl" - NameShift = "Shift" - NameAlt = "Alt" - NameSuper = "Super" - NameCommand = "⌘" - NameF1 = "F1" - NameF2 = "F2" - NameF3 = "F3" - NameF4 = "F4" - NameF5 = "F5" - NameF6 = "F6" - NameF7 = "F7" - NameF8 = "F8" - NameF9 = "F9" - NameF10 = "F10" - NameF11 = "F11" - NameF12 = "F12" - NameBack = "Back" + NameLeftArrow Name = "←" + NameRightArrow Name = "→" + NameUpArrow Name = "↑" + NameDownArrow Name = "↓" + NameReturn Name = "⏎" + NameEnter Name = "⌤" + NameEscape Name = "⎋" + NameHome Name = "⇱" + NameEnd Name = "⇲" + NameDeleteBackward Name = "⌫" + NameDeleteForward Name = "⌦" + NamePageUp Name = "⇞" + NamePageDown Name = "⇟" + NameTab Name = "Tab" + NameSpace Name = "Space" + NameCtrl Name = "Ctrl" + NameShift Name = "Shift" + NameAlt Name = "Alt" + NameSuper Name = "Super" + NameCommand Name = "⌘" + NameF1 Name = "F1" + NameF2 Name = "F2" + NameF3 Name = "F3" + NameF4 Name = "F4" + NameF5 Name = "F5" + NameF6 Name = "F6" + NameF7 Name = "F7" + NameF8 Name = "F8" + NameF9 Name = "F9" + NameF10 Name = "F10" + NameF11 Name = "F11" + NameF12 Name = "F12" + NameBack Name = "Back" ) type FocusDirection int @@ -241,105 +229,10 @@ func (m Modifiers) Contain(m2 Modifiers) bool { // FocusCmd requests to set or clear the keyboard focus. type FocusCmd struct { // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag - // has no InputOp in the same frame. + // has no [event.Op] references. Tag event.Tag } -func (k Set) Contains(name string, mods Modifiers) bool { - ks := string(k) - for len(ks) > 0 { - // Cut next key expression. - chord, rest, _ := cut(ks, "|") - ks = rest - // Separate key set and modifier set. - var modSet, keySet string - sep := strings.LastIndex(chord, "-") - if sep != -1 { - modSet, keySet = chord[:sep], chord[sep+1:] - } else { - modSet, keySet = "", chord - } - if !keySetContains(keySet, name) { - continue - } - if modSetContains(modSet, mods) { - return true - } - } - return false -} - -func keySetContains(keySet, name string) bool { - // Check for single key match. - if keySet == name { - return true - } - // Check for set match. - if len(keySet) < 2 || keySet[0] != '[' || keySet[len(keySet)-1] != ']' { - return false - } - keySet = keySet[1 : len(keySet)-1] - for len(keySet) > 0 { - key, rest, _ := cut(keySet, ",") - keySet = rest - if key == name { - return true - } - } - return false -} - -func modSetContains(modSet string, mods Modifiers) bool { - var smods Modifiers - for len(modSet) > 0 { - mod, rest, _ := cut(modSet, "-") - modSet = rest - if len(mod) >= 2 && mod[0] == '(' && mod[len(mod)-1] == ')' { - mods &^= modFor(mod[1 : len(mod)-1]) - } else { - smods |= modFor(mod) - } - } - return mods == smods -} - -// cut is a copy of the standard library strings.Cut. -// TODO: remove when Go 1.18 is our minimum. -func cut(s, sep string) (before, after string, found bool) { - if i := strings.Index(s, sep); i >= 0 { - return s[:i], s[i+len(sep):], true - } - return s, "", false -} - -func modFor(name string) Modifiers { - switch name { - case NameCtrl: - return ModCtrl - case NameShift: - return ModShift - case NameAlt: - return ModAlt - case NameSuper: - return ModSuper - case NameCommand: - return ModCommand - case "Short": - return ModShortcut - case "ShortAlt": - return ModShortcutAlt - } - return 0 -} - -func (h InputOp) Add(o *op.Ops) { - if h.Tag == nil { - panic("Tag must be non-nil") - } - data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys)) - data[0] = byte(ops.TypeKeyInput) -} - func (h InputHintOp) Add(o *op.Ops) { if h.Tag == nil { panic("Tag must be non-nil") @@ -360,24 +253,25 @@ func (SoftKeyboardCmd) ImplementsCommand() {} func (SelectionCmd) ImplementsCommand() {} func (SnippetCmd) ImplementsCommand() {} +func (Filter) ImplementsFilter() {} func (FocusFilter) ImplementsFilter() {} func (m Modifiers) String() string { var strs []string if m.Contain(ModCtrl) { - strs = append(strs, NameCtrl) + strs = append(strs, string(NameCtrl)) } if m.Contain(ModCommand) { - strs = append(strs, NameCommand) + strs = append(strs, string(NameCommand)) } if m.Contain(ModShift) { - strs = append(strs, NameShift) + strs = append(strs, string(NameShift)) } if m.Contain(ModAlt) { - strs = append(strs, NameAlt) + strs = append(strs, string(NameAlt)) } if m.Contain(ModSuper) { - strs = append(strs, NameSuper) + strs = append(strs, string(NameSuper)) } return strings.Join(strs, "-") } diff --git a/io/key/key_test.go b/io/key/key_test.go deleted file mode 100644 index aa01c40f..00000000 --- a/io/key/key_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package key - -import ( - "testing" -) - -func TestKeySet(t *testing.T) { - const allMods = ModAlt | ModShift | ModSuper | ModCtrl | ModCommand - tests := []struct { - Set Set - Matches []Event - Mismatches []Event - }{ - {"A", []Event{{Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}}, - {"[A,B,C]", []Event{{Name: "A"}, {Name: "B"}}, []Event{}}, - {"Short-A", []Event{{Name: "A", Modifiers: ModShortcut}}, []Event{{Name: "A", Modifiers: ModShift}}}, - {"(Ctrl)-A", []Event{{Name: "A", Modifiers: ModCtrl}, {Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}}, - {"Shift-[A,B,C]", []Event{{Name: "A", Modifiers: ModShift}}, []Event{{Name: "B", Modifiers: ModShift | ModCtrl}}}, - {Set(allMods.String() + "-A"), []Event{{Name: "A", Modifiers: allMods}}, []Event{}}, - } - for _, tst := range tests { - for _, e := range tst.Matches { - if !tst.Set.Contains(e.Name, e.Modifiers) { - t.Errorf("key set %q didn't contain %+v", tst.Set, e) - } - } - for _, e := range tst.Mismatches { - if tst.Set.Contains(e.Name, e.Modifiers) { - t.Errorf("key set %q contains %+v", tst.Set, e) - } - } - } -} diff --git a/widget/button.go b/widget/button.go index 7eb2edbe..e4141083 100644 --- a/widget/button.go +++ b/widget/button.go @@ -7,6 +7,7 @@ import ( "time" "gioui.org/gesture" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/semantic" @@ -25,7 +26,7 @@ type Clickable struct { keyTag struct{} requestClicks int focused bool - pressedKey string + pressedKey key.Name } // Click represents a click. @@ -102,13 +103,7 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) b.click.Add(gtx.Ops) - if gtx.Enabled() { - keys := key.Set("⏎|Space") - if !b.focused { - keys = "" - } - key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops) - } + event.InputOp(gtx.Ops, &b.keyTag) c.Add(gtx.Ops) return dims } @@ -162,7 +157,13 @@ func (b *Clickable) Update(gtx layout.Context) []Click { }) } } - for _, e := range gtx.Events(&b.keyTag, key.FocusFilter{}) { + filters := []event.Filter{ + key.FocusFilter{}, + } + if b.focused { + filters = append(filters, key.Filter{Name: key.NameReturn}, key.Filter{Name: key.NameSpace}) + } + for _, e := range gtx.Events(&b.keyTag, filters...) { switch e := e.(type) { case key.FocusEvent: b.focused = e.Focus diff --git a/widget/editor.go b/widget/editor.go index add1db4e..0b917d3d 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -330,9 +330,57 @@ func (e *Editor) processKey(gtx layout.Context) { if e.text.Changed() { e.events = append(e.events, ChangeEvent{}) } + filters := []event.Filter{key.FocusFilter{}, transfer.TargetFilter{Type: "application/text"}} + if e.focused { + filters = append(filters, + key.Filter{Name: key.NameEnter, Optional: key.ModShift}, + key.Filter{Name: key.NameReturn, Optional: key.ModShift}, + + key.Filter{Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, + key.Filter{Name: "C", Required: key.ModShortcut}, + key.Filter{Name: "V", Required: key.ModShortcut}, + key.Filter{Name: "X", Required: key.ModShortcut}, + key.Filter{Name: "A", Required: key.ModShortcut}, + + key.Filter{Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, + + key.Filter{Name: key.NameHome, Optional: key.ModShift}, + key.Filter{Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Name: key.NamePageUp, Optional: key.ModShift}, + ) + caret, _ := e.text.Selection() + if caret > 0 { + if gtx.Locale.Direction.Progression() == system.FromOrigin { + filters = append(filters, + key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + ) + } else { + filters = append(filters, + key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + ) + } + } + if caret < e.text.Len() { + if gtx.Locale.Direction.Progression() == system.FromOrigin { + filters = append(filters, + key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + ) + } else { + filters = append(filters, + key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + ) + } + } + } // adjust keeps track of runes dropped because of MaxLen. var adjust int - for _, ke := range gtx.Events(&e.eventKey, transfer.TargetFilter{Type: "application/text"}, key.FocusFilter{}) { + for _, ke := range gtx.Events(&e.eventKey, filters...) { e.blinkStart = gtx.Now switch ke := ke.(type) { case key.FocusEvent: @@ -625,33 +673,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() pointer.CursorText.Add(gtx.Ops) - var keys key.Set - if e.focused { - const keyFilterNoLeftUp = "(ShortAlt)-(Shift)-[→,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z" - const keyFilterNoRightDown = "(ShortAlt)-(Shift)-[←,↑]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z" - const keyFilterNoArrows = "(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z" - const keyFilterAllArrows = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z" - caret, _ := e.text.Selection() - switch { - case caret == 0 && caret == e.text.Len(): - keys = keyFilterNoArrows - case caret == 0: - if gtx.Locale.Direction.Progression() == system.FromOrigin { - keys = keyFilterNoLeftUp - } else { - keys = keyFilterNoRightDown - } - case caret == e.text.Len(): - if gtx.Locale.Direction.Progression() == system.FromOrigin { - keys = keyFilterNoRightDown - } else { - keys = keyFilterNoLeftUp - } - default: - keys = keyFilterAllArrows - } - } - key.InputOp{Tag: &e.eventKey, Keys: keys}.Add(gtx.Ops) + event.InputOp(gtx.Ops, &e.eventKey) key.InputHintOp{Tag: &e.eventKey, Hint: e.InputHint}.Add(gtx.Ops) e.scroller.Add(gtx.Ops) diff --git a/widget/editor_test.go b/widget/editor_test.go index 4ea9094e..6d09c140 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -124,6 +124,9 @@ func TestEditorReadOnly(t *testing.T) { gtx.Ops.Reset() layoutEditor() r.Frame(gtx.Ops) + gtx.Ops.Reset() + layoutEditor() + r.Frame(gtx.Ops) // Select everything. gtx.Ops.Reset() @@ -1005,8 +1008,11 @@ func TestSelectMove(t *testing.T) { gtx.Ops.Reset() e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) r.Frame(gtx.Ops) + gtx.Ops.Reset() + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) + r.Frame(gtx.Ops) - for _, keyName := range []string{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { + for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { // Select 345 e.SetCaret(3, 6) if expected, got := "345", e.SelectedText(); expected != got { diff --git a/widget/enum.go b/widget/enum.go index 375c1270..597356fa 100644 --- a/widget/enum.go +++ b/widget/enum.go @@ -4,6 +4,7 @@ package widget import ( "gioui.org/gesture" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/semantic" @@ -59,7 +60,13 @@ func (e *Enum) Update(gtx layout.Context) bool { } } } - for _, ev := range gtx.Events(&state.tag, key.FocusFilter{}) { + filters := []event.Filter{ + key.FocusFilter{}, + } + if e.focused && e.focus == state.key { + filters = append(filters, key.Filter{Name: key.NameReturn}, key.Filter{Name: key.NameSpace}) + } + for _, ev := range gtx.Events(&state.tag, filters...) { switch ev := ev.(type) { case key.FocusEvent: if ev.Focus { @@ -69,7 +76,7 @@ func (e *Enum) Update(gtx layout.Context) bool { e.focused = false } case key.Event: - if !e.focused || ev.State != key.Release { + if ev.State != key.Release { break } if ev.Name != key.NameReturn && ev.Name != key.NameSpace { @@ -117,9 +124,7 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou } clk := &state.click clk.Add(gtx.Ops) - if gtx.Enabled() { - key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops) - } + event.InputOp(gtx.Ops, &state.tag) semantic.SelectedOp(k == e.Value).Add(gtx.Ops) semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) c.Add(gtx.Ops) diff --git a/widget/selectable.go b/widget/selectable.go index c7bfa012..11e9bb5f 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -204,12 +204,7 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, dims := l.text.Dimensions() defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() pointer.CursorText.Add(gtx.Ops) - var keys key.Set - if l.focused { - const keyFilter = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,X,A]" - keys = keyFilter - } - key.InputOp{Tag: l, Keys: keys}.Add(gtx.Ops) + event.InputOp(gtx.Ops, l) l.clicker.Add(gtx.Ops) l.dragger.Add(gtx.Ops) @@ -304,7 +299,27 @@ func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { } func (e *Selectable) processKey(gtx layout.Context) { - for _, ke := range gtx.Events(e, key.FocusFilter{}) { + filters := []event.Filter{ + key.FocusFilter{}, + } + if e.focused { + filters = append(filters, + key.Filter{Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, + key.Filter{Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, + + key.Filter{Name: key.NamePageUp, Optional: key.ModShift}, + key.Filter{Name: key.NamePageDown, Optional: key.ModShift}, + key.Filter{Name: key.NameEnd, Optional: key.ModShift}, + key.Filter{Name: key.NameHome, Optional: key.ModShift}, + + key.Filter{Name: "C", Required: key.ModShortcut}, + key.Filter{Name: "X", Required: key.ModShortcut}, + key.Filter{Name: "A", Required: key.ModShortcut}, + ) + } + for _, ke := range gtx.Events(e, filters...) { switch ke := ke.(type) { case key.FocusEvent: e.focused = ke.Focus diff --git a/widget/selectable_test.go b/widget/selectable_test.go index cb5defeb..8c7d9795 100644 --- a/widget/selectable_test.go +++ b/widget/selectable_test.go @@ -57,8 +57,10 @@ func TestSelectableMove(t *testing.T) { s.SetCaret(3, 6) s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) r.Frame(gtx.Ops) + s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) + r.Frame(gtx.Ops) - for _, keyName := range []string{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { + for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { // Select 345 s.SetCaret(3, 6) if start, end := s.Selection(); start != 3 || end != 6 {