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