Class: Tuile::Component::FloatField

Inherits:
Component
  • Object
show all
Includes:
HasContent, HasValue
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

Only 09, one leading - and one . can be typed; any other printable key is dropped without moving the caret. 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, though no key types an e.

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, 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 HasValue

#on_value_change

Attributes included from HasContent

#content

Instance Method Summary collapse

Constructor Details

#initializeFloatField

Returns a new instance of FloatField.



47
48
49
50
51
52
53
54
# File 'lib/tuile/component/float_field.rb', line 47

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.

Parameters:

  • char (String)

Returns:

  • (Boolean)


139
140
141
142
143
144
145
146
# File 'lib/tuile/component/float_field.rb', line 139

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

#clearvoid

This method returns an undefined value.

Resets #value to #empty_value.



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

def clear: () -> void

#coerce(new_value) ⇒ Float

@param new_value

Parameters:

  • new_value (Numeric)

Returns:

  • (Float)


107
108
109
110
111
112
# File 'lib/tuile/component/float_field.rb', line 107

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

  float
end

#cursor_positionPoint?

@return — the field's caret (the hardware cursor is delegated to the inner field).

Returns:



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

def cursor_position = content.cursor_position

#empty?Boolean

@return — true iff #value equals #empty_value.

Returns:

  • (Boolean)


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

def empty?: () -> bool

#empty_valuevoid

This method returns an undefined value.

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



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

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.

Parameters:

  • key (String)

Returns:

  • (Boolean)


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

def field_key(key)
  case key
  when Keys::UP_ARROW then step(1.0)
  when Keys::DOWN_ARROW then step(-1.0)
  else return Keys.printable?(key) && !accepts?(key)
  end
  true
end

#fire_if_changedvoid

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 ("7""07") stays silent.



152
153
154
155
156
157
158
# File 'lib/tuile/component/float_field.rb', line 152

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

Returns:

  • (Boolean)


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

def focusable?: () -> bool

#handle_mousevoid

This method returns an undefined value.

@param event

Parameters:



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

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

Parameters:



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

def layout(field) = (field.rect = rect)

#on_enterProc, ...

Fired when ENTER is pressed in the field; see TextField#on_enter.

@return — no-arg callable, or nil.

Returns:

  • (Proc, Method, nil)


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

def on_enter = content.on_enter

#on_enter=(callback) ⇒ void

This method returns an undefined value.

@param callback

Parameters:

  • callback (Proc, Method, nil)


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

def on_enter=(callback)
  content.on_enter = callback
end

#on_focusvoid

This method returns an undefined value.



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

def on_focus: () -> void

#rect=void

This method returns an undefined value.

@param rect

Parameters:



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

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

@param delta

Parameters:

  • delta (Float)


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

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)


58
59
60
61
# File 'lib/tuile/component/float_field.rb', line 58

def value
  text = content.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)


70
71
72
73
# File 'lib/tuile/component/float_field.rb', line 70

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