Class: Tuile::Component::Checkbox

Inherits:
Component
  • Object
show all
Includes:
HasCaption, HasValue
Defined in:
lib/tuile/component/checkbox.rb,
sig/tuile.rbs

Overview

A boolean input on one row. Space, Enter or a left click toggles it:

[x] Enable syslog forwarding
[ ] Enable syslog forwarding

cb = Component::Checkbox.new("Enable syslog forwarding", value: true)
cb.on_value_change { |e| config.syslog = e.value }
cb.toggle       # unchecks it, firing the listener with false
cb.checked?     # => false

#value is the canonical seam (HasValue), always true/false and never nil; #checked? / #checked= / #toggle are the domain-word face over it — one piece of state, four names. Unchecked is the #empty_value, so a fresh checkbox is empty and HasValue#clear unchecks.

Space and Enter both toggle — same as a checkable row in a List (CheckboxGroup, RadioGroup), so the gesture reads the same standalone and grouped. A focused checkbox therefore consumes Enter: a form's Enter-to-submit on an ancestor won't see it, exactly as with a focused Button or TextArea. Which widget lets Enter through is per widget, never a framework guarantee — book ch5's Enter table is the list.

A tab stop, so Tab lands on it, and the widget highlights while on the focus chain. Assign a #rect (typically from the surrounding Layout) at least caption.display_width + 4 wide; a narrower one ellipsizes the caption, a wider one leaves a dead tail — see #extent.

Implementation details

The glyphs are a house convention rather than constants: three columns plus a trailing space ([x] , [ ] ), ASCII because ☑/☐ are absent from most monospace fonts and the fallback glyph bleeds over its cell. A widget painting checkbox-like rows without instantiating a Checkbox — checkable rows in a List — repeats those literals to match.

Instance Method Summary collapse

Methods included from HasValue

listener

Methods included from Listeners::Declare

#listener

Methods included from HasValidation

listener

Constructor Details

#initialize(caption = nil, value: false) ⇒ Checkbox

@param caption — the label, coerced as HasCaption#caption= coerces it.

@param value — initial state. Assigned through #value=, which also seeds the backing ivar — an unseeded checkbox would read nil and so report itself non-empty while fresh.

Parameters:

  • caption (?(String | StyledString), nil) (defaults to: nil)
  • value: (Boolean) (defaults to: false)


48
49
50
51
52
# File 'lib/tuile/component/checkbox.rb', line 48

def initialize(caption = nil, value: false)
  super()
  self.caption = caption
  self.value = value
end

Instance Method Details

#caption ⇒ StyledString

