github.com/utopiagio/gio@v0.0.8/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  	"github.com/utopiagio/gio/f32"
    10  	"github.com/utopiagio/gio/io/event"
    11  	"github.com/utopiagio/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  	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  }