Class: Tuile::Component::BigDecimalField
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::BigDecimalField
- Includes:
- HasContent, HasValue
- Defined in:
- lib/tuile/component/big_decimal_field.rb,
sig/tuile.rbs
Overview
A single-line field whose #value is a BigDecimal (or nil when
empty) — the numeric field for money, where FloatField's binary double
would round. Give it a single-row #rect:
price = Component::BigDecimalField.new
price.on_value_change = ->(d) { total.value = d } # BigDecimal or nil
price.value = BigDecimal("19.99") # field shows "19.99"
price.value = 19.99 # ArgumentError: a Float can't be exact
Only 0–9, one leading - and one . can be typed; any other
printable key is dropped without moving the caret. Up/Down step by one.
Range checks (min/max) and a display scale (19.9 → 19.90) belong
to a forms layer, not here — nothing rounds or pads what you typed.
Requires the bigdecimal gem, which Tuile does not depend on: it is a
bundled gem from Ruby 3.4 on, so a Gemfile naming it is what puts it on
the load path. Referencing this class without it raises LoadError.
Implementation details
#value is a derived parse: the buffer is the single source of truth,
recomputed on read and left exactly as typed ("19.90" keeps its zero,
which BigDecimal#to_s would not). It reads nil for a buffer that
isn't a number ("", a lone "-") but 1 / 0.5 for a half-typed
"1." / ".5", so reaching for the decimal point doesn't blink the
value to nil and back through HasValue#on_value_change — which fires per
keystroke, but only on a real value change ("1.0"→"1.00" is silent,
since the two compare equal).
Both ends of that round-trip are written here rather than left to the
library, because bigdecimal 3.1 (Ruby 3.3's default gem) and 4.x
disagree about them: 3.1 rejects BigDecimal("1.") and BigDecimal(0.1)
where 4.x accepts both. So the buffer is normalized before parsing, a
Float is refused on both, and display goes through to_s("F") — plain
notation, never BigDecimal#to_s's "0.1999e2".
It composes a TextField (its single HasContent child) rather than
subclassing one, so its face carries only the typed HasValue seam,
never the widget's String-typed text.
UI-thread-confined, like every component (see Screen).
Constant Summary collapse
- NUMERIC =
A buffer #value parses: an optional sign and digits with an optional fractional part (either side may be empty, but not both). No exponent —
to_s("F")never writes one and no key types ane. /\A-?(?:\d+(?:\.\d*)?|\.\d+)\z/
Instance Attribute Summary
Attributes included from HasValue
Attributes included from HasContent
Instance Method Summary collapse
-
#accepts?(char) ⇒ Boolean
Whether
charmay be inserted. -
#clear ⇒ void
Resets #value to #empty_value.
-
#coerce(new_value) ⇒ ::BigDecimal
@param
new_value. -
#cursor_position ⇒ Point?
@return — the field's caret (the hardware cursor is delegated to the inner field).
-
#empty? ⇒ Boolean
@return — true iff #value equals #empty_value.
-
#empty_value ⇒ void
nil, not"": a numeric field with no parseable number is empty. -
#field_key(key) ⇒ Boolean
The field's key interceptor, consulted before the field acts on the key — which is what lets a rejected character be swallowed without the caret ever moving.
-
#fire_if_changed ⇒ void
Re-emits HasValue#on_value_change with the freshly-parsed #value, but only when it differs from the last one fired — so a buffer edit that leaves the value unchanged (
"1.0"→"1.00") stays silent. -
#focusable? ⇒ Boolean
Input fields are focusable by default (overrides #focusable?); a read-only display field could override back to
false. -
#handle_mouse ⇒ void
@param
event. -
#initialize ⇒ BigDecimalField
constructor
A new instance of BigDecimalField.
-
#layout(field) ⇒ void
Places the wrapped field across the whole rect (HasContent hook).
-
#normalize(text) ⇒ String
Rewrites the half-typed shapes NUMERIC admits into ones every
bigdecimalversion parses:".5"→"0.5","1."→"1". -
#on_enter ⇒ Proc, ...
Fired when ENTER is pressed in the field; see TextField#on_enter.
-
#on_enter=(callback) ⇒ void
@param
callback. - #on_focus ⇒ void
-
#rect= ⇒ void
@param
rect. -
#step(delta) ⇒ void
Nudges #value by
delta, treating an empty/un-parseable field as zero. -
#value ⇒ ::BigDecimal?
@return — the parsed buffer;
nilwhen empty or not a number (e.g. a lone"-"). -
#value=(new_value) ⇒ void
Writes
new_valueinto the buffer in plain notation and parks the caret at its end; fires HasValue#on_value_change only if the value actually changed.
Constructor Details
#initialize ⇒ BigDecimalField
Returns a new instance of BigDecimalField.
67 68 69 70 71 72 73 74 |
# File 'lib/tuile/component/big_decimal_field.rb', line 67 def initialize super() @last_value = nil field = TextField.new field.on_change = ->(_text) { fire_if_changed } field.on_key = method(:field_key) self.content = field end |
Instance Method Details
#accepts?(char) ⇒ Boolean
Whether char may be inserted. Deliberately shallow: it keeps the
buffer typeable rather than always-valid — a transient "-" or
"1." has to be reachable — and #value decides what parses.
@param char — a single printable character.
177 178 179 180 181 182 183 184 |
# File 'lib/tuile/component/big_decimal_field.rb', line 177 def accepts?(char) case char when /\A[0-9]\z/ then true when "-" then content.caret.zero? && !content.text.start_with?("-") when "." then !content.text.include?(".") else false end end |
#clear ⇒ void
This method returns an undefined value.
Resets #value to #empty_value.
5171 |
# File 'sig/tuile.rbs', line 5171
def clear: () -> void
|
#coerce(new_value) ⇒ ::BigDecimal
@param new_value
141 142 143 144 145 146 147 148 149 150 |
# File 'lib/tuile/component/big_decimal_field.rb', line 141 def coerce(new_value) if new_value.is_a?(Float) raise ArgumentError, "a Float is not exact — pass BigDecimal(#{new_value.to_s.inspect}) or the String" end big = BigDecimal(new_value) raise ArgumentError, "value must be finite, got #{big}" unless big.finite? big end |
#cursor_position ⇒ Point?
@return — the field's caret (the hardware cursor is delegated to the inner field).
104 |
# File 'lib/tuile/component/big_decimal_field.rb', line 104 def cursor_position = content.cursor_position |
#empty? ⇒ Boolean
@return — true iff #value equals #empty_value.
5168 |
# File 'sig/tuile.rbs', line 5168
def empty?: () -> bool
|
#empty_value ⇒ void
This method returns an undefined value.
nil, not "": a numeric field with no parseable number is empty.
100 |
# File 'lib/tuile/component/big_decimal_field.rb', line 100 def empty_value = nil |
#field_key(key) ⇒ Boolean
The field's key interceptor, consulted before the field acts on the key — which is what lets a rejected character be swallowed without the caret ever moving.
@param key
@return — true to consume the key.
157 158 159 160 161 162 163 164 |
# File 'lib/tuile/component/big_decimal_field.rb', line 157 def field_key(key) case key when Keys::UP_ARROW then step(1) when Keys::DOWN_ARROW then step(-1) else return Keys.printable?(key) && !accepts?(key) end true end |
#fire_if_changed ⇒ void
This method returns an undefined value.
Re-emits HasValue#on_value_change with the freshly-parsed #value, but only
when it differs from the last one fired — so a buffer edit that leaves
the value unchanged ("1.0"→"1.00") stays silent.
190 191 192 193 194 195 196 |
# File 'lib/tuile/component/big_decimal_field.rb', line 190 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 (DECISIONS.md
D-integer-field).
5178 |
# File 'sig/tuile.rbs', line 5178
def focusable?: () -> bool
|
#handle_mouse ⇒ void
This method returns an undefined value.
@param event
5181 |
# File 'sig/tuile.rbs', line 5181
def handle_mouse: (MouseEvent event) -> void
|
#layout(field) ⇒ void
This method returns an undefined value.
Places the wrapped field across the whole rect (HasContent hook).
@param field
121 |
# File 'lib/tuile/component/big_decimal_field.rb', line 121 def layout(field) = (field.rect = rect) |
#normalize(text) ⇒ String
129 130 131 132 |
# File 'lib/tuile/component/big_decimal_field.rb', line 129 def normalize(text) text = text.sub(".", "0.") if text.start_with?(".", "-.") text.chomp(".") end |
#on_enter ⇒ Proc, ...
Fired when ENTER is pressed in the field; see TextField#on_enter.
@return — no-arg callable, or nil.
108 |
# File 'lib/tuile/component/big_decimal_field.rb', line 108 def on_enter = content.on_enter |
#on_enter=(callback) ⇒ void
This method returns an undefined value.
@param callback
112 113 114 |
# File 'lib/tuile/component/big_decimal_field.rb', line 112 def on_enter=(callback) content.on_enter = callback end |
#on_focus ⇒ void
This method returns an undefined value.
5186 |
# File 'sig/tuile.rbs', line 5186
def on_focus: () -> void
|
#rect= ⇒ void
This method returns an undefined value.
@param rect
5184 |
# File 'sig/tuile.rbs', line 5184
def rect=: (Rect rect) -> void
|
#step(delta) ⇒ void
This method returns an undefined value.
Nudges #value by delta, treating an empty/un-parseable field as
zero.
@param delta
170 |
# File 'lib/tuile/component/big_decimal_field.rb', line 170 def step(delta) = (self.value = (value || BigDecimal(0)) + delta) |
#value ⇒ ::BigDecimal?
@return — the parsed buffer; nil when empty or not a
number (e.g. a lone "-").
78 79 80 81 |
# File 'lib/tuile/component/big_decimal_field.rb', line 78 def value text = content.text text.match?(NUMERIC) ? BigDecimal(normalize(text)) : nil end |
#value=(new_value) ⇒ void
This method returns an undefined value.
Writes new_value into the buffer in plain notation and parks the
caret at its end; fires HasValue#on_value_change only if the value actually
changed.
@param new_value — nil empties the field. A Float is refused, not converted — see the raise.
93 94 95 96 |
# File 'lib/tuile/component/big_decimal_field.rb', line 93 def value=(new_value) content.text = new_value.nil? ? "" : coerce(new_value).to_s("F") content.caret = content.text.length end |