Class: Tuile::Component::CheckboxGroup
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::CheckboxGroup
- Includes:
- HasContent, HasValue
- Defined in:
- lib/tuile/component/checkbox_group.rb,
sig/tuile.rbs
Overview
Multi-select from a set of typed items, one checkable row each. Arrows move a cursor; Space, Enter or a left click toggles the row under it:
[x] Errors
[ ] Warnings <- cursor row, highlighted across the full width
[x] Info
^ the composed {List}'s one-column gutter
cg = Component::CheckboxGroup.new(items: %w[Errors Warnings Info])
cg.value = %w[Errors Info] # any Enumerable, stored as a Set
cg.on_value_change = ->(set) { filter(set) } # once per toggle
cg.value # => #<Set: {"Errors", "Info"}>
cg.item_label = ->(level) { level.name } # default :to_s
#value is a frozen Set of the selected items themselves — of
whatever type #items holds, never their labels. Frozen so cg.value << item fails loudly rather than mutating the selection behind
HasValue#on_value_change's back; assign a new set or an Array instead.
Treat it as unordered: it iterates in toggle order, so use
cg.items & cg.value.to_a when you need #items order.
Composes rather than subclasses, like ComboBox: a List is its single
HasContent child, which is where the cursor, scrolling, the scrollbar and
per-row mouse hit-testing come from. content is that list, so an app can
tune it (scrollbar_visibility, show_cursor_when_inactive, …). Rows
beyond #rect's height scroll; the inner list is the tab stop, not the
group.
items is chrome; value is authoritative
#items= changes only what is presented. It never touches #value and
never fires HasValue#on_value_change, and a selected item absent from
#items renders no checked row while surviving intact — so a form saved
without the user editing anything changes nothing silently. Keeping the two
in sync is the app's job: cg.value &= cg.items.to_set reconciles them.
Same contract as Tuile::Component::ComboBox#value, one item at a time.
There is no select-all — neither a key nor a header row. An app that wants
one writes cg.value = cg.items behind its own affordance.
Implementation details
Items need stable #hash/#eql?, since the selection is a Set: an item
mutated after being selected becomes unfindable. Two ==-equal items also
share one selection — their rows check and uncheck together — whereas two
distinct items that merely render the same label toggle independently,
because a row resolves to an item by index.
Rows repeat Checkbox's [x] /[ ] glyph convention rather than
importing a constant from it.
UI-thread-confined, like every component (see Screen).
Constant Summary collapse
- EMPTY_SELECTION =
Set.new.freeze
Instance Attribute Summary collapse
-
#item_label ⇒ Proc, Method
@return — item -> row label (a
String, StyledString, or anything with#to_s);:to_sby default. -
#items ⇒ ::Array[untyped]
@return — the presented items.
Attributes included from HasValue
Attributes included from HasContent
Instance Method Summary collapse
-
#clear ⇒ void
Resets #value to #empty_value.
-
#coerce(new_value) ⇒ ::Set[untyped]
@param
new_value. -
#empty? ⇒ Boolean
@return — true iff #value equals #empty_value.
-
#empty_value ⇒ ::Set[untyped]
@return — the frozen empty set — HasValue#empty? means nothing is selected.
-
#focusable? ⇒ Boolean
Input fields are focusable by default (overrides #focusable?); a read-only display field could override back to
false. -
#handle_key(key) ⇒ Boolean
Toggles the cursor row on Space.
-
#handle_mouse ⇒ void
@param
event. -
#initialize(items: [], value: nil) ⇒ CheckboxGroup
constructor
@param
items— the items to present, one row each; also settable via #items=. -
#label_for(item) ⇒ StyledString, String
@param
item. -
#layout(list) ⇒ void
Places the composed list across the whole rect (HasContent hook).
- #on_focus ⇒ void
-
#rebuild_rows ⇒ void
Re-renders every row from the current items, labels and selection.
-
#rect= ⇒ void
@param
rect. -
#toggle_at(index) ⇒ void
Flips membership of the item on row
index; an index outside #items is ignored. -
#value ⇒ Object
@return — the current value;
niluntil first set. -
#value=(new_value) ⇒ void
Replaces the selection, firing HasValue#on_value_change when it really changed.
Constructor Details
#initialize(items: [], value: nil) ⇒ CheckboxGroup
@param items — the items to present, one row each; also settable via #items=.
@param value — the initial selection. Seeds the backing ivar directly, so no listener fires and assignment order doesn't matter to a form helper.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/tuile/component/checkbox_group.rb', line 68 def initialize(items: [], value: nil) super() @items = items.to_a @item_label = :to_s.to_proc @value = coerce(value) @on_value_change = nil list = List.new # A List has no cursor at all by default (Cursor::None, position -1). list.cursor = List::Cursor.new list.on_item_chosen = ->(index, _line) { toggle_at(index) } self.content = list rebuild_rows end |
Instance Attribute Details
#item_label ⇒ Proc, Method
@return — item -> row label (a String, StyledString, or
anything with #to_s); :to_s by default.
88 89 90 |
# File 'lib/tuile/component/checkbox_group.rb', line 88 def item_label @item_label end |
#items ⇒ ::Array[untyped]
@return — the presented items.
84 85 86 |
# File 'lib/tuile/component/checkbox_group.rb', line 84 def items @items end |
Instance Method Details
#clear ⇒ void
This method returns an undefined value.
Resets #value to #empty_value.
4270 |
# File 'sig/tuile.rbs', line 4270
def clear: () -> void
|
#coerce(new_value) ⇒ ::Set[untyped]
@param new_value
@return — a frozen copy; nil becomes #empty_value.
172 173 174 175 176 177 |
# File 'lib/tuile/component/checkbox_group.rb', line 172 def coerce(new_value) return empty_value if new_value.nil? raise TypeError, "expected Enumerable, got #{new_value.inspect}" unless new_value.is_a?(Enumerable) Set.new(new_value).freeze end |
#empty? ⇒ Boolean
@return — true iff #value equals #empty_value.
4267 |
# File 'sig/tuile.rbs', line 4267
def empty?: () -> bool
|
#empty_value ⇒ ::Set[untyped]
@return — the frozen empty set — HasValue#empty? means nothing is selected.
110 |
# File 'lib/tuile/component/checkbox_group.rb', line 110 def empty_value = EMPTY_SELECTION |
#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).
4277 |
# File 'sig/tuile.rbs', line 4277
def focusable?: () -> bool
|
#handle_key(key) ⇒ Boolean
Toggles the cursor row on Space. Nothing else is claimed: the composed List — being the focused component — has already had its chance at the key (its arrows, Home/End, PgUp/PgDn, ^U/^D and Enter), and whatever neither of us wants bubbles on to an ancestor.
@param key
134 135 136 137 138 139 |
# File 'lib/tuile/component/checkbox_group.rb', line 134 def handle_key(key) return false unless key == " " toggle_at(content.cursor.position) true end |
#handle_mouse ⇒ void
This method returns an undefined value.
@param event
4280 |
# File 'sig/tuile.rbs', line 4280
def handle_mouse: (MouseEvent event) -> void
|
#label_for(item) ⇒ StyledString, String
@param item
@return — whichever StyledString#+ accepts on the right — so a styled label keeps its spans and a plain one is parsed.
182 183 184 185 |
# File 'lib/tuile/component/checkbox_group.rb', line 182 def label_for(item) label = @item_label.call(item) label.is_a?(StyledString) ? label : label.to_s end |
#layout(list) ⇒ void
This method returns an undefined value.
Places the composed list across the whole rect (HasContent hook).
@param list
146 |
# File 'lib/tuile/component/checkbox_group.rb', line 146 def layout(list) = (list.rect = rect) |
#on_focus ⇒ void
This method returns an undefined value.
4285 |
# File 'sig/tuile.rbs', line 4285
def on_focus: () -> void
|
#rebuild_rows ⇒ void
This method returns an undefined value.
Re-renders every row from the current items, labels and selection.
163 164 165 166 167 |
# File 'lib/tuile/component/checkbox_group.rb', line 163 def rebuild_rows content.lines = @items.map do |item| StyledString.plain(value.include?(item) ? "[x] " : "[ ] ") + label_for(item) end end |
#rect= ⇒ void
This method returns an undefined value.
@param rect
4283 |
# File 'sig/tuile.rbs', line 4283
def rect=: (Rect rect) -> void
|
#toggle_at(index) ⇒ void
This method returns an undefined value.
Flips membership of the item on row index; an index outside #items is
ignored.
@param index
154 155 156 157 158 159 |
# File 'lib/tuile/component/checkbox_group.rb', line 154 def toggle_at(index) return unless index.between?(0, @items.size - 1) item = @items[index] self.value = value.include?(item) ? value - [item] : value + [item] end |
#value ⇒ Object
@return — the current value; nil until first set.
4264 |
# File 'sig/tuile.rbs', line 4264
def value: () -> Object
|
#value=(new_value) ⇒ void
This method returns an undefined value.
Replaces the selection, firing HasValue#on_value_change when it really
changed. Stores a frozen Set copy, so a set the caller goes on
mutating can't reach in.
@param new_value — nil selects nothing.
118 119 120 121 122 123 124 125 126 |
# File 'lib/tuile/component/checkbox_group.rb', line 118 def value=(new_value) selected = coerce(new_value) # HasValue#value= no-ops on an unchanged value; this guard is what also # skips the row rebuild. return if value == selected super(selected) rebuild_rows end |