Class: Tuile::Component::TabSheet
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::TabSheet
- Defined in:
- lib/tuile/component/tab_sheet.rb,
sig/tuile.rbs
Overview
A Tabs strip on its top row plus the pane belonging to the selected tab underneath it:
␣Details␣│␣Payment␣│␣Shipping␣
the selected tab's pane fills the rest of the rect
sheet = Component::TabSheet.new
sheet.add_tab("Details", details_form) # selected, and shown
sheet.add_tab("Payment", payment_form)
sheet.select_next # shows payment_form
sheet.on_tab_selected { |e| log("now on #{e.tab&.caption}") }
Tab lands on the strip first and enters the pane on the next press, which is the browser's order; switching tabs does not move focus into the new pane, but if focus was inside the pane that just went away it lands back on the strip.
Hidden panes are detached
Only the selected tab's pane is in the component tree — the others are detached, which is how Tuile hides a component (there is no visibility flag, and an empty rect gates painting only). Consequences worth designing around:
- A hidden pane is invisible to everything: the Tab cycle, focus
cascades, repaint, the cursor,
walk_treewalks. No gates anywhere. - Its state survives, because state is ivars — scroll position, caret,
list cursor, text are all exactly as the user left them, and mutating a
hidden pane is safe (
invalidatewhile detached is a silent no-op). - #handle_detached / #handle_attached fire on every switch, so a pane holding a mounted-lifetime resource — a ProgressBar's ticker — releases it while hidden and re-acquires it on return. A pane that must keep something alive while hidden can't; that something belongs in the model the pane renders, not in the pane.
Implementation details
children is [strip, pane], the strip pinned at index 0, so pre-order
traversal gives the strip-then-pane Tab order for free. The swap follows
the slot-swap recipe #detach_child documents — detach, rewire,
handle_child_removed last, so the focus repair sees the new occupant.
Panes live in an identity-keyed Tab => Component map here rather than in
a slot on Tuile::Component::Tabs::Tab: the strip's tab array stays the sole ordering
authority, and the strip itself stays ignorant of panes. One idempotent
sync_pane is the sole writer of the visible pane, deriving it from
strip.selected on every call, so registering a pane and selecting a tab
can happen in either order.
The sheet registers on the strip's on_tab_selected first (that is what
drives the swap); an app's listener goes on #on_tab_selected here, which
fires after the pane has been swapped in.
Instance Attribute Summary collapse
-
#pane ⇒ Component?
readonly
@return — the pane currently in the tree — the selected tab's,
nilwhile the sheet has no tabs. -
#strip ⇒ Tabs
readonly
@return — the strip.
Instance Method Summary collapse
-
#add_tab(caption, pane) ⇒ Tabs::Tab
Adds a tab and the pane to show while it is selected.
-
#forget_removed_tabs ⇒ void
Drops entries whose tab is gone.
-
#handle_child_removed(child) ⇒ void
Lands focus on the strip rather than on
selfwhen the focused pane is swapped out — a bare container can't use keys, and the user's last action was a tab switch. -
#handle_focus ⇒ void
Sends focus to the strip: a sheet is a container, and the strip is where a tab switch is driven from.
-
#initialize(separator: Tabs::DEFAULT_SEPARATOR) ⇒ TabSheet
constructor
@param
separator— the strip's separator; see Tuile::Component::Tabs#separator=. -
#on_tab_selected ⇒ Listeners
An app's own selection listener, fired with a Tuile::Component::Tabs::TabSelectedEvent carrying this sheet as its source, after the pane has been swapped in.
-
#pane_for(tab) ⇒ Component?
@param
tab. -
#relayout ⇒ void
The strip takes the top row, the selected pane everything under it.
-
#remove_tab(tab) ⇒ Component?
Removes a tab and forgets its pane, detaching it if it was the visible one.
-
#select_next ⇒ Boolean
Selects the next tab, clamping at the last one.
-
#select_previous ⇒ Boolean
Selects the previous tab, clamping at the first one.
-
#selected ⇒ Tabs::Tab?
@return — the selected tab.
-
#selected=(tab) ⇒ void
@param
tab— one of this sheet's tabs. -
#selected_index ⇒ Integer?
@return — the selected tab's position.
-
#selected_index=(index) ⇒ void
@param
index— a position in0...tabs.size. -
#sync_pane ⇒ void
Makes the visible pane match
strip.selected, swapping if it doesn't. -
#tabs ⇒ ::Array[Tabs::Tab]
@return — the strip's tabs, in order.
Constructor Details
#initialize(separator: Tabs::DEFAULT_SEPARATOR) ⇒ TabSheet
@param separator — the strip's separator; see Tuile::Component::Tabs#separator=.
66 67 68 69 70 71 72 73 74 75 |
# File 'lib/tuile/component/tab_sheet.rb', line 66 def initialize(separator: Tabs::DEFAULT_SEPARATOR) super() @panes = {}.compare_by_identity @strip = Tabs.new(separator:) add_child(@strip) @strip.on_tab_selected do |e| sync_pane on_tab_selected.fire(Tabs::TabSelectedEvent.new(source: self, index: e.index, tab: e.tab)) end end |
Instance Attribute Details
#pane ⇒ Component? (readonly)
@return — the pane currently in the tree — the selected
tab's, nil while the sheet has no tabs.
86 87 88 |
# File 'lib/tuile/component/tab_sheet.rb', line 86 def pane @pane end |
#strip ⇒ Tabs (readonly)
@return — the strip. Reach through it for the rest of its API —
sheet.strip.separator = "|". Its on_tab_selected is safe to
register on (the sheet's own listener is wired first, so the pane is
already swapped), but #on_tab_selected is the one that names this
sheet as the event's source.
82 83 84 |
# File 'lib/tuile/component/tab_sheet.rb', line 82 def strip @strip end |
Instance Method Details
#add_tab(caption, pane) ⇒ Tabs::Tab
Adds a tab and the pane to show while it is selected. The first tab added becomes the selection, so its pane is shown immediately.
@param caption — parsed as Tuile::Component::Tabs::Tab#caption= parses it.
@param pane — shown while this tab is selected, detached while it isn't.
@return — the new tab's handle.
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/tuile/component/tab_sheet.rb', line 98 def add_tab(caption, pane) raise TypeError, "expected Component, got #{pane.inspect}" unless pane.is_a?(Component) forget_removed_tabs if @panes.each_value.any? { |existing| existing.equal?(pane) } raise ArgumentError, "#{pane} is already a pane of this TabSheet" end tab = @strip.add_tab(caption) @panes[tab] = pane sync_pane tab end |
#forget_removed_tabs ⇒ void
This method returns an undefined value.
Drops entries whose tab is gone. Tuile::Component::Tabs::Tab#remove takes a tab off the strip without passing through #remove_tab, and a detached tab can never be selected again, so its entry is dead weight — it pins the pane against garbage collection and makes #add_tab reject that pane as still in use. Idempotent and the only cleaner, because the rule it enforces is an invariant ("every key is a live tab of my strip") rather than a step in one code path.
212 213 214 |
# File 'lib/tuile/component/tab_sheet.rb', line 212 def forget_removed_tabs @panes.delete_if { |tab, _pane| !tab.attached? } end |
#handle_child_removed(child) ⇒ void
This method returns an undefined value.
Lands focus on the strip rather than on self when the focused pane is
swapped out — a bare container can't use keys, and the user's last
action was a tab switch.
@param child
176 177 178 179 |
# File 'lib/tuile/component/tab_sheet.rb', line 176 def handle_child_removed(child) super screen.focused = @strip if attached? && screen.focused.equal?(self) end |
#handle_focus ⇒ void
This method returns an undefined value.
Sends focus to the strip: a sheet is a container, and the strip is where a tab switch is driven from. The pane is a Tab press away.
166 167 168 169 |
# File 'lib/tuile/component/tab_sheet.rb', line 166 def handle_focus super screen.focused = @strip end |
#on_tab_selected ⇒ Listeners
An app's own selection listener, fired with a Tuile::Component::Tabs::TabSelectedEvent carrying this sheet as its source, after the pane has been swapped in. Same contract as Tuile::Component::Tabs#on_tab_selected: it reports that the selection changed, whatever changed it.
62 |
# File 'lib/tuile/component/tab_sheet.rb', line 62 listener :on_tab_selected |
#pane_for(tab) ⇒ Component?
@param tab
@return — the pane registered for tab; nil for a
removed tab, a tab of another sheet, or nil.
128 129 130 131 132 |
# File 'lib/tuile/component/tab_sheet.rb', line 128 def pane_for(tab) return nil unless tab&.attached? @panes[tab] end |
#relayout ⇒ void
This method returns an undefined value.
The strip takes the top row, the selected pane everything under it.
218 219 220 221 |
# File 'lib/tuile/component/tab_sheet.rb', line 218 def relayout @strip.rect = Rect.new(0, 0, rect.width, [rect.height, 1].min) @pane&.rect = Rect.new(0, 1, rect.width, [rect.height - 1, 0].max) end |
#remove_tab(tab) ⇒ Component?
Removes a tab and forgets its pane, detaching it if it was the visible one. The strip re-selects as Tuile::Component::Tabs#remove_tab describes, and this sheet shows whatever it lands on.
@param tab — one of this sheet's tabs.
@return — the pane that tab owned.
118 119 120 121 122 123 |
# File 'lib/tuile/component/tab_sheet.rb', line 118 def remove_tab(tab) pane = @panes[tab] # read first: the strip's own removal may prune the entry @strip.remove_tab(tab) @panes.delete(tab) pane end |
#select_next ⇒ Boolean
Selects the next tab, clamping at the last one.
@return — false only when there are no tabs.
157 |
# File 'lib/tuile/component/tab_sheet.rb', line 157 def select_next = @strip.select_next |
#select_previous ⇒ Boolean
Selects the previous tab, clamping at the first one.
@return — false only when there are no tabs.
161 |
# File 'lib/tuile/component/tab_sheet.rb', line 161 def select_previous = @strip.select_previous |
#selected ⇒ Tabs::Tab?
@return — the selected tab.
138 |
# File 'lib/tuile/component/tab_sheet.rb', line 138 def selected = @strip.selected |
#selected=(tab) ⇒ void
This method returns an undefined value.
@param tab — one of this sheet's tabs.
142 143 144 |
# File 'lib/tuile/component/tab_sheet.rb', line 142 def selected=(tab) @strip.selected = tab end |
#selected_index ⇒ Integer?
@return — the selected tab's position.
147 |
# File 'lib/tuile/component/tab_sheet.rb', line 147 def selected_index = @strip.selected_index |
#selected_index=(index) ⇒ void
This method returns an undefined value.
@param index — a position in 0...tabs.size.
151 152 153 |
# File 'lib/tuile/component/tab_sheet.rb', line 151 def selected_index=(index) @strip.selected_index = index end |
#sync_pane ⇒ void
This method returns an undefined value.
Makes the visible pane match strip.selected, swapping if it doesn't.
Idempotent and the sole writer of @pane: it derives everything from
current state, so #add_tab can register a pane after the strip has
already selected its tab.
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/tuile/component/tab_sheet.rb', line 188 def sync_pane forget_removed_tabs wanted = @panes[@strip.selected] return if wanted.equal?(@pane) old = @pane detach_child(old) unless old.nil? @pane = wanted unless wanted.nil? add_child(wanted) # appended: the strip stays at index 0 wanted.invalidate end invalidate handle_child_removed(old) unless old.nil? end |
#tabs ⇒ ::Array[Tabs::Tab]
@return — the strip's tabs, in order.
135 |
# File 'lib/tuile/component/tab_sheet.rb', line 135 def tabs = @strip.tabs |