gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/io/input/key.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package input 4 5 import ( 6 "image" 7 "sort" 8 9 "gioui.org/f32" 10 "gioui.org/io/event" 11 "gioui.org/io/key" 12 ) 13 14 // EditorState represents the state of an editor needed by input handlers. 15 type EditorState struct { 16 Selection struct { 17 Transform f32.Affine2D 18 key.Range 19 key.Caret 20 } 21 Snippet key.Snippet 22 } 23 24 type TextInputState uint8 25 26 type keyQueue struct { 27 order []event.Tag 28 dirOrder []dirFocusEntry 29 hint key.InputHint 30 } 31 32 // keyState is the input state related to key events. 33 type keyState struct { 34 focus event.Tag 35 state TextInputState 36 content EditorState 37 } 38 39 type keyHandler struct { 40 // visible will be true if the InputOp is present 41 // in the current frame. 42 visible bool 43 // reset tracks whether the handler has seen a 44 // focus reset. 45 reset bool 46 hint key.InputHint 47 orderPlusOne int 48 dirOrder int 49 trans f32.Affine2D 50 } 51 52 type keyFilter []key.Filter 53 54 type dirFocusEntry struct { 55 tag event.Tag 56 row int 57 area int 58 bounds image.Rectangle 59 } 60 61 const ( 62 TextInputKeep TextInputState = iota 63 TextInputClose 64 TextInputOpen 65 ) 66 67 func (k *keyHandler) inputHint(hint key.InputHint) { 68 k.hint = hint 69 } 70 71 // InputState returns the input state and returns a state 72 // reset to [TextInputKeep]. 73 func (s keyState) InputState() (keyState, TextInputState) { 74 state := s.state 75 s.state = TextInputKeep 76 return s, state 77 } 78 79 // InputHint returns the input hint from the focused handler and whether it was 80 // changed since the last call. 81 func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) { 82 focused, ok := handlers[state.focus] 83 if !ok { 84 return q.hint, false 85 } 86 old := q.hint 87 q.hint = focused.key.hint 88 return q.hint, old != q.hint 89 } 90 91 func (k *keyHandler) Reset() { 92 k.visible = false 93 k.orderPlusOne = 0 94 k.hint = key.HintAny 95 } 96 97 func (q *keyQueue) Reset() { 98 q.order = q.order[:0] 99 q.dirOrder = q.dirOrder[:0] 100 } 101 102 func (k *keyHandler) ResetEvent() (event.Event, bool) { 103 if k.reset { 104 return nil, false 105 } 106 k.reset = true 107 return key.FocusEvent{Focus: false}, true 108 } 109 110 func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState { 111 if state.focus != nil { 112 if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible { 113 // Remove focus from the handler that is no longer focusable. 114 state.focus = nil 115 state.state = TextInputClose 116 } 117 } 118 q.updateFocusLayout(handlers) 119 return state 120 } 121 122 // updateFocusLayout partitions input handlers handlers into rows 123 // for directional focus moves. 124 // 125 // The approach is greedy: pick the topmost handler and create a row 126 // containing it. Then, extend the handler bounds to a horizontal beam 127 // and add to the row every handler whose center intersect it. Repeat 128 // until no handlers remain. 129 func (q *keyQueue) updateFocusLayout(handlers map[event.Tag]*handler) { 130 order := q.dirOrder 131 // Sort by ascending y position. 132 sort.SliceStable(order, func(i, j int) bool { 133 return order[i].bounds.Min.Y < order[j].bounds.Min.Y 134 }) 135 row := 0 136 for len(order) > 0 { 137 h := &order[0] 138 h.row = row 139 bottom := h.bounds.Max.Y 140 end := 1 141 for ; end < len(order); end++ { 142 h := &order[end] 143 center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2 144 if center > bottom { 145 break 146 } 147 h.row = row 148 } 149 // Sort row by ascending x position. 150 sort.SliceStable(order[:end], func(i, j int) bool { 151 return order[i].bounds.Min.X < order[j].bounds.Min.X 152 }) 153 order = order[end:] 154 row++ 155 } 156 for i, o := range q.dirOrder { 157 handlers[o.tag].key.dirOrder = i 158 } 159 } 160 161 // MoveFocus attempts to move the focus in the direction of dir. 162 func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) { 163 if len(q.dirOrder) == 0 { 164 return state, nil 165 } 166 order := 0 167 if state.focus != nil { 168 order = handlers[state.focus].key.dirOrder 169 } 170 focus := q.dirOrder[order] 171 switch dir { 172 case key.FocusForward, key.FocusBackward: 173 if len(q.order) == 0 { 174 break 175 } 176 order := 0 177 if dir == key.FocusBackward { 178 order = -1 179 } 180 if state.focus != nil { 181 order = handlers[state.focus].key.orderPlusOne - 1 182 if dir == key.FocusForward { 183 order++ 184 } else { 185 order-- 186 } 187 } 188 order = (order + len(q.order)) % len(q.order) 189 return q.Focus(handlers, state, q.order[order]) 190 case key.FocusRight, key.FocusLeft: 191 next := order 192 if state.focus != nil { 193 next = order + 1 194 if dir == key.FocusLeft { 195 next = order - 1 196 } 197 } 198 if 0 <= next && next < len(q.dirOrder) { 199 newFocus := q.dirOrder[next] 200 if newFocus.row == focus.row { 201 return q.Focus(handlers, state, newFocus.tag) 202 } 203 } 204 case key.FocusUp, key.FocusDown: 205 delta := +1 206 if dir == key.FocusUp { 207 delta = -1 208 } 209 nextRow := 0 210 if state.focus != nil { 211 nextRow = focus.row + delta 212 } 213 var closest event.Tag 214 dist := int(1e6) 215 center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2 216 loop: 217 for 0 <= order && order < len(q.dirOrder) { 218 next := q.dirOrder[order] 219 switch next.row { 220 case nextRow: 221 nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2 222 d := center - nextCenter 223 if d < 0 { 224 d = -d 225 } 226 if d > dist { 227 break loop 228 } 229 dist = d 230 closest = next.tag 231 case nextRow + delta: 232 break loop 233 } 234 order += delta 235 } 236 if closest != nil { 237 return q.Focus(handlers, state, closest) 238 } 239 } 240 return state, nil 241 } 242 243 func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle { 244 order := k.dirOrder 245 return q.dirOrder[order].bounds 246 } 247 248 func (q *keyQueue) AreaFor(k *keyHandler) int { 249 order := k.dirOrder 250 return q.dirOrder[order].area 251 } 252 253 func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool { 254 for _, f := range *k { 255 if keyFilterMatch(focus, f, e, system) { 256 return true 257 } 258 } 259 return false 260 } 261 262 func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool { 263 if f.Focus != nil && f.Focus != focus { 264 return false 265 } 266 if (f.Name != "" || system) && f.Name != e.Name { 267 return false 268 } 269 if e.Modifiers&f.Required != f.Required { 270 return false 271 } 272 if e.Modifiers&^(f.Required|f.Optional) != 0 { 273 return false 274 } 275 return true 276 } 277 278 func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) { 279 if focus == state.focus { 280 return state, nil 281 } 282 state.content = EditorState{} 283 var evts []taggedEvent 284 if state.focus != nil { 285 evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}}) 286 } 287 state.focus = focus 288 if state.focus != nil { 289 evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}}) 290 } 291 if state.focus == nil || state.state == TextInputKeep { 292 state.state = TextInputClose 293 } 294 return state, evts 295 } 296 297 func (s keyState) softKeyboard(show bool) keyState { 298 if show { 299 s.state = TextInputOpen 300 } else { 301 s.state = TextInputClose 302 } 303 return s 304 } 305 306 func (k *keyFilter) Add(f key.Filter) { 307 for _, f2 := range *k { 308 if f == f2 { 309 return 310 } 311 } 312 *k = append(*k, f) 313 } 314 315 func (k *keyFilter) Merge(k2 keyFilter) { 316 *k = append(*k, k2...) 317 } 318 319 func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) { 320 state.visible = true 321 if state.orderPlusOne == 0 { 322 state.orderPlusOne = len(q.order) + 1 323 q.order = append(q.order, tag) 324 q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds}) 325 } 326 state.trans = t 327 } 328 329 func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState { 330 if req.Tag != state.focus { 331 return state 332 } 333 state.content.Selection.Range = req.Range 334 state.content.Selection.Caret = req.Caret 335 return state 336 } 337 338 func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState { 339 s := state.content 340 if f := state.focus; f != nil { 341 s.Selection.Transform = handlers[f].key.trans 342 } 343 return s 344 } 345 346 func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState { 347 if req.Tag == state.focus { 348 state.content.Snippet = req.Snippet 349 } 350 return state 351 } 352 353 func (t TextInputState) String() string { 354 switch t { 355 case TextInputKeep: 356 return "Keep" 357 case TextInputClose: 358 return "Close" 359 case TextInputOpen: 360 return "Open" 361 default: 362 panic("unexpected value") 363 } 364 }