Skip to main content

Module split_button

Module split_button 

Source
Expand description

The Material 3 Expressive split button: a leading action segment joined to a trailing menu-trigger segment across a hairline gap, reading as one connected shape.

split_button keeps its original four-argument shape — label, the controlled open flag, the leading on_press action, and the trailing on_open menu callback — and every reference axis is a chained builder on top: SplitButtonView::size, SplitButtonView::variant, SplitButtonView::shape, SplitButtonView::items, SplitButtonView::menu_style.

§Two segments, two focus targets

The reference builds the halves as two independent Focus nodes inside one FocusTraversalGroup, each with its own InkWell, its own canRequestFocus/skipTraversal gate and its own key handler (m3e_split_button_segments.dart:100 and :335). This port matches that structurally: each half is a real child widget in its own pod, so it is its own hit target, its own focus target (a Key event routes down the focus path to exactly one of them), its own semantics node, and separately disable-able — SplitButtonView::enabled gates the whole control (the reference’s IgnorePointer(ignoring: !enabled)) while SplitButtonView::trailing_enabled disables the menu trigger alone, making real the leadingEnabled/trailingEnabled split the reference already carries as two distinct variables fed from one prop (m3e_split_button_content.dart:212-:213).

Honest limitation, the same one crate::button records: frust has no keyboard focus traversal — nothing hands a button focus, and neither segment claims focus on a press (a tap-shows-a-focus-ring button is the wrong behavior on touch). The focus ring, the focus-widened gap and the Enter/Space activation below are ported, gated on the authoritative focus reads and unit-tested by seeding a pod’s recorded focus path directly; they become live the moment focus routing can reach a button.

§Sizes

Segment height / trailing width / leading paddings / icon, from M3ESplitButtonTheme’s tables (m3e_split_button_theme.dart:76-:210), with the label type role from _LeadingContent (m3e_split_button_widgets.dart:31):

SizeHeightTrailing widthLeft padLabel right padIconLabel role
SplitButtonSize::Xs3248121020label_small
SplitButtonSize::Sm (default)4048161224label_medium
SplitButtonSize::Md5656242424label_large
SplitButtonSize::Lg9696484832title_medium
SplitButtonSize::Xl136136646440title_large

The trailing width column is the reference’s own three-way resolution (unselected = left inner pad + icon + right outer pad; selected = side pad × 2 + icon; circle = the segment height), floored at SPLIT_MIN_TAP_TARGET — all three agree for every size, which the_three_trailing_width_formulas_agree pins rather than assumes.

§Variants

Container / content roles are the plain button’s (m3e_button_theme.dart:111/:127, reached through _resolveColorsAndShapes): SplitButtonVariant::Filled primary / on_primary, SplitButtonVariant::Tonal secondary_container / on_secondary_container, SplitButtonVariant::Elevated surface_container_low / primary (plus a shadow), SplitButtonVariant::Outlined transparent + outline hairline / primary. There is no Text variant: the reference asserts it away (m3e_split_buttons.dart:76).

§Connected geometry and the spring clip

Each segment resolves four independent corner radii and springs to them, then clips its own content to the animating shape — the reference’s M3ERadiusAndPaddingMotion feeding a Material(clipBehavior: antiAlias, borderRadius: animatedRadius) (m3e_split_button_segments.dart:235, :330). The outer (world-facing) corners take the shape family’s radius; the two inner (adjacent) corners resolve per state:

StateLeading innerTrailing innerTrailing outer
restinginnerinnerouter
hoveredhoveredhoveredouter
pressedpressedpressedouter
menu open—height / 2height / 2

_leadingRadii/_trailingRadii (m3e_split_button_style.dart:166/:182) in precedence order — the open (selected) state outranks pressed, which outranks hovered. The per-size inner column (4/4/4/8/12) lands exactly on this crate’s extra_small/small/medium shape tokens and therefore resolves from the live frust::Theme; the hovered (8/12/12/20/20), pressed (2/2/2/4/6) and square-outer (8/10/14/24/34) columns have no whole-table token to resolve from and stay literal — the same partial-coincidence rule crate::toggle_button applies.

The spring is SPLIT_SHAPE_SPRING — stiffness 380, damping ratio 0.55, the expressiveSpatialPress preset every button in this family defaults to (m3e_base_button_state.dart:183), i.e. crate::MaterialSpring::EXPRESSIVE_SPATIAL_PRESS. It is a named constant rather than a theme read because a press starts in the event pass, which never threads a theme; the_shape_spring_matches_expressive_spatial_press is the tripwire. [ShapeMotion] carries the retarget contract crate::button’s RadiusPaddingMotion documents — mount-frame snap, a 0.1px dead band, mid-flight continuity from the value being painted right now, and a 1.5× overshoot cap — re-pinned here directly, since that type is private to a sibling module.

§The chevron: a tween, not a spring

