Skip to main content

Module segmented_button

Module segmented_button 

Source
Expand description

The Material 3 Expressive segmented button: a row of 2-5 connected segments for selecting an option, switching a view, or sorting — single- or multi-select, with an optional checkmark on the selected member(s).

segmented_button builds a SegmentedButtonView; segment builds one Segment of it. It is a controlled component (see docs/CODE_STANDARDS.md’s Interaction Semantics): a tap fires on_selection_changed with the requested selection and leaves selected untouched — the app feeds the confirmed selection back on the next rebuild, exactly like crate::button_group/crate::switch.

§One Vec<T> shape for both selection modes

The reference carries a single Set<T> selected field and a single ValueChanged<Set<T>> onSelectionChanged callback regardless of multiSelect — _handleTap (m3e_segmented_buttons.dart:47-61) is what branches on the mode, not the widget’s own shape. This port mirrors that exactly rather than splitting into two view-fns with two different reported-value types (T for single-select, Vec<T> for multi-select): segmented_button always takes/reports Vec<T> (this framework’s ordered stand-in for Dart’s Set<T> — order is caller-controlled and never reshuffled by this widget), and SegmentedButtonView::multi_select only changes what [next_selection] computes internally on a tap. One controlled component, one reported-value type, matching the reference’s own single-widget shape.

§Selection semantics: single can’t deselect, multi toggles

[next_selection] transcribes _handleTap exactly:

  • Single-select (the default) always replaces the whole selection with the tapped value alone (next..clear()..add(value)) — including when the tapped segment is already the sole selected member. A single-select segmented button can never be tapped down to zero selected members; there is no toggle-off branch in the reference for this mode.
  • Multi-select (SegmentedButtonView::multi_select(true)) toggles the tapped value’s membership: absent → inserted, present → removed. A multi-select group can reach zero selected members.

§Checkmark: swaps in over the segment’s own icon, even a label-only one

SegmentedButtonView::show_selected_icon (default true, matching the reference) makes a selected segment’s leading icon slot resolve to crate::icons::CHECK instead of Segment::icon — _resolveLeading (m3e_segmented_buttons.dart:268-286) checks selected && showSelectedIcon first, unconditionally, before falling back to the segment’s own icon. One consequence transcribed faithfully rather than “fixed”: a label-only segment (no Segment::icon) gains a leading checkmark the instant it is selected, which changes that segment’s measured width — this port’s Widget::layout recomputes icon presence from the current selection every layout pass, so the row reflows exactly as the reference’s Row does. Porting decision: Segment::icon takes an frust::IconSource rather than the reference’s arbitrary Widget? icon — the swap target is itself a same-kind frust::IconSource (crate::icons::CHECK), so both slots stay the same closed shape, mirroring crate::switch’s selected_icon/unselected_icon pair rather than crate::toggle_button’s arbitrary-AnyView icon slot (a label is still a plain string, painted the same directly-shaped-and-rebrushed way crate::toggle_button’s own LabelRun is).

§Connected geometry: outer pill, square inner edges, hairline dividers

The reference wraps the whole row in one Container whose borderRadius is M3EShapes.resolve(height / 2) — a literal pill radius for the group’s own height, not a themed shape-scale rung — and clips the row to it via ClipRRect; individual members are plain, un-rounded Containers (m3e_segmented_buttons.dart:80-98). This port reproduces the same pixels without a clip primitive: [outer_radius] resolves frust::ShapeScale::full against the group’s own (width, height) — the theme-scale token whose frust::ShapeScale::resolve contract (min(width, height) / 2, clamped-not-literal) coincides with the reference’s height / 2 for every realistic (wider-than-tall) group, and degrades gracefully instead of overflowing for a pathologically narrow one the reference does not defend against — and [segment_radii] gives only the leftmost segment’s left corners / rightmost segment’s right corners that resolved radius, 0.0 (square) on every other corner, including every inner, adjacent-segment corner. This is a different inner value than crate::button_group’s own member_radii (a small nonzero frust::ShapeScale::extra_small rung) — that module clips nothing and rounds every member independently; this one has no clip to stand in for, so inner corners are literally square, exactly as the reference’s own un-rounded member Containers paint. Each member’s background/state-layer overlay is painted with its own [segment_radii] via frust::authoring::PaintScene::fill_rounded_rect_radii — the same per-corner-native paint call crate::toggle_button’s own connected-group corners use, rather than crate::button_group’s kurbo-path route; both reach the same pixels.

