Class: Tuile::Component::AbstractWrappingField
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::AbstractWrappingField
- Includes:
- HasPlaceholder, HasValue
- Defined in:
- lib/tuile/component/abstract_wrapping_field.rb,
sig/tuile.rbs
Overview
Abstract base for a field that wraps one editor completely: it carries
a typed HasValue#value but paints nothing itself, handing the whole UI to
a single AbstractStringField it owns and hides. Subclass it by passing
the editor to super and defining the conversion both ways:
class IntegerField < Component::AbstractWrappingField
def initialize = super(TextField.new)
def value = Integer(editor.text, 10) rescue nil
def value=(new_value)
editor.text = new_value.nil? ? "" : new_value.to_s
editor.caret = editor.text.length
end
def empty_value = nil
end
Everything else arrives already wired: the editor is added as the single child and positioned across #rect, focus forwards into it, it sits on this field's one background well, and #placeholder / #on_enter / #cursor_position / #clear are re-exposed here so an app never addresses it. Give the field a single-row rect.
The editor is private machinery
There is no public accessor — #editor is protected, for subclasses — and
that is the point: swapping it would break the conversion. An app never
addresses the editor; what it needs is either already delegated here or
earns a forwarder here. It is still in children, because the tree is
reported honestly, but that is not an invitation.
A spec is the exception, and it has a sanctioned path — driving the editor is how a test reaches a state no public setter produces:
editor = Testing.get(Component::TextField, in: field)
editor.text = "-" # bad input; field.value still reads nil
A knob that is editor-shaped rather than a concept of this field's own domain is not forwarded, and the subclass sets it on its editor instead:
def initialize
super(TextField.new)
editor.max_text_length = 20 # an internal cap, not part of my surface
end
Committing: leaving the widget, and ENTER
#commit fires on both commit gestures. Leaving the focus chain is one — the field's, not its editor's, which is left on every hop within a widget. ENTER is the other, because a form whose default button is reached by ENTER never moves focus at all. Override it to canonicalize a buffer the user typed loosely:
def commit = (self.value = value unless value.nil?) # rewrite in the canonical form
ENTER is committed and then left to keep bubbling, so a scope's default button still sees it; only an #on_enter of this field's own consumes it, which is TextField#on_enter's existing contract.
When the value notice fires
Per edit by default. A field whose grammar is not prefix-closed sets
#notify_on_edit? to false and lets the notice settle onto those same
two gestures, so a form is never handed a half-typed date that happens to
parse (DateField, TimeField).
Implementation details
- HasValue#value and #value= raise until overridden. The inherited
pair stores into
@valueand never touches the editor, so a subclass that defined only one would silently half-work. - HasValue#empty_value is called during construction, to seed the
change guard, so it must not depend on subclass state that
superhas not set yet. In practice it is a constant per class. - The editor's
on_changeandon_enterslots are claimed — for that guard, and to commit before an app's ENTER handler runs. A slot cannot be shared, so a subclass reacting to buffer edits overrides #handle_editor_change (every edit), #value= or #commit rather than reassigning either. - Not for a field whose editor is a filter. This base assumes the buffer is a rendering of the value, so an edit may change the value. ComboBox breaks both halves — its text is a transient query and only a commit moves its value — which is the same line HasBadInput draws.
UI-thread-confined, like every component (see Screen).
Direct Known Subclasses
BigDecimalField, DateField, FloatField, IntegerField, TimeField
Instance Attribute Summary collapse
-
#editor ⇒ AbstractStringField
readonly
@return — the wrapped editor.
-
#on_enter ⇒ Proc, ...
@return — fired when ENTER is pressed, after #commit; see TextField#on_enter.
Attributes included from HasValue
Attributes included from HasValidation
Instance Method Summary collapse
-
#active=(flag) ⇒ void
Runs #commit on the falling edge, i.e.
-
#clear ⇒ void
Empties the input, not just the value — a field holding bad input already reads HasValue#empty_value, so clearing through #value= could leave the glyphs on screen (HasBadInput).
-
#commit ⇒ void
Called on a commit gesture — the field leaving the focus chain, or ENTER; no-op by default.
-
#commit_and_notify ⇒ void
Every commit gesture runs through here, so a field holding its notice (#notify_on_edit?) announces from one place rather than three; the diff guard makes the call free for a field that fired on the way in.
-
#cursor_position ⇒ Point?
@return — the editor's caret — the hardware cursor is delegated to it.
-
#default_bg_color ⇒ Color
The field well the face sits on — the editor is marked BG_INHERIT, so this one covers it (exactly one well per widget) and #bg_color set here reaches the cells it paints.
-
#empty? ⇒ Boolean
Empty of value: a field whose parse is partial reports
truewhile the user is looking at glyphs it could not use, so ask HasBadInput#bad_input? first. - #empty_value ⇒ Object
-
#error_bg_color ⇒ Color?
The invalid well, picked up by everything this component paints — including the inner face of a composed field and the List of a group, neither of which forwards anything: both declare no background of their own, so the ordinary chain walks up to this (overrides #error_bg_color).
-
#error_ink? ⇒ Boolean
Whether to paint the invalid well right now.
-
#error_message ⇒ StyledString?
@return — why the field is invalid, or
nilwhen it is not;niluntil something sets it. -
#error_message= ⇒ void
Sets the verdict and repaints the field in Theme#error_color;
nilclears it. -
#fire_if_changed ⇒ void
Re-emits HasValue#on_value_change, but only when #value differs from the last one fired — so a buffer edit that leaves the value alone (
"7"→"07") stays silent. -
#focusable? ⇒ Boolean
Input fields are focusable by default (overrides #focusable?); a read-only display field could override back to
false. -
#handle_editor_change ⇒ void
Called whenever the editor's buffer changes, however the characters arrived — a typed key, a paste, or a #value= of this field's own.
- #handle_focus ⇒ void
-
#handle_key?(key) ⇒ Boolean
Commits on ENTER, and leaves the key unconsumed so it keeps bubbling.
-
#initialize(editor) ⇒ AbstractWrappingField
constructor
@param
editor— the editor to wrap; becomes this field's single child and is never swapped. -
#inspect_details ⇒ ::Array[String]
Adds
placeholder="…"to #inspect, omitted while unset. -
#layout(editor) ⇒ void
Places the editor across the whole rect; override to reserve cells for a face of your own.
-
#notify_on_edit? ⇒ Boolean
Whether an edit of the buffer fires HasValue#on_value_change as it happens.
-
#placeholder ⇒ String?
@return — the hint the editor paints while empty (HasPlaceholder).
-
#placeholder=(text) ⇒ void
@param
text. -
#rect=(new_rect) ⇒ void
@param
new_rect. -
#value ⇒ Object
@return — the typed value, parsed from the editor's buffer.
-
#value=(new_value) ⇒ void
Writes
new_valueinto the editor's buffer.
Constructor Details
#initialize(editor) ⇒ AbstractWrappingField
@param editor — the editor to wrap; becomes this field's single child and is never swapped.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 95 def initialize(editor) super() raise TypeError, "expected AbstractStringField, got #{editor.inspect}" unless editor.is_a?(AbstractStringField) @editor = editor @last_value = empty_value @on_enter = nil # One widget, one surface: the editor paints no well of its own, so this # field's bg_color reaches the cells the editor paints. editor.bg_color = BG_INHERIT editor.on_change = lambda do |_text| handle_editor_change fire_if_changed if notify_on_edit? end add_child(editor, at: 0) end |
Instance Attribute Details
#editor ⇒ AbstractStringField (readonly)
@return — the wrapped editor.
211 212 213 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 211 def editor @editor end |
#on_enter ⇒ Proc, ...
@return — fired when ENTER is pressed, after #commit; see TextField#on_enter.
149 150 151 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 149 def on_enter @on_enter end |
Instance Method Details
#active=(flag) ⇒ void
This method returns an undefined value.
Runs #commit on the falling edge, i.e. when this field leaves the focus chain. Moving focus within a widget keeps it active, so a future multi-editor field inherits the same semantics unchanged.
@param flag
187 188 189 190 191 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 187 def active=(flag) was = active? super commit_and_notify if was && !active? end |
#clear ⇒ void
This method returns an undefined value.
Empties the input, not just the value — a field holding bad input already reads HasValue#empty_value, so clearing through #value= could leave the glyphs on screen (HasBadInput).
128 129 130 131 132 133 134 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 128 def clear editor.clear # Announced here rather than through the editor's change, so a field # holding its notice ({#notify_on_edit?}) still reports an emptying as # it happens: emptying is not a half-typed prefix. fire_if_changed end |
#commit ⇒ void
This method returns an undefined value.
Called on a commit gesture — the field leaving the focus chain, or ENTER; no-op by default. This is the commit point a canonicalizing field rewrites its buffer from.
217 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 217 def commit = nil |
#commit_and_notify ⇒ void
This method returns an undefined value.
Every commit gesture runs through here, so a field holding its notice (#notify_on_edit?) announces from one place rather than three; the diff guard makes the call free for a field that fired on the way in.
261 262 263 264 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 261 def commit_and_notify commit fire_if_changed end |
#cursor_position ⇒ Point?
@return — the editor's caret — the hardware cursor is delegated to it.
180 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 180 def cursor_position = editor.cursor_position |
#default_bg_color ⇒ Color
The field well the face sits on — the editor is marked BG_INHERIT, so this one covers it (exactly one well per widget) and Tuile::Component#bg_color set here reaches the cells it paints.
253 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 253 def default_bg_color = active? ? screen.theme.active_bg_color : screen.theme.input_bg_color |
#empty? ⇒ Boolean
Empty of value: a field whose parse is partial reports true while the
user is looking at glyphs it could not use, so ask
HasBadInput#bad_input? first.
@return — true iff #value equals #empty_value.
11320 |
# File 'sig/tuile.rbs', line 11320
def empty?: () -> bool
|
#empty_value ⇒ Object
11324 |
# File 'sig/tuile.rbs', line 11324
def empty_value: () -> Object
|
#error_bg_color ⇒ Color?
The invalid well, picked up by everything this component paints — including the inner face of a composed field and the List of a group, neither of which forwards anything: both declare no background of their own, so the ordinary chain walks up to this (overrides Tuile::Component#error_bg_color).
11352 |
# File 'sig/tuile.rbs', line 11352
def error_bg_color: () -> Color?
|
#error_ink? ⇒ Boolean
Whether to paint the invalid well right now. Its own hook because HasBadInput widens it: a field holding input its value cannot represent is invalid on the face too, even with no verdict written.
11357 |
# File 'sig/tuile.rbs', line 11357
def error_ink?: () -> bool
|
#error_message ⇒ StyledString?
@return — why the field is invalid, or nil when it
is not; nil until something sets it.
11335 |
# File 'sig/tuile.rbs', line 11335
def error_message: () -> StyledString?
|
#error_message= ⇒ void
This method returns an undefined value.
Sets the verdict and repaints the field in Theme#error_color; nil
clears it. No-op (no repaint, no listener) when unchanged. A String is
parsed via StyledString.parse, as HasCaption#caption= does.
Safe on a detached field — an app validates a form it assembled but has not mounted, and Tuile::Component#invalidate is already a no-op there.
@param new_message
11345 |
# File 'sig/tuile.rbs', line 11345
def error_message=: ((String | StyledString)? new_message) -> void
|
#fire_if_changed ⇒ void
This method returns an undefined value.
Re-emits HasValue#on_value_change, but only when #value differs from
the last one fired — so a buffer edit that leaves the value alone
("7"→"07") stays silent.
270 271 272 273 274 275 276 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 270 def fire_if_changed v = value return if v == @last_value @last_value = v on_value_change&.call(v) end |
#focusable? ⇒ Boolean
Input fields are focusable by default (overrides Tuile::Component#focusable?);
a read-only display field could override back to false. Only
focusable? lives here — tab_stop? diverges between leaf fields and
composing wrappers, so it stays per-class (design/decisions.md
D_integer_field).
11331 |
# File 'sig/tuile.rbs', line 11331
def focusable?: () -> bool
|
#handle_editor_change ⇒ void
This method returns an undefined value.
Called whenever the editor's buffer changes, however the characters arrived — a typed key, a paste, or a #value= of this field's own. It is named for the editor, not for the user, because those last two are not input. No-op by default; override it to drop state that describes the previous buffer, as a field latching whether its input has settled must (HasBadInput).
241 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 241 def handle_editor_change; end |
#handle_focus ⇒ void
This method returns an undefined value.
194 195 196 197 198 199 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 194 def handle_focus super # The editor is what actually edits, so it takes the focus this field was # given — the field itself has no keys of its own. screen.focused = editor if editor.focusable? end |
#handle_key?(key) ⇒ Boolean
Commits on ENTER, and leaves the key unconsumed so it keeps bubbling.
The editor declines ENTER whenever #on_enter is nil, so the key reaches this field instead — and it must be committed on the way past, or the form default button it is bubbling towards acts on an uncommitted buffer.
@param key
@return — whatever super returns — committing never consumes
the key.
173 174 175 176 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 173 def handle_key?(key) commit_and_notify if key == Keys::ENTER super end |
#inspect_details ⇒ ::Array[String]
Adds placeholder="…" to Tuile::Component#inspect, omitted while unset.
11313 |
# File 'sig/tuile.rbs', line 11313
def inspect_details: () -> ::Array[String]
|
#layout(editor) ⇒ void
This method returns an undefined value.
Places the editor across the whole rect; override to reserve cells for a face of your own.
@param editor
247 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 247 def layout(editor) = (editor.rect = rect) |
#notify_on_edit? ⇒ Boolean
Whether an edit of the buffer fires HasValue#on_value_change as it
happens. true here, which is right wherever every buffer state is a
value the user might mean: an IntegerField passing through 4 on the
way to 42 really does hold 4 for that keystroke. A field whose
grammar is not prefix-closed answers false and lets the notice
settle onto the commit gestures instead (DateField, D_date_field).
Only the push settles: HasValue#value stays a live parse of the
buffer either way. And overriding this is half the job — #commit is
covered here, but the field must fire from its own value= too, or a
programmatic write and an Up/Down step go unannounced until the next
commit.
232 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 232 def notify_on_edit? = true |
#placeholder ⇒ String?
@return — the hint the editor paints while empty (HasPlaceholder).
138 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 138 def placeholder = editor.placeholder |
#placeholder=(text) ⇒ void
This method returns an undefined value.
@param text
143 144 145 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 143 def placeholder=(text) editor.placeholder = text end |
#rect=(new_rect) ⇒ void
This method returns an undefined value.
@param new_rect
203 204 205 206 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 203 def rect=(new_rect) super layout(editor) end |
#value ⇒ Object
@return — the typed value, parsed from the editor's buffer.
114 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 114 def value = raise(NotImplementedError, "#{self.class} must implement value") |
#value=(new_value) ⇒ void
This method returns an undefined value.
Writes new_value into the editor's buffer.
@param new_value
120 121 122 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 120 def value=(new_value) raise(NotImplementedError, "#{self.class} must implement value=") end |