Expand description
The unified M3(E) interaction substrate: hover/focus/pressed/dragged
tracking, an M3E-precedence state-layer opacity resolver, an optional
press-scale spring spec, and a pluggable haptics hook. Modeled on the
reference’s M3ETappable/M3EStateLayer/M3EHaptics foundations
(tmp/material_3_expressive/lib/foundations/m3e_tappable.dart,
m3e_state_layer.dart, m3e_haptics.dart), unified here into one
substrate every interactive component in this crate builds on, rather
than each hand-rolling its own copy of the same four booleans.
§A deliberate porting decision
The reference’s own M3EButton notably does not build on
M3ETappable — it hand-rolls its own state mixin instead (a verified
research correction, not an omission in this port). This crate unifies
anyway: every interactive component here — present and future — is
meant to track its interaction state through InteractionState
rather than re-deriving the same four flags per widget, trading the
reference’s per-widget flexibility for one shared substrate.
§Opacity model: precedence, not max-of-active
InteractionState::resolve_opacity is the new documented default,
porting M3EInteractionState.opacity’s priority resolution
exactly: dragged > pressed > focused > hovered — the single
highest-priority active state contributes the overlay, never a stack of
several. crate::state_layer::StateLayer (this crate’s older helper,
kept fully back-compatible for its existing consumers —
list_item/card/chips/fab/switch) instead resolves by
InteractionState::max_active_opacity, the maximum of every
active state’s opacity.
Given this crate’s own token table — hover 8% < focus 10% = pressed 10% < dragged 16%, i.e. every state later in the precedence order
carries opacity >= every state earlier in it — the two algorithms
always agree numerically today: the highest-priority active state is
always the maximum active one. They stay two distinct functions
rather than collapsing to one because that agreement is a property of
today’s token values, not a guarantee the resolver shapes make on their
own — a future token retune that broke the monotonic ordering would
make them diverge, and StateLayer’s own tests pin its (max-of-active)
behavior specifically, independent of this module.
§Haptics: a pluggable, no-op-by-default hook
MaterialHaptics is a process-global, set-once hook. This crate
reaches no frust-haptics dependency itself — the design-system
charter forbids a plugin-tier crate depending on a sibling plugin (see
docs/PLUGINS_ARCHITECTURE.md’s Layer Dependencies). Firing a
HapticSignal before an app installs a hook is a silent no-op.
MaterialHaptics::set’s doc has the exact install-once timing
contract and a worked frust-haptics wiring example.
§Press-scale: shared spec, no animation runtime
PressScaleSpec carries only the shape of an optional press-scale
spring — a target scale plus a named PressSpringId — mirroring the
reference’s M3ETappable.pressedScale + .spring pair. This module
ships no animation-controller driving loop of its own: actually
springing a widget’s paint scale toward target_scale on press (and
back on release) stays each widget’s own job, the established pattern
button_group’s own PRESS_SPRING constant plus press_anim field
set — this task ships the shared state and resolved-values plumbing
(PressScaleSpec::is_active, PressSpringId::resolve), not a
runtime.
Structs§
- Interaction
State - Tracks a widget’s hover/focus/pressed/dragged interaction state. See the module docs for the two opacity resolvers this feeds.
- Material
Haptics - The design system’s pluggable, process-global haptics seam. Carries no
state of its own — every method is a plain associated function over one
process-wide hook cell. See the module docs for why this crate
reaches no
frust-hapticsdependency itself. - Press
Scale Spec - The optional press-scale spring spec a tappable surface may opt into:
the target scale to spring toward while pressed, and which named spring
drives it. Mirrors the reference’s
M3ETappable.pressedScale+.springpair. See the module docs for why this is spec data only, with no driving loop of its own.
Enums§
- Haptic
Signal - A haptic feedback intensity signal, mirroring the reference’s
M3EHapticFeedback(none/light/medium/heavy) plus aSliderTickvariant for a discrete stepped control’s tick — the reference’s separateM3EHaptics.selection()entry point, folded into this one closed vocabulary rather than left as a same-numbered alias ofLight, since a caller may want to route it to a different platform primitive (e.g. iOS’sUISelectionFeedbackGeneratorrather than an impact generator). - Press
Spring Id - A named reference into
crate::tokens::motion_scheme()’s six spring presets. APressScaleSpeccarries this id rather than a rawSpringDescso a widget’s event-pass code — which never reads a theme, seedocs/CODE_STANDARDS.md’s Theming conventions — can still name a resolvable spring by its motion-scheme slot;Self::resolvereads this crate’s own compile-time token table, not aThemerecovered from paint/layout context.
Constants§
- DISABLED_
CONTAINER_ OPACITY - Container opacity for a disabled component (12%). Same unwired status as
DISABLED_CONTENT_OPACITY. - DISABLED_
CONTENT_ OPACITY - Content (icon/label) opacity for a disabled component (38%). Not yet
wired to any live disabled-state widget in this crate — a named token
so the value has one source the moment one exists, the same
aspirational-but-stable status
crate::state_layerdocuments for its still-unwireddraggedstate. - DRAGGED_
OPACITY - Dragged-state overlay opacity (16%).
- FOCUS_
OPACITY - Focus-state overlay opacity (10%).
- HOVER_
OPACITY - Hover-state overlay opacity (8%). Source: androidx Compose Material3
StateTokens(v0_210), the same tablecrate::state_layercites. - PRESSED_
OPACITY - Pressed-state overlay opacity (source: androidx Compose Material3
StateTokensv0_210, retrieved 2026-07-17 — supersedes material-web v0.192’s 12%).