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  }