Class: Tuile::Component::TextInput
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::TextInput
- 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:
- #preprocess_text — input filter (e.g. TextField truncates to
fit
rect.width - 1). - #on_text_mutated / #on_caret_mutated — post-mutation side effects (e.g. TextArea invalidates its wrap cache and scrolls to keep the caret visible).
Instance Attribute Summary collapse
-
#caret ⇒ Integer
@return — caret index in
0..text.length. -
#on_change ⇒ Proc, ...
Optional callback fired whenever #text changes.
-
#on_escape ⇒ Proc, ...
Callback fired when ESC is pressed.
-
#on_key ⇒ Proc, ...
Optional interceptor consulted before the input's own key handling.
-
#text ⇒ String
@return — current text contents.
Instance Method Summary collapse
-
#background(text) ⇒ StyledString
Renders
texton 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. -
#default_on_escape ⇒ void
Default #on_escape action: clear focus.
- #delete_at_caret ⇒ void
- #delete_before_caret ⇒ void
-
#empty? ⇒ Boolean
@return — true iff #text is the empty string.
- #focusable? ⇒ Boolean
-
#handle_key(key) ⇒ Boolean
Handles a key.
-
#handle_text_input_key(key) ⇒ Boolean
Dispatch hook for #handle_key.
-
#initialize ⇒ TextInput
constructor
A new instance of TextInput.
-
#on_caret_mutated ⇒ void
Hook called after #caret has been mutated, before invalidation.
-
#on_text_mutated ⇒ void
Hook called after #text has been mutated, before invalidation / #on_change.
-
#preprocess_text(new_text) ⇒ String
Input filter for #text=.
- #tab_stop? ⇒ Boolean
-
#word_left ⇒ Integer
Caret target for ctrl+left: skip whitespace going left, then a run of non-whitespace.
-
#word_right ⇒ Integer
Caret target for ctrl+right: skip non-whitespace going right, then a run of whitespace.
Constructor Details
#initialize ⇒ TextInput
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
#caret ⇒ Integer
@return — caret index in 0..text.length.
45 46 47 |
# File 'lib/tuile/component/text_input.rb', line 45 def caret @caret end |
#on_change ⇒ Proc, ...
51 52 53 |
# File 'lib/tuile/component/text_input.rb', line 51 def on_change @on_change end |
#on_escape ⇒ Proc, ...
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.
73 74 75 |
# File 'lib/tuile/component/text_input.rb', line 73 def on_escape @on_escape end |
#on_key ⇒ Proc, ...
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.
65 66 67 |
# File 'lib/tuile/component/text_input.rb', line 65 def on_key @on_key end |
#text ⇒ String
@return — current text contents.
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.
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_escape ⇒ void
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_caret ⇒ void
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_caret ⇒ void
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.
42 |
# File 'lib/tuile/component/text_input.rb', line 42 def empty? = @text.empty? |
#focusable? ⇒ 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
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.
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_mutated ⇒ void
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_mutated ⇒ void
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.
135 |
# File 'lib/tuile/component/text_input.rb', line 135 def preprocess_text(new_text) = new_text.to_s |
#tab_stop? ⇒ Boolean
77 |
# File 'lib/tuile/component/text_input.rb', line 77 def tab_stop? = true |
#word_left ⇒ Integer
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.
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_right ⇒ Integer
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.
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 |