Class: Tuile::Component::TextInput

Inherits:
Component
  • Object
show all
Defined in:
lib/tuile/component/text_input.rb,
sig/tuile.rbs

Overview

Abstract base for editable text components (TextField, TextArea).

Holds the shared state — a mutable #text buffer, a #caret index, #on_change and #on_escape callbacks — and the keyboard machinery that single-line and multi-line inputs both need: ESC handling, LEFT/RIGHT caret movement, CTRL+LEFT/CTRL+RIGHT word jumps, and the focusable?/tab_stop? flags.

Subclasses implement the layout-specific pieces (#cursor_position, #repaint) and add their own keys (HOME/END, ENTER, UP/DOWN, printable insertion) by overriding the protected #handle_text_input_key hook — super falls through to the common navigation handling.

The mutation pipeline is a template method: #text= and #caret= detect no-ops, mutate state, fire #on_change, and invalidate. Subclasses inject their own behavior via two protected hooks:

Direct Known Subclasses

TextArea, TextField

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTextInput

Returns a new instance of TextInput.



29
30
31
32
33
34
35
36
# File 'lib/tuile/component/text_input.rb', line 29

def initialize
  super
  @text = +""
  @caret = 0
  @on_change = nil
  @on_key = nil
  @on_escape = method(:default_on_escape)
end

Instance Attribute Details

#caretInteger

@return — caret index in 0..text.length.

Returns:

  • (Integer)


45
46
47
# File 'lib/tuile/component/text_input.rb', line 45

def caret
  @caret
end

#on_changeProc, ...

Optional callback fired whenever #text changes. Receives the new text as a single argument. Not fired by #caret= (text unchanged) and not fired when a setter is a no-op.

@return — one-arg callable, or nil.

Returns:

  • (Proc, Method, nil)


51
52
53
# File 'lib/tuile/component/text_input.rb', line 51

def on_change
  @on_change
end

#on_escapeProc, ...

Callback fired when ESC is pressed. Defaults to a closure that clears focus (screen.focused = nil) so ESC visibly cancels text entry instead of bubbling to the parent — and, in particular, instead of reaching the screen's default ESC-to-quit handler. Set to nil to let ESC fall through to the parent again; set to any other callable to replace the default.

@return — no-arg callable, or nil.

Returns:

  • (Proc, Method, nil)


73
74
75
# File 'lib/tuile/component/text_input.rb', line 73

def on_escape
  @on_escape
end

#on_keyProc, ...

Optional interceptor consulted before the input's own key handling. Receives the pressed key; return a truthy value to consume it (the input then ignores that key), falsy to let normal editing proceed.

The keyboard analog of #on_change: it lets app code layer behavior onto an input without subclassing. The motivating case is an autocomplete / slash-command overlay (a non-modal Popup): while it is open the interceptor claims Up/Down/Enter/ESC and forwards them to the overlay's list, but lets ordinary characters fall through so typing keeps editing the field (and #on_change keeps refilling the list).

@return — one-arg callable, or nil.

Returns:

  • (Proc, Method, nil)


65
66
67
# File 'lib/tuile/component/text_input.rb', line 65

def on_key
  @on_key
end

#textString

@return — current text contents.

Returns:

  • (String)


39
40
41
# File 'lib/tuile/component/text_input.rb', line 39

def text
  @text
end

Instance Method Details

#background(text) ⇒ StyledString

Renders text on the field's background well, looked up from the current Screen#theme at paint time: Theme#active_bg_color when this input is on the active (focus) chain, Theme#input_bg_color otherwise — visibly a field either way, distinctly highlighted when active.

@param text

@return — text on the field's background well.

Parameters:

  • text (String)

Returns:



127
128
129
# File 'lib/tuile/component/text_input.rb', line 127

def background(text)
  StyledString.styled(text, bg: active? ? screen.theme.active_bg_color : screen.theme.input_bg_color)
end

#default_on_escapevoid

This method returns an undefined value.

Default #on_escape action: clear focus. Component deactivates; user can re-focus by clicking or tabbing back in.



197
198
199
# File 'lib/tuile/component/text_input.rb', line 197

def default_on_escape
  screen.focused = nil
end

#delete_at_caretvoid

This method returns an undefined value.



184
185
186
187
188
189
190
# File 'lib/tuile/component/text_input.rb', line 184

def delete_at_caret
  return if @caret >= @text.length

  new_text = @text.dup
  new_text.slice!(@caret)
  self.text = new_text
end

#delete_before_caretvoid

This method returns an undefined value.



174
175
176
177
178
179
180
181
# File 'lib/tuile/component/text_input.rb', line 174

def delete_before_caret
  return if @caret.zero?

  new_text = @text.dup
  new_text.slice!(@caret - 1)
  @caret -= 1
  self.text = new_text
end

#empty?Boolean

@return — true iff #text is the empty string.

Returns:

  • (Boolean)


42
# File 'lib/tuile/component/text_input.rb', line 42

def empty? = @text.empty?

#focusable?Boolean

Returns:

  • (Boolean)


75
# File 'lib/tuile/component/text_input.rb', line 75

def focusable? = true

#handle_key(key) ⇒ Boolean

Handles a key. An #on_key interceptor (if set) gets first refusal — a truthy return consumes the key — otherwise it delegates to #handle_text_input_key. Dispatch (ScreenPane#handle_key) only routes keys here when this input is on the focus chain, so there is no Tuile::Component#active? gate.

@param key

Parameters:

  • key (String)

Returns:

  • (Boolean)


113
114
115
116
117
# File 'lib/tuile/component/text_input.rb', line 113

def handle_key(key)
  return true if @on_key&.call(key)

  handle_text_input_key(key)
end

#handle_text_input_key(key) ⇒ Boolean

Dispatch hook for #handle_key. Handles ESC and the navigation keys that have identical semantics in single-line and multi-line inputs: LEFT/RIGHT arrows, CTRL+LEFT/CTRL+RIGHT for word jumps. Subclasses override to add their own keys (HOME/END, UP/DOWN, ENTER, BACKSPACE/ DELETE, printable insertion) and call super to fall back to the common navigation handling.

@param key

@return — true if the key was handled.

Parameters:

  • key (String)

Returns:

  • (Boolean)


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/tuile/component/text_input.rb', line 157

def handle_text_input_key(key)
  case key
  when Keys::LEFT_ARROW then self.caret = @caret - 1
  when Keys::RIGHT_ARROW then self.caret = @caret + 1
  when Keys::CTRL_LEFT_ARROW then self.caret = word_left
  when Keys::CTRL_RIGHT_ARROW then self.caret = word_right
  when Keys::ESC
    return false if @on_escape.nil?

    @on_escape.call
  else
    return false
  end
  true
end

#on_caret_mutatedvoid

This method returns an undefined value.

Hook called after #caret has been mutated, before invalidation. Default no-op. Subclasses use this to keep the caret visible (Tuile::Component::TextArea's vertical scroll).



147
# File 'lib/tuile/component/text_input.rb', line 147

def on_caret_mutated; end

#on_text_mutatedvoid

This method returns an undefined value.

Hook called after #text has been mutated, before invalidation / #on_change. Default no-op. Subclasses use this to invalidate caches (Tuile::Component::TextArea's wrap cache) and update derived state.



141
# File 'lib/tuile/component/text_input.rb', line 141

def on_text_mutated; end

#preprocess_text(new_text) ⇒ String

Input filter for #text=. Subclasses override to truncate or reject invalid input. Default coerces to String.

@param new_text

@return — possibly transformed text.

Parameters:

  • new_text (String)

Returns:

  • (String)


135
# File 'lib/tuile/component/text_input.rb', line 135

def preprocess_text(new_text) = new_text.to_s

#tab_stop?Boolean

Returns:

  • (Boolean)


77
# File 'lib/tuile/component/text_input.rb', line 77

def tab_stop? = true

#word_leftInteger

Caret target for ctrl+left: skip whitespace going left, then a run of non-whitespace. Lands at the beginning of the current word, or the beginning of the previous word if already there.

Returns:

  • (Integer)


205
206
207
208
209
210
# File 'lib/tuile/component/text_input.rb', line 205

def word_left
  c = @caret
  c -= 1 while c.positive? && @text[c - 1].match?(/\s/)
  c -= 1 while c.positive? && !@text[c - 1].match?(/\s/)
  c
end

#word_rightInteger

Caret target for ctrl+right: skip non-whitespace going right, then a run of whitespace. Lands at the beginning of the next word, or at the end of the text if no further word exists.

Returns:

  • (Integer)


216
217
218
219
220
221
# File 'lib/tuile/component/text_input.rb', line 216

def word_right
  c = @caret
  c += 1 while c < @text.length && !@text[c].match?(/\s/)
  c += 1 while c < @text.length && @text[c].match?(/\s/)
  c
end