Skip to main content

Module interaction

Module interaction 

Source
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§

InteractionState
Tracks a widget’s hover/focus/pressed/dragged interaction state. See the module docs for the two opacity resolvers this feeds.
MaterialHaptics
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-haptics dependency itself.
PressScaleSpec
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 + .spring pair. See the module docs for why this is spec data only, with no driving loop of its own.

Enums§

HapticSignal
A haptic feedback intensity signal, mirroring the reference’s M3EHapticFeedback (none/light/medium/heavy) plus a SliderTick variant for a discrete stepped control’s tick — the reference’s separate M3EHaptics.selection() entry point, folded into this one closed vocabulary rather than left as a same-numbered alias of Light, since a caller may want to route it to a different platform primitive (e.g. iOS’s UISelectionFeedbackGenerator rather than an impact generator).
PressSpringId
A named reference into crate::tokens::motion_scheme()’s six spring presets. A PressScaleSpec carries this id rather than a raw SpringDesc so a widget’s event-pass code — which never reads a theme, see docs/CODE_STANDARDS.md’s Theming conventions — can still name a resolvable spring by its motion-scheme slot; Self::resolve reads this crate’s own compile-time token table, not a Theme recovered 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_layer documents for its still-unwired dragged state.
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 table crate::state_layer cites.
PRESSED_OPACITY
Pressed-state overlay opacity (source: androidx Compose Material3 StateTokens v0_210, retrieved 2026-07-17 — supersedes material-web v0.192’s 12%).