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 set_value(new_value, from_user:)
editor.set_value(new_value.nil? ? "" : new_value.to_s, from_user:)
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.value = "-" # 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 = (set_value(value, from_user: true) unless value.nil?) # rewrite canonically
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).
Who made the change
The editor's own event says whether the user moved the buffer, and this
field's notice passes that on: a subclass writes the editor through
Tuile::Component::AbstractStringField#set_value with the from_user: it was handed, and
the edit route relays it. The commit gestures announce as the user's —
an approximation for the one commit code can cause, a programmatic
screen.focused = moving focus off this field.
Implementation details
- HasValue#value and #set_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_value_changeandon_enterslots carry this field's own listeners — for that guard, and to commit before an app's ENTER handler runs. They are lists, so nothing an app adds displaces them; a subclass reacting to buffer edits still overrides #handle_editor_change (every edit), #set_value or #commit, which run in a defined order. - 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
Defined Under Namespace
Classes: EnterEvent
Instance Attribute Summary collapse
-
#editor ⇒ AbstractStringField
readonly
@return — the wrapped editor.
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.
-
#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(from_user:) ⇒ 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 #set_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. -
#notify_on_edit? ⇒ Boolean
Whether an edit of the buffer fires HasValue#on_value_change as it happens.
-
#on_enter ⇒ Listeners
Fired with an EnterEvent when ENTER is pressed, after #commit; see TextField#on_enter.
-
#on_error_message_change ⇒ Listeners
Fired with an HasValidation::ErrorMessageChangeEvent whenever #error_message actually changes — never on a no-op set.
-
#on_value_change ⇒ Listeners
Fired with a HasValue::ValueChangeEvent whenever #value actually changes — never on a no-op set.
-
#placeholder ⇒ String?
@return — the hint the editor paints while empty (HasPlaceholder).
-
#placeholder=(text) ⇒ void
@param
text. -
#relayout ⇒ void
Places the editor across the whole rect; override to reserve cells for a face of your own.
-
#set_value(new_value, from_user:) ⇒ void
Writes
new_valueinto the editor's buffer, passingfrom_useron to Tuile::Component::AbstractStringField#set_value. -
#shown_message ⇒ StyledString, ...
The message a consumer with cells of its own paints beside the field: the verdict here, and HasBadInput widens it to prefer the field's own report, which outranks a verdict computed a pass ago.
-
#value ⇒ Object
@return — the typed value, parsed from the editor's buffer.
-
#value= ⇒ void
A programmatic write: #set_value with
from_user: false.
Methods included from HasValue
Methods included from Listeners::Declare
Methods included from HasValidation
Constructor Details
#initialize(editor) ⇒ AbstractWrappingField
@param editor — the editor to wrap; becomes this field's single child and is never swapped.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 103 def initialize(editor) super() raise TypeError, "expected AbstractStringField, got #{editor.inspect}" unless editor.is_a?(AbstractStringField) @editor = editor @last_value = empty_value # Held so the transition block can unsubscribe the same object it added: # a lambda is only equal to itself. @enter_bridge = lambda do commit_and_notify on_enter.fire(EnterEvent.new(source: self)) end # One widget, one surface: the editor paints no well of its own, so this # field's well covers it and its bg_color reaches the cells the editor paints. editor.bg_color = ComponentBackground::INHERIT bg.default_color = ComponentBackground::INPUT_WELL editor.on_value_change do |e| handle_editor_change fire_if_changed(from_user: e.from_user?) if notify_on_edit? end add_child(editor, at: 0) end |
Instance Attribute Details
#editor ⇒ AbstractStringField (readonly)
@return — the wrapped editor.
222 223 224 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 222 def editor @editor 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
205 206 207 208 209 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 205 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).
144 145 146 147 148 149 150 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 144 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(from_user: false) 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.
228 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 228 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.
265 266 267 268 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 265 def commit_and_notify commit fire_if_changed(from_user: true) end |
#cursor_position ⇒ Point?
@return — the editor's caret — the hardware cursor is delegated to it.
198 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 198 def cursor_position = editor.cursor_position |
#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.
14451 |
# File 'sig/tuile.rbs', line 14451
def empty?: () -> bool
|
#empty_value ⇒ Object
14455 |
# File 'sig/tuile.rbs', line 14455
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).
14502 |
# File 'sig/tuile.rbs', line 14502
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.
14507 |
# File 'sig/tuile.rbs', line 14507
def error_ink?: () -> bool
|
#error_message ⇒ StyledString?
@return — why the field is invalid, or nil when it
is not; nil until something sets it.
14472 |
# File 'sig/tuile.rbs', line 14472
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
14482 |
# File 'sig/tuile.rbs', line 14482
def error_message=: ((String | StyledString)? new_message) -> void
|
#fire_if_changed(from_user:) ⇒ 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.
@param from_user — what the write that moved the buffer declared.
275 276 277 278 279 280 281 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 275 def fire_if_changed(from_user:) v = value return if v == @last_value @last_value = v on_value_change.fire(HasValue::ValueChangeEvent.new(source: self, value: v, from_user:)) 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).
14462 |
# File 'sig/tuile.rbs', line 14462
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 #set_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).
252 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 252 def handle_editor_change; end |
#handle_focus ⇒ void
This method returns an undefined value.
212 213 214 215 216 217 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 212 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 empty, 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.
191 192 193 194 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 191 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.
14433 |
# File 'sig/tuile.rbs', line 14433
def inspect_details: () -> ::Array[String]
|
#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 set_value too, or a
programmatic write and an Up/Down step go unannounced until the next
commit.
243 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 243 def notify_on_edit? = true |
#on_enter ⇒ Listeners
Fired with an EnterEvent when ENTER is pressed, after #commit; see TextField#on_enter.
Empty means the field declines ENTER, which keeps it bubbling to the scope's default button — see #handle_key?. The editor's own slot is claimed only while this one is non-empty, by a bridge that commits first, so a listener here always reads a committed buffer.
178 179 180 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 178 listener :on_enter do |claimed| claimed ? editor.on_enter << @enter_bridge : editor.on_enter.remove(@enter_bridge) end |
#on_error_message_change ⇒ Listeners
Fired with an HasValidation::ErrorMessageChangeEvent whenever #error_message actually changes — never on a no-op set. The container that paints the message registers here, and so may an app painting its own: the list takes both, which a single slot could not.
14468 |
# File 'sig/tuile.rbs', line 14468
def on_error_message_change: () -> Listeners
|
#on_value_change ⇒ Listeners
Fired with a HasValue::ValueChangeEvent whenever #value actually changes — never on a no-op set.
14437 |
# File 'sig/tuile.rbs', line 14437
def on_value_change: () -> Listeners
|
#placeholder ⇒ String?
@return — the hint the editor paints while empty (HasPlaceholder).
154 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 154 def placeholder = editor.placeholder |
#placeholder=(text) ⇒ void
This method returns an undefined value.
@param text
159 160 161 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 159 def placeholder=(text) editor.placeholder = text end |
#relayout ⇒ void
This method returns an undefined value.
Places the editor across the whole rect; override to reserve cells for a face of your own.
257 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 257 def relayout = (editor.rect = local_rect) |
#set_value(new_value, from_user:) ⇒ void
This method returns an undefined value.
Writes new_value into the editor's buffer, passing from_user on to
Tuile::Component::AbstractStringField#set_value.
@param new_value
@param from_user — see HasValue#set_value.
136 137 138 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 136 def set_value(new_value, from_user:) raise(NotImplementedError, "#{self.class} must implement set_value") end |
#shown_message ⇒ StyledString, ...
The message a consumer with cells of its own paints beside the field: the verdict here, and HasBadInput widens it to prefer the field's own report, which outranks a verdict computed a pass ago.
field.on_error_message_change { message_label.caption = field.shown_message.to_s }
field.on_bad_input_change { message_label.caption = field.shown_message.to_s }
Register on both: two channels with two writers, either of which moves it.
@return — nil when there is nothing to show;
a plain String when it is the field's own bad-input report.
14495 |
# File 'sig/tuile.rbs', line 14495
def shown_message: () -> (StyledString | String)?
|
#value ⇒ Object
@return — the typed value, parsed from the editor's buffer.
128 |
# File 'lib/tuile/component/abstract_wrapping_field.rb', line 128 def value = raise(NotImplementedError, "#{self.class} must implement value") |
#value= ⇒ void
This method returns an undefined value.
A programmatic write: #set_value with from_user: false. Defined
here once and never overridden — override #set_value instead, since a
setter has no call syntax for the keyword.
@param new_value
14444 |
# File 'sig/tuile.rbs', line 14444
def value=: (Object new_value) -> void
|