Class: Tuile::Component::BigDecimalField
- Inherits:
-
AbstractWrappingField
- Object
- Component
- AbstractWrappingField
- Tuile::Component::BigDecimalField
- Includes:
- HasBadInput
- 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
The buffer only ever holds 0–9, one leading - and one .: a key
that would break that is dropped without moving the caret, and so is a
paste that would ("$19.99" lands nothing, rather than sieving through
as a price the user never copied). 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 wraps a TextField rather than subclassing one, so its face carries
only the typed HasValue seam and never the widget's String-typed
text; AbstractWrappingField supplies the wrapping.
UI-thread-confined, like every component (see Screen).
Defined Under Namespace
Classes: Field
Constant Summary collapse
- BAD_INPUT_MESSAGE =
Returns what #bad_input_message reports for a buffer that is typeable but not a decimal.
"not a decimal number"- 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 HasValidation
Attributes inherited from AbstractWrappingField
Attributes included from HasValue
Instance Method Summary collapse
-
#bad_input? ⇒ Boolean
@return — true iff the field is holding input its value cannot represent.
-
#bad_input_message ⇒ String?
"-","."and"-."are typeable and parse to nothing; an empty buffer is empty, not bad (HasBadInput). -
#bad_input_settled? ⇒ Boolean
Whether bad input may paint the well yet.
-
#coerce(new_value) ⇒ ::BigDecimal
@param
new_value. -
#empty_value ⇒ void
nil, not"": a numeric field with no parseable number is empty. -
#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
Widens HasValidation#error_ink?: bad input paints the invalid well too, with no verdict written — once #bad_input_settled? says the report may be shown.
-
#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. -
#initialize ⇒ BigDecimalField
constructor
A new instance of BigDecimalField.
-
#inspect_details ⇒ ::Array[String]
Adds
error_message=…to #inspect, omitted while valid — so a Testing.get failure dump says which field is already flagged. -
#normalize(text) ⇒ String
Rewrites the half-typed shapes NUMERIC admits into ones every
bigdecimalversion parses:".5"→"0.5","1."→"1". -
#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.
Methods inherited from AbstractWrappingField
#active=, #clear, #commit, #commit_and_notify, #cursor_position, #default_bg_color, #empty?, #fire_if_changed, #focusable?, #handle_editor_change, #handle_focus, #handle_key?, #layout, #notify_on_edit?, #placeholder, #placeholder=, #rect=
Methods included from HasPlaceholder
Methods included from HasValue
Constructor Details
#initialize ⇒ BigDecimalField
Returns a new instance of BigDecimalField.
98 99 100 101 102 103 |
# File 'lib/tuile/component/big_decimal_field.rb', line 98 def initialize super(Field.new) # Not the general on_key interceptor: that slot stays free for the app. editor.on_key_up = -> { step(1) } editor.on_key_down = -> { step(-1) } end |
Instance Method Details
#bad_input? ⇒ Boolean
@return — true iff the field is holding input its value cannot represent.
10734 |
# File 'sig/tuile.rbs', line 10734 def bad_input?: () -> bool |
#bad_input_message ⇒ String?
"-", "." and "-." are typeable and parse to nothing; an empty
buffer is empty, not bad (HasBadInput).
134 |
# File 'lib/tuile/component/big_decimal_field.rb', line 134 def = value.nil? && !editor.text.empty? ? BAD_INPUT_MESSAGE : nil |
#bad_input_settled? ⇒ Boolean
Whether bad input may paint the well yet. true here, so the well is
as continuous as the report: a FloatField reddens at the half-typed
"1.", which is a fair warning while the residue is one or two
transient buffers. Override it to latch where the grammar makes
every prefix bad input, or the well is red for the whole time the
user types a correct value:
def bad_input_settled? = @settled # set on commit, cleared on an edit
It gates the ink only: #bad_input? is a pull, and a save gate
asking at a click must get the answer settled or not (design/decisions.md
D_bad_input).
10753 |
# File 'sig/tuile.rbs', line 10753 def bad_input_settled?: () -> bool |
#coerce(new_value) ⇒ ::BigDecimal
@param new_value
154 155 156 157 158 159 160 161 162 163 |
# File 'lib/tuile/component/big_decimal_field.rb', line 154 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 |
#empty_value ⇒ void
This method returns an undefined value.
nil, not "": a numeric field with no parseable number is empty.
129 |
# File 'lib/tuile/component/big_decimal_field.rb', line 129 def empty_value = nil |
#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).
10774 |
# File 'sig/tuile.rbs', line 10774 def error_bg_color: () -> Color? |
#error_ink? ⇒ Boolean
Widens HasValidation#error_ink?: bad input paints the invalid well too, with no verdict written — once #bad_input_settled? says the report may be shown.
10739 |
# File 'sig/tuile.rbs', line 10739 def error_ink?: () -> bool |
#error_message ⇒ StyledString?
@return — why the field is invalid, or nil when it
is not; nil until something sets it.
10757 |
# File 'sig/tuile.rbs', line 10757 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
10767 |
# File 'sig/tuile.rbs', line 10767 def error_message=: ((String | StyledString)? ) -> void |
#inspect_details ⇒ ::Array[String]
Adds error_message=… to Tuile::Component#inspect, omitted while valid — so
a Testing.get failure dump says which field is already flagged.
10778 |
# File 'sig/tuile.rbs', line 10778 def inspect_details: () -> ::Array[String] |
#normalize(text) ⇒ String
142 143 144 145 |
# File 'lib/tuile/component/big_decimal_field.rb', line 142 def normalize(text) text = text.sub(".", "0.") if text.start_with?(".", "-.") text.chomp(".") end |
#step(delta) ⇒ void
This method returns an undefined value.
Nudges #value by delta, treating an empty/un-parseable field as
zero.
@param delta
169 |
# File 'lib/tuile/component/big_decimal_field.rb', line 169 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 "-").
107 108 109 110 |
# File 'lib/tuile/component/big_decimal_field.rb', line 107 def value text = editor.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.
122 123 124 125 |
# File 'lib/tuile/component/big_decimal_field.rb', line 122 def value=(new_value) editor.text = new_value.nil? ? "" : coerce(new_value).to_s("F") editor.caret = editor.text.length end |