Read through this method, never @caption — the ivar stays nil until the first non-empty set (#caption= short-circuits when unchanged).

@return — the caption; empty when never set.

Returns:



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

def caption: () -> StyledString

#caption= ⇒ void

This method returns an undefined value.

Sets the caption and invalidates the component. No-op when unchanged. A String is parsed via StyledString.parse (embedded ANSI is honored); a StyledString is used as-is; nil clears it.

@param new_caption

Parameters:



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

def caption=: ((String | StyledString)? new_caption) -> void

#checked=(new_value) ⇒ void

This method returns an undefined value.

#value= under its domain word. A delegator rather than an alias, so it keeps routing through the one write path, #set_value.

@param new_value — anything; truthiness decides.

Parameters:

  • new_value (Object)


77
78
79
# File 'lib/tuile/component/checkbox.rb', line 77

def checked=(new_value)
  self.value = new_value
end

#checked? ⇒ Boolean

@return — #value under its domain word — license.checked? reads better than license.value. Not a second piece of state.

Returns:

  • (Boolean)


71
# File 'lib/tuile/component/checkbox.rb', line 71

def checked? = value

#clear ⇒ void

This method returns an undefined value.

Resets #value to #empty_value.

An includer whose input can outrun its value (HasBadInput) must clear the input: a field holding bad input already reads empty_value, so inheriting this default — over a #set_value that returns early on a no-op set — is a clear that leaves the garbage on screen. A clear is programmatic: its event says from_user? == false.



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

def clear: () -> void

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

Returns:

  • (Boolean)


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

def empty?: () -> bool

#empty_value ⇒ Boolean

@return — false — HasValue#empty? means unchecked.

Returns:

  • (Boolean)


57
# File 'lib/tuile/component/checkbox.rb', line 57

def empty_value = false

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

Returns:



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

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.

Returns:

  • (Boolean)


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

def error_ink?: () -> bool

#error_message ⇒ StyledString?

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

Returns:



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

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:



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

def error_message=: ((String | StyledString)? new_message) -> void

#extent ⇒ Size

The cells the widget actually paints: one row, caption.display_width + 4 columns, clipped to Tuile::Component#rect. A form column routinely hands a checkbox a 40-column rect for a 22-column [ ] Enable syslog forwarding — the extent is those 22 columns.

Both the focus highlight and the click hit test use it, so a click on the blank tail — or on a lower row, when the rect is taller than one — does not toggle. It still focuses: Mouse::Router's click-to-focus is ungated by geometry, and the tail is the field's own row.

The extent ignores Tuile::Component#bg_color: an inherited tint paints the dead tail, but a hit test that silently widened with a background would be a mode switch invisible in the code and untestable by inspection.

Returns:



100
# File 'lib/tuile/component/checkbox.rb', line 100

def extent = Size.new([caption.display_width + 4, rect.width].min, 1)

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

Returns:

  • (Boolean)


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

def focusable?: () -> bool

#handle_key?(key) ⇒ Boolean

Toggles on Space or Enter. Every other key is left unhandled so it bubbles to an ancestor.

@param key

Parameters:

  • key (String)

Returns:

  • (Boolean)


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

def handle_key?(key)
  return false unless [" ", Keys::ENTER].include?(key)

  set_value(!value, from_user: true)
  true
end

#handle_mouse_down?(event) ⇒ Boolean

Toggles on a left press; a press on the dead tail past #extent focuses the checkbox without reaching here.

@param event

Parameters:

Returns:

  • (Boolean)


117
118
119
120
121
122
# File 'lib/tuile/component/checkbox.rb', line 117

def handle_mouse_down?(event)
  return false unless event.button == :left

  set_value(!value, from_user: true)
  true
end

#inspect_details ⇒ ::Array[String]

Adds caption="…" to Tuile::Component#inspect, omitted while empty.

Returns:

  • (::Array[String])


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

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

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

Returns:



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

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.

Returns:



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

def on_value_change: () -> Listeners

#repaint(canvas) ⇒ void

This method returns an undefined value.

@param canvas — see Tuile::Component#repaint.

Parameters:



126
127
128
129
130
131
132
133
# File 'lib/tuile/component/checkbox.rb', line 126

def repaint(canvas)
  super
  return if rect.empty?

  label = (StyledString.plain(value ? "[x] " : "[ ] ") + caption).ellipsize(rect.width)
  label = label.with_bg(screen.theme.active_bg_color) if active?
  canvas.set_text(0, 0, label)
end

#set_value(new_value, from_user:) ⇒ void

This method returns an undefined value.

Coerces to true/false before storing, so the two-state invariant holds whatever a caller assigns — and cb.value = nil on a fresh checkbox is the no-op it looks like rather than a spurious change event.

@param new_value — anything; truthiness decides.

@param from_user — see HasValue#set_value.

Parameters:

  • new_value (Object)
  • from_user: (Boolean)


65
66
67
# File 'lib/tuile/component/checkbox.rb', line 65

def set_value(new_value, from_user:)
  super(new_value ? true : false, from_user:)
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.

Returns:



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

def shown_message: () -> (StyledString | String)?

#tab_stop? ⇒ Boolean

Returns:

  • (Boolean)


54
# File 'lib/tuile/component/checkbox.rb', line 54

def tab_stop? = true

#toggle ⇒ void

This method returns an undefined value.

Flips #value — programmatically; Space, Enter and a click flip it as the user's (HasValue::ValueChangeEvent#from_user?).



84
# File 'lib/tuile/component/checkbox.rb', line 84

def toggle = (self.value = !value)

#value ⇒ Object

@return — the current value; nil until first set.

Returns:

  • (Object)


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

def value: () -> Object

#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

Parameters:

  • new_value (Object)


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

def value=: (Object new_value) -> void