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’sM3ESegment<T>(models/m3e_segment.dart). - Segmented
Button View - A declarative M3 Expressive segmented button. See the module docs.
- Segmented
Button Widget - 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 adebug_assert!here rather than a Dartassert). - MIN_
SEGMENTS - Fewest segments a group may carry — the reference’s own
assert(m3e_segmented_buttons.dart:26).
Functions§
- Segmented
Button - PascalCase alias for
segmented_button, matching this catalog’s view-fn vocabulary. - segment
- Create a segment reporting
valuewhen tapped/selected. AttachSegment::labeland/orSegment::icon— a segment needs at least one (debug_assert!-checked bysegmented_button; see the module docs’ Bounds section). - segmented_
button - Create a segmented button from
segments(2-5,debug_assert!-checked — seeMIN_SEGMENTS/MAX_SEGMENTS), reflectingselected(the currently-confirmed selection). Single-select by default; callSegmentedButtonView::multi_select(true)to allow more than one member selected at once.on_selection_changedfires with the requested next selection on release inside a member — see the module docs’ Selection semantics section.