Dividers — solid color, not the reference’s sampled gradient. The reference’s M3ESegmentDivider samples an optional gradient across the whole row via a GlobalKey-hosted RenderBox (components/m3e_segment_divider.dart) so every hairline lines up with one continuous ramp instead of each stretching its own copy over a sub-pixel width. This framework has no GPU shader-gradient divider primitive and no cross-widget geometry query a GlobalKey read would need; this port paints every divider as a solid frust::authoring::PaintScene::stroke_line in the theme’s outline role (segmentedButtonTheme.divider’s own default, since dividerColor/dividerGradient are never set by any of this crate’s constructors) — the visual result for the only configuration this crate ships (solid dividers), with the gradient-sampling case left unported.

§Generic selection model — no T on the retained widget

Mirrors crate::radio’s own “Generic selection model” section exactly: segmented_button takes owned T: PartialEq + Clone + 'static values, but the retained SegmentedButtonWidget carries no generic parameter of its own. [bind_on_tap] closes over on_selection_changed, the current selected/multi_select, and one segment’s own value at build/rebuild time (where T is still in scope) into a plain frust::authoring::ErasedCallback — the type-erase- to-avoid-a-generic idiom docs/CODE_STANDARDS.md’s Language Idioms describes for a downstream-crate dependency, applied here to a generic instead. Closures aren’t comparable, so every rebuild reinstalls the whole Vec of them unconditionally, same as crate::radio/ crate::switch/crate::button_group’s own callback rebinding.

§State layer + press via the interaction core, no hover/focus

Each segment tracks its own crate::interaction::InteractionState (the newer, precedence-resolving substrate — see that module’s docs), but only ever calls crate::interaction::InteractionState::set_pressed: no row- of-items widget in this catalog wires hover or focus claiming today (crate::button_group, navbar, chips all leave both unwired too), so this module follows the same established scope rather than inventing a new per-segment hover-claim mechanism this task does not need. The reference’s pressedScale stays at its class default (1, i.e. no press-scale spring — M3ESegmentedButton never overrides M3ETappable.pressedScale), so unlike crate::button_group’s own shape-morph press emphasis, a pressed segment here paints a plain crate::interaction::InteractionState::resolve_opacity state-layer fill in its own foreground color — the same mechanism crate::icon_button/crate::toggle_button use, standing in for the reference’s M3EStateLayerOverlay (Flutter’s InkWell ripple this framework has no equivalent of — this crate’s whole-catalog convention, see mod@crate::icon_button’s suppress_ink doc).

Haptics fire crate::interaction::HapticSignal::None on every confirmed tap through crate::interaction::MaterialHaptics::fire — M3ETappable’s own default (m3e_tappable.dart:36’s haptic = M3EHapticFeedback.none, never overridden by M3ESegmentedButton), a documented no-op today, wired for a future revision to override, exactly crate::switch’s own Haptics section documents.

§Bounds: 2-5 segments, debug_assert-checked

segmented_button debug_assert!s MIN_SEGMENTS..=MAX_SEGMENTS. The reference’s own constructor asserts only the lower bound (segments.length >= 2, m3e_segmented_buttons.dart:26) — the upper bound of 5 is the class doc’s own stated scope (“Presents 2-5 connected M3ESegments”, m3e_segmented_buttons.dart:14), not a second runtime assert in the Dart source. This port pins both ends as one debug_assert!, matching crate::slider’s own precedent of debug_assert!-checking a documented-but-unenforced-upstream invariant rather than leaving it unchecked. Segment’s own “a segment needs a label or an icon” invariant (m3e_segment.dart:8-11) is likewise debug_assert!-checked, in segmented_button rather than at segment-construction time, since this crate’s builder pattern adds label/icon after segment returns.

Structs§

Segment
One option within a SegmentedButtonView — the reference’s M3ESegment<T> (models/m3e_segment.dart).
SegmentedButtonView
A declarative M3 Expressive segmented button. See the module docs.
SegmentedButtonWidget
The retained widget for a SegmentedButtonView — carries no generic parameter; see the module docs’ Generic selection model section.

Constants§

MAX_SEGMENTS
Most segments a group may carry — the reference class doc’s stated scope (m3e_segmented_buttons.dart:14; see the module docs’ Bounds section for why this is a debug_assert! here rather than a Dart assert).
MIN_SEGMENTS
Fewest segments a group may carry — the reference’s own assert (m3e_segmented_buttons.dart:26).

Functions§

SegmentedButton
PascalCase alias for segmented_button, matching this catalog’s view-fn vocabulary.
segment
Create a segment reporting value when tapped/selected. Attach Segment::label and/or Segment::icon — a segment needs at least one (debug_assert!-checked by segmented_button; see the module docs’ Bounds section).
segmented_button
Create a segmented button from segments (2-5, debug_assert!-checked — see MIN_SEGMENTS/MAX_SEGMENTS), reflecting selected (the currently-confirmed selection). Single-select by default; call SegmentedButtonView::multi_select(true) to allow more than one member selected at once. on_selection_changed fires with the requested next selection on release inside a member — see the module docs’ Selection semantics section.