SegmentedControl

SegmentedControl — mutually exclusive segments in a horizontal row.
Each segment is a real composed widget — a centered icon + label with
a reactive tint — built from a Segment descriptor. Selection is
bound to a Signal<Option<SegmentId>>: keyed, not positional, so
inserting or removing a segment never silently re-points the
selection at a different one. The chrome (rounded frame, hover tint,
selected-segment surface) is delegated to the active
SegmentedControlStyle.
const LIST: SegmentId = SegmentId::from_u64(1);
const GRID: SegmentId = SegmentId::from_u64(2);
let view = ctx.signal(Some(LIST));
SegmentedControl::new(view.clone())
.segment(Segment::new(tr!(list_view())).id(LIST).icon(|| IconWidget::list(14.0)))
.segment(Segment::new(tr!(grid_view())).id(GRID).icon(|| IconWidget::grid(14.0)))
// Pairing with a Switcher:
Switcher::new(segmented_control::index_signal(&view, &[LIST, GRID]))
When to use
- Use a
SegmentedControlfor mutually exclusive modes that read well as a compact horizontal strip (view mode, time period). - Prefer a
ComboBoxwhen the options are many and the strip form buys nothing — though a segmented control no longer breaks down at seven segments, because it overflows (below). - Prefer
RadioButton/RadioTileGroupwhen the options need vertical space or descriptions.
Width: overflow, not squeeze
When the segments do not fit, the ones that do not fit move into a
trailing chevron menu rather than all of them compressing into
ellipsised stubs (SegmentOverflow::Menu, the default; opt out with
SegmentOverflow::Compress).
Declaration order is stable, with exactly one exception: the selected segment is always visible. If it would have been pushed into the menu it takes the last slot, and it stays there until another segment is chosen from the menu — so the strip does not reshuffle under the pointer, and the promotion is forgotten once the control is wide enough to show everything again.
Declared: [A][B][C][D][E][F][G] fits 4 + chevron
start, A selected [A][B][C][D][v] menu: E F G
pick F from menu [A][B][C][F][v] menu: D E G
click A (F stays) [A][B][C][F][v] menu: D E G
widen to full fit [A][B][C][D][E][F][G]
Accessibility
Role::RadioGroup on the control with active_descendant pointing at
the selected segment; Role::RadioButton per segment, carrying
"N of M" over the whole segment list — including segments currently in
the overflow menu, which are still reachable. Arrow keys cycle
selection (RTL-aware, resolved at event time) and Home/End jump to the
ends, both skipping disabled segments; stepping onto an overflowed
segment promotes it into view. Increment/Decrement AT actions
mirror the arrows.
The strip is one tab stop. While the control is overflowing the chevron adds a second, because an overflow menu that no keyboard can reach is not an overflow menu; it cannot join the arrow sequence, since here arrows move selection rather than a roving focus.
Builder methods at a glance
indexed, segment, segments, segment_ids, enabled, label, on_change, style, text_style, display, sizing, overflow, is_overflowing, fill_width
API reference
📖 Full rustdoc API for this module
pub enum SegmentDisplay
What a segment paints: its icon, its label, or both.
Set on the control with
SegmentedControl::display; it
applies to every segment. Mirrors TabWidget's TabDisplayMode.
Icon-only is the classic compact fallback before overflow kicks in:
a bar of icon-only segments fits far more of them, so switching to
Icon can be the difference between a
complete strip and a chevron menu.
#![allow(unused)] fn main() { pub enum SegmentDisplay { /* variants */ } }
Variants
Auto— Paint whatever the segment declares — icon and label when both are present, label alone otherwise. The default, and the behaviour of everySegmentedControlbefore this mode existed.Text— Label only. A declared icon is suppressed.Icon— Icon only; the label is promoted to the hover tooltip (unless the segment already declares one). A segment with no icon falls back to its label, so the mode is never a silent no-op.IconText— Icon and label. Identical toAutofor a segment that declares both; kept for parity withTabDisplayModeso a caller can be explicit.
pub enum SegmentSizing
How the visible segments divide the control's width.
#![allow(unused)] fn main() { pub enum SegmentSizing { /* variants */ } }
Variants
Uniform— Every visible segment gets the same width — the Apple / IntUI look, and the behaviour of everySegmentedControlbefore this knob existed. The fit calculation uses the widest segment's natural width as the unit, so segments never look ragged.Fit— Every visible segment gets its own natural width, and leftover space (when the control fills a wider slot) is shared equally. Fits more short segments before overflowing, at the cost of an uneven strip.
pub enum SegmentOverflow
What the control does when its segments do not fit.
#![allow(unused)] fn main() { pub enum SegmentOverflow { /* variants */ } }
Variants
Menu— Move the segments that do not fit into a trailing chevron menu, keeping the rest at a legible width. The selected segment is always among the visible ones. This is the default.Compress— Keep every segment on the strip and let them compress, truncating labels with an ellipsis. The behaviour of everySegmentedControlbefore overflow existed — appropriate for two or three short segments that will never realistically overflow.
pub struct Segment
One segment descriptor: a localized label with a stable
SegmentId, an optional leading icon, a hover tooltip, and
reactive disabled / visible flags.
#![allow(unused)] fn main() { pub struct Segment { /* fields */ } }
Methods
pub fn new(label: impl Into<LocalizedString>) -> Self
A text segment with a freshly allocated SegmentId. The label
may come from tr!(...) (translated — follows a live locale
switch) or lit!(...) (untranslated).
Call id when the segment needs a stable identity —
one that survives a restart, or that another crate can name.
pub fn id(mut self, id: SegmentId) -> Self
Give this segment an app-chosen stable identity, replacing the
fresh id new allocated. Use this whenever the
selection is persisted or the segment is contributed by another
crate.
pub fn segment_id(&self) -> SegmentId
This segment's identity.
pub fn icon(mut self, factory: impl Fn() -> IconWidget + 'static) -> Self
Add a leading icon. The factory is invoked at build time (and on rebuild); the icon's tint is bound reactively to the segment's selected / focus / enabled state so it matches the label.
pub fn tooltip(mut self, text: impl Into<LocalizedString>) -> Self
Hover tooltip — most useful for icon-only segments.
Mutually exclusive with rich_tooltip /
rich_tooltip_content /
composite_tooltip — last call wins.
pub fn rich_tooltip(mut self, key: impl Into<String>) -> Self
Rich hover tooltip resolved from the app-wide registry by key.
Mutually exclusive with tooltip /
rich_tooltip_content /
composite_tooltip — last call wins.
pub fn rich_tooltip_content(mut self, content: crate::tooltip::TooltipContent) -> Self
Rich hover tooltip driven by an inline
TooltipContent entry
(no registry key needed).
Mutually exclusive with tooltip /
rich_tooltip /
composite_tooltip — last call wins.
pub fn composite_tooltip(mut self, factory: impl Fn() -> Box<dyn Widget> + 'static) -> Self
Composite hover tooltip built by a factory closure at attach time.
The factory is called once per build() to produce the tooltip
body widget. It is stored as an Rc<dyn Fn> so that Segment
remains Clone.
Mutually exclusive with tooltip /
rich_tooltip /
rich_tooltip_content — last call wins.
pub fn disabled(mut self, disabled: impl Into<Prop<bool>>) -> Self
Disable this segment: not selectable via click or keyboard, dimmed, and announced disabled to assistive tech.
Accepts a bool or a Signal<bool> — a bound signal flips the
segment live, with no rebuild, and keyboard stepping honours
the new value immediately (the flags are read at event time, not
snapshotted at build time).
pub fn visible(mut self, visible: impl Into<Prop<bool>>) -> Self
Hide this segment entirely: it leaves the strip, the overflow menu, the keyboard order, and the accessibility tree, and it is excluded from the overflow calculation.
Distinct from overflowed — an overflowed segment is still
reachable from the chevron menu, a hidden one is not there at all.
Accepts a bool or a Signal<bool>; a bound signal re-runs the
overflow plan with no rebuild.
pub fn index_signal(...)
Derive a Switcher-compatible index from a keyed selection.
SegmentedControl is keyed precisely so that a contributed segment
cannot silently re-point the selection, but Switcher is index-driven
— this is the adapter between the two. Unknown or absent ids resolve
to 0, matching Switcher's own out-of-range behaviour.
Switcher::new(segmented_control::index_signal(&view, &[LIST, GRID, COLUMNS]))
.child(list_pane)
.child(grid_pane)
.child(columns_pane)
#![allow(unused)] fn main() { pub fn index_signal(selected: &Signal<Option<SegmentId>>, ids: &[SegmentId]) -> Signal<usize>; }
pub struct SegmentedControl
A segmented control binding a Signal<Option<SegmentId>> to a row of
mutually exclusive segments. Build the segment list with
segment or segments.
#![allow(unused)] fn main() { pub struct SegmentedControl { /* fields */ } }
Methods
pub fn new(selected: Signal<Option<SegmentId>>) -> Self
Create an empty segmented control bound to selected. Add segments
with segment or segments.
pub fn indexed(index: Signal<usize>) -> Self
Bind a positional Signal<usize> instead of a keyed
selection, mirrored in both directions.
Use this only when position is the meaning and the segment list
is closed and local — an enum discriminant over a fixed ALL
array, a Switcher index, a settings choice. For anything else
prefer new: an index silently stops meaning the
same thing the moment a segment is inserted ahead of it, which is
the entire reason selection is keyed. A persisted selection, or
segments contributed by another crate, are both firmly in
"anything else".
Positions address the declared list, so a segment hidden with
Segment::visible does not renumber the others.
// `bucket_idx` already drives the rollup maths and a Switcher.
SegmentedControl::indexed(bucket_idx.clone())
.segments([lit!("×2"), lit!("×4"), lit!("×8")])
pub fn segment(mut self, segment: impl Into<Segment>) -> Self
Append one segment. Accepts a Segment or, via
From<LocalizedString>, a bare tr!(...) / lit!(...) label
(which gets a freshly allocated SegmentId).
pub fn segments(mut self, segments: impl IntoIterator<Item = impl Into<Segment>>) -> Self
Append several segments. Label-only:
.segments([tr!(day()), tr!(week())]); rich:
.segments([Segment::new(...).id(DAY).icon(...), ...]).
pub fn segment_ids(&self) -> Vec<SegmentId>
The ids of the segments added so far, in declaration order.
Convenient for feeding index_signal without repeating the list.
pub fn enabled(mut self, enabled: impl Into<Prop<bool>>) -> Self
Set the enabled state, statically or reactively. Forwarded to
the arena at build time via
ctx.enabled_when(segmented_control_id, self.enabled.clone()).
pub fn label(mut self, label: impl Into<LocalizedString>) -> Self
Accessible name for the group — e.g. "View mode". Screen readers
announce it before the selected segment. Matches
RadioGroup::label and
RadioTileGroup::label.
pub fn on_change(mut self, f: impl Fn(SegmentId, &mut EventContext) + 'static) -> Self
Called whenever the user changes the selection — by click, arrow
key, assistive technology, or the overflow menu. Receives the
newly selected SegmentId and an EventContext, so it can do
things a bare Signal write cannot (ctx.set_locale(...),
ctx.send_intent(...), opening a window).
Does not fire for programmatic writes to the bound signal — there is no event in flight to carry. Observe the signal for that.
pub fn style(mut self, style: impl teksilo_core::styles::SegmentedControlStyle) -> Self
Per-call override for the segmented-control chrome.
pub fn text_style(mut self, style: impl Into<teksilo_core::color_prop::TextStyleProp>) -> Self
Override every segment's label text style (font, size, weight).
Accepts a TextStyleRole, a TextStyle, or a Signal of either.
Default (unset) is TextStyleRole::Small. Text color stays
state-driven and is intentionally not overridable here.
pub fn display(mut self, display: SegmentDisplay) -> Self
What each segment paints: its icon, its label, or both. See
SegmentDisplay. Icon-only fits far more segments, so it is
worth reaching for before the control starts overflowing.
pub fn sizing(mut self, sizing: SegmentSizing) -> Self
How the visible segments divide the width. See SegmentSizing.
pub fn overflow(mut self, mode: SegmentOverflow) -> Self
What to do when the segments do not fit. See SegmentOverflow.
pub fn is_overflowing(&self) -> Signal<bool>
Reactive "some segments are in the overflow menu right now".
Republished from place_children behind an equality guard, so it
is safe for RepaintOnly / AccessibilityOnly consumers and for
Relayout consumers that do not feed back into this control's own
width. Mirrors Toolbar::is_overflowing.
pub fn fill_width(mut self, fill: bool) -> Self
Whether the control claims all the width offered to it (the default, and the behaviour before this knob existed) or hugs its segments.
false also makes the control shrinkable: in an over-constrained
stack it compresses — and overflows — instead of spilling past its
bounds.
pub struct SegmentId
Stable identity of a segment. Cheap to copy; survives rebuilds, locale changes, and segments being inserted around it.
#![allow(unused)] fn main() { pub struct SegmentId(NonZeroU64); }
Methods
pub fn fresh() -> Self
Allocate a new, never-before-seen id. Backed by a monotonic global counter — overflow is theoretically possible after 2^64 calls, at which point the universe has had bigger problems.
Segment::new calls this for you, so a
control that never persists its selection needs no explicit ids.
Allocations start at 2^48, so they can never collide with a small
constant an app declared through from_u64.
pub const fn from_raw(value: NonZeroU64) -> Self
Wrap an externally-allocated key. Use this when the segment's
identity comes from an app-side store (a view-mode enum
discriminant, a plugin key hash, …) — calling SegmentId::fresh
would allocate a new id every restart, breaking a persisted
selection.
pub const fn from_u64(value: u64) -> Self
const convenience over from_raw, so an app
can declare its segments as constants:
# use teksilo_widgets::SegmentId;
const SYNOPSIS: SegmentId = SegmentId::from_u64(1);
const CHAPTER: SegmentId = SegmentId::from_u64(2);
Panics
If value is zero. Because this is a const fn, a literal zero
is caught at compile time rather than at run time.
pub const fn raw(self) -> NonZeroU64
The underlying non-zero u64. Serialize this to persist a
selection across sessions; restore via from_raw
or from_u64.
pub const fn get(self) -> u64
The underlying value as a plain u64.