Only the clip springs. The chevron itself is an AnimatedRotation (m3e_split_button_segments.dart:276): CHEVRON_OPEN_TURNS (half a turn) over CHEVRON_ROTATION_DURATION (120ms) on Curve::EaseOut, transcribed as a duration-driven frust::AnimationController rather than a fling. It also carries a per-size optical nudge (SplitButtonTrailingAlignment::OpticalCenter, the default) that drops to zero while the menu is open, exactly as _computeTrailingGeometry does.

SplitButtonMenuStyle is the ported half of the reference’s three-way M3ESplitButtonMenuStyle, and SplitButtonView::menu_route is the selector: it hands back the presentation this button’s style calls for, built from the same SplitButtonView::items list.

Both routes render the item list through the one item-list surface this catalog owns (crate::menu::menu_panel) rather than a second row renderer — a porting decision, since the reference’s sheet rows (m3e_split_button_bottom_sheet.dart:105) are a near-duplicate of its own menu rows.

§Not ported

M3ESplitButtonDecoration’s gradient layers (the plain button’s crate::ButtonDecoration seam is where that belongs), M3EButtonSize.custom, the multi-select bottom sheet and its checkbox style, menuBuilder/ m3eMenuBuilder (an app composes crate::menu() itself), tooltips and onLongPress. Each is either a seam this catalog already owns elsewhere or a Flutter-plumbing concept with no frust analogue — the same mapping table crate::button records.

§Attribution

See plugins/material/NOTICE’s “MIT License — Additional Copyright Holders (Vendored Components)” section (Mudit Purohit / m3e_buttons) and its Module Attribution Header Convention.

Structs§

SplitButtonItem
One selectable entry in a split button’s menu — the reference’s M3ESplitButtonItem (models/m3e_split_button_item.dart).
SplitButtonView
A declarative split button. See the module docs.
SplitButtonWidget
The retained widget for a SplitButtonView: the two segment pods plus the gap between them. It paints nothing of its own — each segment owns its container, clip, state layer and content.

Enums§

SplitButtonMenuRoute
The presentation a split button’s SplitButtonMenuStyle calls for, as SplitButtonView::menu_route resolves it. See the module docs’ Menu styles section for how an app mounts each.
SplitButtonMenuStyle
Which presentation the trailing trigger’s menu takes — the ported half of the reference’s M3ESplitButtonMenuStyle. See the module docs’ Menu styles section (the third, native, is descoped).
SplitButtonShape
The split button’s corner-radius family — the reference’s M3EButtonShape (m3e_button_enums.dart:45) as _computeRadii consumes it.
SplitButtonSize
The split button’s size tier — the reference’s M3EButtonSize (m3e_button_enums.dart:64) as M3ESplitButtonTheme measures it. See the module docs’ Sizes table.
SplitButtonTrailingAlignment
How the trailing chevron is centred — the reference’s M3ESplitButtonTrailingAlignment.
SplitButtonVariant
The split button’s container treatment — the reference’s M3EButtonStyle minus text, which M3ESplitButton asserts away (m3e_split_buttons.dart:76). See the module docs’ Variants section.

Constants§

CHEVRON_OPEN_TURNS
How far the chevron rotates when the menu opens, in turns — chevronOpenTurns (:16).
CHEVRON_ROTATION_DURATION
The chevron rotation’s duration — AnimatedRotation(duration: 120ms) (m3e_split_button_segments.dart:277). A tween, not a spring; see the module docs.
SPLIT_ELEVATED_INNER_GAP
The gap an SplitButtonVariant::Elevated split button takes instead, in logical px — elevatedInnerGap (:15), widened so two shadows do not merge.
SPLIT_FOCUS_RING_OUTSET
How far a focus ring extends past its segment, in logical px — the kFocusRingGap + kFocusRingWidth sum _computeSegmentGeometry adds to the inner gap while either half is focused (m3e_split_button_content.dart:296).
SPLIT_INNER_GAP
The gap between the two segments, in logical px — innerGap (:14).
SPLIT_MIN_TAP_TARGET
The minimum tap target the control as a whole reserves, in logical px — M3ESplitButtonTheme.minTapTarget (:13). A segment shorter than this (the xs/sm rows) is centred inside it rather than stretched.
SPLIT_SHAPE_SPRING
The segment shape (clip) spring: stiffness 380, damping ratio 0.55, mass 1 — expressiveSpatialPress, i.e. crate::MaterialSpring::EXPRESSIVE_SPATIAL_PRESS. See the module docs for why it is a constant rather than a theme read.
TRAILING_SELECTED_CORNER_PERCENT
The open trailing segment’s corner radius as a percentage of its height — trailingInnerSelectedCornerPercent (:17), i.e. a pill.

Functions§

SplitButton
PascalCase alias for split_button, matching the catalog’s view-fn vocabulary.
split_button
Create a split button labelled label, with a leading on_press action and a trailing on_open menu callback.
split_button_item
Create a menu item labelled label.