Class: Tuile::Component::FloatField

Inherits:
AbstractWrappingField show all
Includes:
HasBadInput
Defined in:
lib/tuile/component/float_field.rb,
sig/tuile.rbs

Overview

A single-line field whose #value is a Float (or nil when empty) — the IntegerField twin, one Ruby type over. Give it a single-row #rect:

field = Component::FloatField.new
field.on_value_change = ->(x) { puts x.inspect }  # Float or nil, per change
field.value = 19.99                               # field shows "19.99"
field.clear                                       # empties it; value => nil

The buffer only ever holds 09, one leading -, one . and an optional exponent: a key that would break that is dropped without moving the caret, and so is a paste that would (a European "1,5" lands nothing, rather than sieving through as the plausible, wrong "15"). Up/Down step by 1.0 (an empty field counting as 0.0). A Float is a binary double, so this is the wrong field for money — hold that as Integer cents in an IntegerField — and range checks (min/max) belong to a forms layer, not here.

Implementation details

#value is a derived parse: the buffer is the single source of truth, recomputed on read and left exactly as typed ("007" keeps its zeros). It reads nil for a buffer that isn't a number ("", a lone "-") but 1.0 / 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 ("7""07" is silent). The parse also accepts the exponent Float#to_s writes for extreme magnitudes, so value = 1e-5 round-trips through the "1.0e-05" it displays — and e is typeable, so what the field displays is always something the user can go on editing.

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 number.

Returns:

  • (String)

    what #bad_input_message reports for a buffer that is typeable but not a number.

"not a number"
NUMERIC =

A buffer #value parses: an optional sign, digits with an optional fractional part (either side may be empty, but not both), and the exponent #value= can write.

Returns:

  • (Regexp)
/\A-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?\z/

Instance Attribute Summary

Attributes included from HasValidation

#on_error_message_change

Attributes inherited from AbstractWrappingField

#editor, #on_enter

Attributes included from HasValue

#on_value_change

Instance Method Summary collapse

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

#placeholder, #placeholder=

Methods included from HasValue

#clear, #empty?, #focusable?

Constructor Details

#initializeFloatField

Returns a new instance of FloatField.



79
80
81
82
83
84
# File 'lib/tuile/component/float_field.rb', line 79

def initialize
  super(Field.new)
  # Not the general on_key interceptor: that slot stays free for the app.
  editor.on_key_up = -> { step(1.0) }
  editor.on_key_down = -> { step(-1.0) }
end

Instance Method Details

#bad_input?Boolean

@return — true iff the field is holding input its value cannot represent.

Returns:

  • (Boolean)


8608
# File 'sig/tuile.rbs', line 8608

def bad_input?: () -> bool

#bad_input_messageString?

"-", ".", "-." and every exponent in progress ("1e", "1.0e-", …) are typeable and parse to nothing; an empty buffer is empty, not bad (HasBadInput).

Returns:

  • (String, nil)


113
# File 'lib/tuile/component/float_field.rb', line 113

def bad_input_message = 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 Tuile::Component::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).

Returns:

  • (Boolean)


8627
# File 'sig/tuile.rbs', line 8627

def bad_input_settled?: () -> bool

#coerce(new_value) ⇒ Float

@param new_value

Parameters:

  • new_value (Numeric)

Returns:

  • (Float)


122
123
124
125
126
127
# File 'lib/tuile/component/float_field.rb', line 122

def coerce(new_value)
  float = Float(new_value)
  raise ArgumentError, "value must be finite, got #{float}" unless float.finite?

  float
end

#empty_valuevoid

This method returns an undefined value.

nil, not "": a numeric field with no parseable number is empty.



107
# File 'lib/tuile/component/float_field.rb', line 107

def empty_value = nil

#error_bg_colorColor?

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).

Returns:



8648
# File 'sig/tuile.rbs', line 8648

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.

Returns:

  • (Boolean)


8613
# File 'sig/tuile.rbs', line 8613

def error_ink?: () -> bool

#error_messageStyledString?

@return — why the field is invalid, or nil when it is not; nil until something sets it.

Returns:



8631
# File 'sig/tuile.rbs', line 8631

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

Parameters:



8641
# File 'sig/tuile.rbs', line 8641

def error_message=: ((String | StyledString)? new_message) -> 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.

Returns:

  • (::Array[String])


8652
# File 'sig/tuile.rbs', line 8652

def inspect_details: () -> ::Array[String]

#step(delta) ⇒ void

This method returns an undefined value.

Nudges #value by delta, treating an empty/un-parseable field as 0.0.

@param delta

Parameters:

  • delta (Float)


133
# File 'lib/tuile/component/float_field.rb', line 133

def step(delta) = (self.value = (value || 0.0) + delta)

#valueFloat?

@return — the parsed buffer; nil when empty or not a number (e.g. a lone "-").

Returns:

  • (Float, nil)


88
89
90
91
# File 'lib/tuile/component/float_field.rb', line 88

def value
  text = editor.text
  text.match?(NUMERIC) ? text.to_f : nil
end

#value=(new_value) ⇒ void

This method returns an undefined value.

Writes new_value into the buffer and parks the caret at its end; fires HasValue#on_value_change only if the value actually changed.

@param new_valuenil empties the field; anything else is coerced with Float(), so an Integer 3 shows as "3.0".

Parameters:

  • new_value (Numeric, nil)


100
101
102
103
# File 'lib/tuile/component/float_field.rb', line 100

def value=(new_value)
  editor.text = new_value.nil? ? "" : coerce(new_value).to_s
  editor.caret = editor.text.length
end