Skip to main content

cranpose_ui/modifier/
mod.rs

1//! Modifier system for Cranpose
2//!
3//! This module now acts as a thin builder around modifier elements. Each
4//! [`Modifier`] stores the element chain required by the modifier node system
5//! together with inspector metadata while resolved state is computed directly
6//! from the modifier nodes.
7
8use std::{fmt, rc::Rc};
9
10use cranpose_core::ProvidedValue;
11use smallvec::SmallVec;
12
13mod alignment;
14mod background;
15mod blur;
16mod border;
17mod chain;
18mod clickable;
19mod coordinator_rect;
20mod drag_and_drop;
21mod draw_cache;
22mod fill;
23pub(crate) mod focus;
24mod focus_ring;
25mod graphics_layer;
26mod key_input;
27mod layout_data;
28mod local;
29mod long_press;
30mod minimum_interactive;
31mod offset;
32mod padding;
33mod pointer_icon;
34pub(crate) mod pointer_input;
35mod pointer_opaque;
36mod resolved_modifiers;
37mod rotary_input;
38mod scroll;
39mod selectable;
40mod semantics;
41mod shadow;
42mod size;
43mod slices;
44mod toggleable;
45mod weight;
46mod window_root;
47mod wrap_content;
48mod z_index;
49
50pub(crate) use chain::{
51    InPlaceReach, InPlaceUpdate, ModifierChainHandle, ModifierChainInspectorNode,
52    ModifierLocalsHandle,
53};
54pub(crate) use coordinator_rect::CoordinatorRect;
55use cranpose_foundation::{
56    DynModifierElement, ModifierNodeElement, NodeCapabilities, ProgressBarRangeInfo,
57    SemanticsConfiguration, modifier_element,
58};
59pub use cranpose_foundation::{
60    PointerButton, PointerButtons, PointerEvent, PointerEventKind, PointerSource, RotaryScrollEvent,
61};
62pub use cranpose_ui_graphics::{
63    BlendMode, Brush, Color, CompositingStrategy, CornerRadii, DpOffset, EdgeInsets, GraphicsLayer,
64    LayerShape, Point, Rect, RenderEffect, RoundedCornerShape, RuntimeShader, Shadow, ShadowScope,
65    Size, TransformOrigin,
66};
67use cranpose_ui_layout::{Alignment, HorizontalAlignment, VerticalAlignment};
68pub use drag_and_drop::{
69    DragAndDropEvent, DragAndDropOutcome, DragAndDropPayload, DragAndDropPoint, DragAndDropSource,
70    DragAndDropSourceElement, DragAndDropSourceNode, DragAndDropState, DragAndDropTarget,
71    DragAndDropTargetElement, DragAndDropTargetNode,
72};
73#[cfg(test)]
74pub(crate) use focus::FocusRequesterElement;
75pub use focus::{FocusDirection, FocusRequestError, FocusRequester};
76pub use graphics_layer::GlassMaterial;
77pub use key_input::KeyEventRoute;
78pub(crate) use local::{
79    ModifierLocalAncestorResolver, ModifierLocalSource, ModifierLocalToken, ResolvedModifierLocal,
80};
81use local::{ModifierLocalConsumerElement, ModifierLocalProviderElement};
82pub use local::{ModifierLocalKey, ModifierLocalReadScope};
83pub use pointer_input::PointerInputScope;
84pub(crate) use resolved_modifiers::{DimensionConstraint, LayoutProperties, LayoutWeight};
85pub use resolved_modifiers::{ResolvedBackground, ResolvedModifiers};
86pub use rotary_input::RotaryInputModifierNode;
87#[cfg(feature = "test-helpers")]
88pub use scroll::{last_fling_velocity, reset_last_fling_velocity};
89use semantics::SemanticsElement;
90pub use semantics::{SemanticsRequester, collect_semantics_from_modifier};
91pub(crate) use semantics::{
92    SemanticsRequesterElement, collect_semantics_from_chain, semantics_reach_of_chain,
93};
94pub(crate) use slices::SlicesSnapshot;
95pub use slices::{
96    ModifierNodeSlices, ModifierNodeSlicesDebugStats, SliceTextRef, collect_modifier_slices,
97    collect_slices_from_modifier,
98};
99pub use window_root::{
100    WindowRootDescriptor, WindowRootElement, WindowRootEntry, WindowRootNode, WindowRootRegistry,
101    WindowRootRoutingScratch, is_window_root, nearest_window_root, nearest_window_roots,
102    nearest_window_roots_into, window_roots, window_roots_revision,
103};
104pub use z_index::{z_index_in_use, z_index_modifier_in_use};
105
106pub(crate) use crate::draw::{DrawCacheBuilder, DrawCommand};
107use crate::modifier_nodes::ClipToBoundsElement;
108
109#[derive(Clone, Debug, Default)]
110pub(crate) struct InspectorInfo {
111    properties: Vec<InspectorProperty>,
112}
113
114impl InspectorInfo {
115    pub(crate) fn new() -> Self {
116        Self::default()
117    }
118
119    pub(crate) fn add_property<V: Into<String>>(&mut self, name: &'static str, value: V) {
120        self.properties.push(InspectorProperty {
121            name,
122            value: value.into(),
123        });
124    }
125
126    pub(crate) fn properties(&self) -> &[InspectorProperty] {
127        &self.properties
128    }
129
130    pub(crate) fn is_empty(&self) -> bool {
131        self.properties.is_empty()
132    }
133
134    pub(crate) fn add_dimension(&mut self, name: &'static str, constraint: DimensionConstraint) {
135        self.add_property(name, describe_dimension(constraint));
136    }
137
138    pub(crate) fn add_offset_components(
139        &mut self,
140        x_name: &'static str,
141        y_name: &'static str,
142        offset: Point,
143    ) {
144        self.add_property(x_name, offset.x.to_string());
145        self.add_property(y_name, offset.y.to_string());
146    }
147
148    pub(crate) fn add_alignment<A>(&mut self, name: &'static str, alignment: A)
149    where
150        A: fmt::Debug,
151    {
152        self.add_property(name, format!("{alignment:?}"));
153    }
154}
155
156#[derive(Clone, Debug, PartialEq)]
157pub struct InspectorProperty {
158    pub name: &'static str,
159    pub value: String,
160}
161
162#[derive(Clone, Debug, PartialEq)]
163pub struct ModifierInspectorRecord {
164    pub name: &'static str,
165    pub properties: Vec<InspectorProperty>,
166}
167
168#[derive(Clone, Debug)]
169pub(crate) struct InspectorMetadata {
170    name: &'static str,
171    info: InspectorInfo,
172}
173
174impl InspectorMetadata {
175    pub(crate) fn new<F>(name: &'static str, recorder: F) -> Self
176    where
177        F: FnOnce(&mut InspectorInfo),
178    {
179        let mut info = InspectorInfo::new();
180        recorder(&mut info);
181        Self { name, info }
182    }
183
184    fn is_empty(&self) -> bool {
185        self.info.is_empty()
186    }
187
188    fn to_record(&self) -> ModifierInspectorRecord {
189        ModifierInspectorRecord {
190            name: self.name,
191            properties: self.info.properties().to_vec(),
192        }
193    }
194}
195
196fn describe_dimension(constraint: DimensionConstraint) -> String {
197    match constraint {
198        DimensionConstraint::Unspecified => "unspecified".to_string(),
199        DimensionConstraint::Points(value) => value.to_string(),
200        DimensionConstraint::Fraction(value) => format!("fraction({value})"),
201        DimensionConstraint::Intrinsic(size) => format!("intrinsic({size:?})"),
202    }
203}
204
205fn inspector_slice(inspector: &Option<Rc<[InspectorMetadata]>>) -> &[InspectorMetadata] {
206    inspector.as_deref().unwrap_or(&[])
207}
208
209/// The inspector metadata of two modifiers joined, first `first`'s, or
210/// `None` when neither has any.
211fn merged_inspector(
212    first: &[InspectorMetadata],
213    second: &[InspectorMetadata],
214) -> Option<Rc<[InspectorMetadata]>> {
215    if first.is_empty() && second.is_empty() {
216        return None;
217    }
218    Some(first.iter().chain(second).cloned().collect())
219}
220
221/// Whether `a` and `b` are equal elements: one element, as a kept chain
222/// shares them, or two that compare equal.
223fn same_element(a: &DynModifierElement, b: &DynModifierElement) -> bool {
224    Rc::ptr_eq(a, b) || a.equals_element(&**b)
225}
226
227pub(crate) fn inspector_metadata<F>(name: &'static str, recorder: F) -> InspectorMetadata
228where
229    F: FnOnce(&mut InspectorInfo),
230{
231    if !inspector_metadata_enabled() {
232        return InspectorMetadata::new(name, |_| {});
233    }
234    InspectorMetadata::new(name, recorder)
235}
236
237pub(crate) fn modifier_debug_enabled() -> bool {
238    #[cfg(not(target_arch = "wasm32"))]
239    {
240        cranpose_core::DIAGNOSTICS.compose_debug_modifiers.is_set()
241    }
242    #[cfg(target_arch = "wasm32")]
243    {
244        false
245    }
246}
247
248fn inspector_metadata_enabled() -> bool {
249    cfg!(any(test, feature = "inspection")) || modifier_debug_enabled()
250}
251
252/// A modifier's elements: a chain of up to four lives inline in its one
253/// shared allocation.
254type ModifierElements = SmallVec<[DynModifierElement; 4]>;
255
256/// The elements of a non-empty modifier. A lone element, as each link of a
257/// builder chain starts out, is held as it is; joining it to more moves the
258/// chain into one shared allocation, which the chain's later links extend
259/// in place while it is the only owner.
260#[derive(Clone)]
261enum Elements {
262    One(DynModifierElement),
263    Many(Rc<ModifierElements>),
264}
265
266impl Elements {
267    /// These elements followed by `more`. Elements move into the joined
268    /// chain; only those another modifier also holds are copied.
269    fn joined(self, more: Self) -> Self {
270        let mut chain = match self {
271            Self::One(first) => {
272                let mut chain = ModifierElements::new();
273                chain.push(first);
274                Rc::new(chain)
275            }
276            Self::Many(chain) => chain,
277        };
278        match Rc::get_mut(&mut chain) {
279            Some(owned) => more.move_into(owned),
280            None => {
281                let mut copy: ModifierElements = chain.iter().cloned().collect();
282                more.move_into(&mut copy);
283                chain = Rc::new(copy);
284            }
285        }
286        Self::Many(chain)
287    }
288
289    fn move_into(self, chain: &mut ModifierElements) {
290        match self {
291            Self::One(element) => chain.push(element),
292            Self::Many(shared) => match Rc::try_unwrap(shared) {
293                Ok(owned) => chain.extend(owned),
294                Err(shared) => chain.extend(shared.iter().cloned()),
295            },
296        }
297    }
298
299    /// Whether both are the same storage, so equal without comparing.
300    fn shares_storage(&self, other: &Self) -> bool {
301        match (self, other) {
302            (Self::One(a), Self::One(b)) => Rc::ptr_eq(a, b),
303            (Self::Many(a), Self::Many(b)) => Rc::ptr_eq(a, b),
304            _ => false,
305        }
306    }
307}
308
309impl std::ops::Deref for Elements {
310    type Target = [DynModifierElement];
311
312    fn deref(&self) -> &[DynModifierElement] {
313        match self {
314            Self::One(element) => std::slice::from_ref(element),
315            Self::Many(elements) => elements.as_slice(),
316        }
317    }
318}
319
320#[derive(Clone)]
321enum ModifierKind {
322    Empty,
323    Single {
324        elements: Elements,
325        /// `None` when no element records inspector metadata, which is
326        /// always outside tests and modifier debugging: no allocation.
327        inspector: Option<Rc<[InspectorMetadata]>>,
328    },
329}
330
331pub(crate) struct ModifierElementIterator<'a> {
332    inner: std::slice::Iter<'a, DynModifierElement>,
333}
334
335impl<'a> Iterator for ModifierElementIterator<'a> {
336    type Item = &'a DynModifierElement;
337
338    #[inline]
339    fn next(&mut self) -> Option<Self::Item> {
340        self.inner.next()
341    }
342
343    #[inline]
344    fn size_hint(&self) -> (usize, Option<usize>) {
345        self.inner.size_hint()
346    }
347}
348
349impl ExactSizeIterator for ModifierElementIterator<'_> {}
350
351pub(crate) struct ModifierInspectorIterator<'a> {
352    inner: std::slice::Iter<'a, InspectorMetadata>,
353}
354
355impl<'a> Iterator for ModifierInspectorIterator<'a> {
356    type Item = &'a InspectorMetadata;
357
358    #[inline]
359    fn next(&mut self) -> Option<Self::Item> {
360        self.inner.next()
361    }
362
363    #[inline]
364    fn size_hint(&self) -> (usize, Option<usize>) {
365        self.inner.size_hint()
366    }
367}
368
369impl ExactSizeIterator for ModifierInspectorIterator<'_> {}
370
371/// A modifier chain that can be applied to composable elements.
372///
373/// Modifiers allow you to decorate or augment a composable. Common operations include:
374/// - Adjusting layout (e.g., `padding`, `fill_max_size`)
375/// - Adding behavior (e.g., `clickable`, `scrollable`)
376/// - Drawing (e.g., `background`, `border`)
377///
378/// Modifiers are immutable and form a chain using the builder pattern.
379/// The order of modifiers matters: previous modifiers wrap subsequent ones.
380///
381/// # Example
382///
383/// ```rust,ignore
384/// Modifier::padding(16.0)     // Applied first (outer)
385///     .background(Color::Red) // Applied second
386///     .clickable(|| println!("Clicked")) // Applied last (inner)
387/// ```
388#[derive(Clone)]
389#[must_use = "a modifier does nothing until a composable receives it"]
390pub struct Modifier {
391    kind: ModifierKind,
392    element_count: usize,
393    provides_composition_locals: bool,
394}
395
396impl Default for Modifier {
397    fn default() -> Self {
398        Self {
399            kind: ModifierKind::Empty,
400            element_count: 0,
401            provides_composition_locals: false,
402        }
403    }
404}
405
406impl Modifier {
407    /// Returns a modifier that changes nothing.
408    pub fn empty() -> Self {
409        Self::default()
410    }
411
412    /// Creates a modifier from a custom modifier node element.
413    pub fn from_element<E>(element: E) -> Self
414    where
415        E: ModifierNodeElement,
416    {
417        Self::with_element(element)
418    }
419
420    /// Clip the content to the bounds of this modifier.
421    ///
422    /// Example: `Modifier::empty().clip_to_bounds()`
423    pub fn clip_to_bounds(self) -> Self {
424        let modifier = crate::thread_state::with(|state| {
425            state
426                .clip_to_bounds
427                .get_or_init(|| Self::with_element(ClipToBoundsElement::new()))
428                .clone()
429        });
430        let modifier =
431            modifier.with_inspector_metadata(inspector_metadata("clipToBounds", |info| {
432                info.add_property("clipToBounds", "true");
433            }));
434        self.then(modifier)
435    }
436
437    /// Provides the value that `value` returns for `key` to later modifiers in this chain and to
438    /// nodes below. Compose's `Modifier.modifierLocalProvider`.
439    pub fn modifier_local_provider<T, F>(self, key: ModifierLocalKey<T>, value: F) -> Self
440    where
441        T: 'static,
442        F: Fn() -> T + 'static,
443    {
444        let element = ModifierLocalProviderElement::new(key, value);
445        let modifier = Self::from_dyn_element(modifier_element(element));
446        self.then(modifier)
447    }
448
449    /// Calls `consumer` to read modifier local values through a [`ModifierLocalReadScope`]. It runs
450    /// again when a value it read changes. Compose's `Modifier.modifierLocalConsumer`.
451    pub fn modifier_local_consumer<F>(self, consumer: F) -> Self
452    where
453        F: for<'scope> Fn(&mut ModifierLocalReadScope<'scope>) + 'static,
454    {
455        let element = ModifierLocalConsumerElement::new(consumer);
456        let modifier = Self::from_dyn_element(modifier_element(element));
457        self.then(modifier)
458    }
459
460    /// Says what a screen reader reads for this node, as one spec value.
461    ///
462    /// This is [`Modifier::semantics`] with a value in place of a closure.
463    /// Compose has only the closure form, because Kotlin's receiver lambda
464    /// makes `semantics { contentDescription = "Save" }` read well; Rust has
465    /// none, so a spec value reads better, costs one chain element rather
466    /// than one per property, and can be compared with another.
467    ///
468    /// Example:
469    /// `Modifier::empty().semantics_spec(SemanticsSpec::new().content_description("Amount").error("needs a number"))`
470    pub fn semantics_spec(self, spec: cranpose_foundation::SemanticsSpec) -> Self {
471        self.stable_semantics(move |config: &mut SemanticsConfiguration| config.merge(&spec))
472    }
473
474    /// Records what a screen reader reads for this node. The recorder may
475    /// read live state, so every semantics update runs it again; one whose
476    /// answer changes without the node changing can say so with a
477    /// [`SemanticsRequester`](crate::SemanticsRequester).
478    pub fn semantics<F>(self, recorder: F) -> Self
479    where
480        F: Fn(&mut SemanticsConfiguration) + 'static,
481    {
482        self.semantics_recorder(Rc::new(recorder), true)
483    }
484
485    /// [`Modifier::semantics`] for a recorder that reads nothing but what it
486    /// captured. Its answer changes only when the modifier is set again, which
487    /// invalidates the node's semantics, so a semantics update keeps what it
488    /// reported instead of running it.
489    ///
490    /// Example: `Modifier::empty().stable_semantics(move |config| config.content_description = Some(label.clone()))`
491    pub fn stable_semantics<F>(self, recorder: F) -> Self
492    where
493        F: Fn(&mut SemanticsConfiguration) + 'static,
494    {
495        self.semantics_recorder(Rc::new(recorder), false)
496    }
497
498    fn semantics_recorder(
499        self,
500        recorder: Rc<dyn Fn(&mut SemanticsConfiguration)>,
501        reads_live_state: bool,
502    ) -> Self {
503        let metadata = if inspector_metadata_enabled() {
504            let mut preview = SemanticsConfiguration::default();
505            recorder(&mut preview);
506            let description = preview.content_description.clone();
507            let state_description = preview.state_description.clone();
508            let role = preview.role;
509            let is_clickable = preview.is_activatable();
510            let canvas_children = preview.canvas_children.len();
511            inspector_metadata("semantics", move |info| {
512                if let Some(desc) = &description {
513                    info.add_property("contentDescription", desc.clone());
514                }
515                if let Some(state) = &state_description {
516                    info.add_property("stateDescription", state.clone());
517                }
518                if let Some(role) = role {
519                    info.add_property("role", format!("{role:?}"));
520                }
521                if is_clickable {
522                    info.add_property("isClickable", "true");
523                }
524                if canvas_children > 0 {
525                    info.add_property("canvasSemanticsChildren", canvas_children.to_string());
526                }
527            })
528        } else {
529            inspector_metadata("semantics", |_| {})
530        };
531        let element = SemanticsElement::new(recorder, reads_live_state);
532        let modifier =
533            Self::from_dyn_element(modifier_element(element)).with_inspector_metadata(metadata);
534        self.then(modifier)
535    }
536
537    /// Tells a screen reader the value this control holds inside a range, so
538    /// it reads the value and offers its own way to change it.
539    ///
540    /// This is Compose's `Modifier.progressSemantics(value, valueRange,
541    /// steps)`. Without it a slider reads as text and a person who cannot see
542    /// the screen has no way to move it.
543    ///
544    /// Example: `Modifier::empty().progress_semantics(0.35, 0.0, 1.0, 0)`
545    pub fn progress_semantics(self, current: f32, start: f32, end: f32, steps: u32) -> Self {
546        let info = ProgressBarRangeInfo::new(current, start, end, steps);
547        self.stable_semantics(move |config| config.progress = Some(info))
548    }
549
550    /// Marks this control as one that opens a list of choices, so a reader
551    /// says "combo box" rather than "button" and knows to look for the
552    /// choice it holds. Compose's `Role.DropdownList`.
553    pub fn dropdown_list(self) -> Self {
554        self.role(cranpose_foundation::SemanticsWidgetRole::DropdownList)
555    }
556
557    /// Marks this control as one that holds one value out of an ordered set,
558    /// so a reader steps through them. Compose's `Role.ValuePicker`.
559    pub fn value_picker(self) -> Self {
560        self.role(cranpose_foundation::SemanticsWidgetRole::ValuePicker)
561    }
562
563    /// Says what this control does when a screen reader asks it to open, and
564    /// marks it as closed right now: a reader offers "expand" and says the
565    /// control is collapsed. Compose's
566    /// `Modifier.semantics { expand { … } }`.
567    pub fn expand(self, action: impl Fn() -> bool + 'static) -> Self {
568        let action = cranpose_foundation::SemanticsExpand::new(action);
569        self.stable_semantics(move |config| config.expand = Some(action.clone()))
570    }
571
572    /// Says what a long press on this control does, so a screen reader can
573    /// ask for it and read the label out first: "Remove receipt". Compose's
574    /// `Modifier.semantics { onLongClick("Remove receipt") { … } }`, which
575    /// `Modifier.combinedClickable(onLongClickLabel = …)` fills in for a
576    /// control that takes a long press from a finger too.
577    ///
578    /// The label is asked for, not optional as in Compose: Android is the one
579    /// platform of the four with a long press of its own, and the other three
580    /// list the action by name, so a nameless one reads as nothing.
581    pub fn on_long_click(
582        self,
583        label: impl Into<String>,
584        action: impl Fn() -> bool + 'static,
585    ) -> Self {
586        let label = label.into();
587        let action = cranpose_foundation::SemanticsLongClick::new(action);
588        self.stable_semantics(move |config| {
589            config.on_long_click_label = Some(label.clone());
590            config.on_long_click = Some(action.clone());
591        })
592    }
593
594    /// Says what this control does on VoiceOver's magic tap, the two finger
595    /// double tap for the main action of a screen, and the verb phrase the
596    /// other platforms list it under: "Take the photo". SwiftUI's
597    /// `accessibilityAction(.magicTap)`.
598    pub fn on_magic_tap(
599        self,
600        label: impl Into<String>,
601        action: impl Fn() -> bool + 'static,
602    ) -> Self {
603        let label = label.into();
604        let action = cranpose_foundation::SemanticsMagicTap::new(action);
605        self.stable_semantics(move |config| {
606            config.on_magic_tap_label = Some(label.clone());
607            config.on_magic_tap = Some(action.clone());
608        })
609    }
610
611    /// The short names a person says to Voice Control to reach this control,
612    /// when the name a reader hears is too long to say. SwiftUI's
613    /// `accessibilityInputLabels`.
614    pub fn input_labels<S: Into<String>>(self, labels: impl IntoIterator<Item = S>) -> Self {
615        let labels: Vec<String> = labels.into_iter().map(Into::into).collect();
616        self.stable_semantics(move |config| config.input_labels.clone_from(&labels))
617    }
618
619    /// The language of this control's text, as a BCP 47 tag such as "de" or
620    /// "pt-BR", so a reader picks the right voice. SwiftUI's
621    /// `accessibilityLanguage`, ARIA's `lang`.
622    pub fn language(self, tag: impl Into<String>) -> Self {
623        let tag = tag.into();
624        self.stable_semantics(move |config| config.language = Some(tag.clone()))
625    }
626
627    /// Says what this control does when a screen reader asks it to close, and
628    /// marks it as open right now. Compose's
629    /// `Modifier.semantics { collapse { … } }`.
630    pub fn collapse(self, action: impl Fn() -> bool + 'static) -> Self {
631        let action = cranpose_foundation::SemanticsExpand::new(action);
632        self.stable_semantics(move |config| config.collapse = Some(action.clone()))
633    }
634
635    /// Says what this control does when a screen reader asks to send it away:
636    /// a row a sighted person swipes off, a sheet a sighted person taps
637    /// outside of. A reader that cannot make the gesture gets the same way
638    /// out. Compose's `Modifier.semantics { dismiss { … } }`.
639    pub fn dismiss(self, action: impl Fn() -> bool + 'static) -> Self {
640        let action = cranpose_foundation::SemanticsDismiss::new(action);
641        self.stable_semantics(move |config| config.dismiss = Some(action.clone()))
642    }
643
644    /// Says what this list does when a screen reader asks for the row at an
645    /// index, so a reader reaches row 300 of a long list at once instead of
646    /// paging to it. The index counts rows from zero and the answer says
647    /// whether the list moved. `LazyColumn` and `LazyRow` declare it on their
648    /// own. Compose's `Modifier.semantics { scrollToIndex { … } }`.
649    pub fn scroll_to_index(self, action: impl Fn(usize) -> bool + 'static) -> Self {
650        let action = cranpose_foundation::SemanticsScrollToIndex::new(action);
651        self.stable_semantics(move |config| config.scroll_to_index = Some(action.clone()))
652    }
653
654    /// Moves this node in the order a screen reader visits the nodes beside
655    /// it: a smaller number comes first, and a node left alone keeps the
656    /// order the app laid it out in. A search field drawn last but meant to
657    /// be read first takes a negative number. Compose's
658    /// `Modifier.semantics { traversalIndex = -1f }`.
659    pub fn traversal_index(self, index: f32) -> Self {
660        self.stable_semantics(move |config| config.traversal_index = index)
661    }
662
663    /// Marks a field as one that holds a secret, so no screen reader reads
664    /// its text out: a reader hears the name the app gave the field, and
665    /// "password" in place of the text. Compose's
666    /// `Modifier.semantics { password() }`.
667    pub fn password(self) -> Self {
668        self.stable_semantics(|config| config.password = true)
669    }
670
671    /// Says why the control's content is wrong, so a screen reader reads
672    /// "invalid, the amount needs a number" after the control's state.
673    /// Compose's `Modifier.semantics { error("...") }`.
674    pub fn error(self, message: impl Into<String>) -> Self {
675        let message = message.into();
676        self.stable_semantics(move |config| config.error = Some(message.clone()))
677    }
678
679    /// Tells a screen reader what using the control does or what it wants,
680    /// read after its other words: "double tap to choose a color", "a name of
681    /// up to 20 letters". Compose's `Modifier.semantics { hintText = "..." }`.
682    pub fn hint_text(self, hint: impl Into<String>) -> Self {
683        let hint = hint.into();
684        self.stable_semantics(move |config| config.hint_text = Some(hint.clone()))
685    }
686
687    /// Names the screen or pane this node is the root of, so a screen reader
688    /// hears where it is when the app moves on: "Library" as the library
689    /// opens. Compose's `Modifier.semantics { paneTitle = "..." }`.
690    pub fn pane_title(self, title: impl Into<String>) -> Self {
691        let title = title.into();
692        self.stable_semantics(move |config| config.pane_title = Some(title.clone()))
693    }
694
695    /// Makes the selectable controls under this node one group, so a screen
696    /// reader says which of how many a tab or a radio button is: "Library,
697    /// tab, 2 of 5". `LiquidTabBar` declares it on its own. Compose's
698    /// `Modifier.selectableGroup()`.
699    pub fn selectable_group(self) -> Self {
700        self.stable_semantics(|config| config.selectable_group = true)
701    }
702
703    /// Makes a screen reader take this node and the text under it as one
704    /// stop, the way it does for a button: a row whose name, count and price
705    /// belong together reads as "Milk, 2, 3.40" and not as three stops.
706    /// Compose's `Modifier.semantics(mergeDescendants = true) {}`.
707    pub fn merge_descendants(self) -> Self {
708        self.stable_semantics(|config| config.merge_descendants = true)
709    }
710
711    /// Takes this node and everything under it out of what a screen reader
712    /// sees: a decorative image, or a placeholder drawn under a field that
713    /// carries the same words as its name. Compose's
714    /// `semantics { hideFromAccessibility() }`.
715    pub fn hide_from_accessibility(self) -> Self {
716        self.stable_semantics(|config| config.hidden = true)
717    }
718
719    /// Marks this component as a heading, so a screen reader lists it among
720    /// the headings of the screen and a person can jump between them.
721    ///
722    /// This is Compose's `Modifier.semantics { heading() }`.
723    pub fn heading(self) -> Self {
724        self.role(cranpose_foundation::SemanticsWidgetRole::Header)
725    }
726
727    /// Tells a screen reader what kind of control this is, when the widget
728    /// does not say so on its own.
729    ///
730    /// This is Compose's `Modifier.semantics { role = Role.Button }`.
731    pub fn role(self, role: cranpose_foundation::SemanticsWidgetRole) -> Self {
732        self.stable_semantics(move |config| config.role = Some(role))
733    }
734
735    /// Makes a screen reader read this component's text out whenever it
736    /// changes, without the reader's cursor on it: a status line, a toast, a
737    /// count that moves.
738    ///
739    /// This is Compose's `Modifier.semantics { liveRegion = LiveRegionMode.Polite }`.
740    pub fn live_region(self, mode: cranpose_foundation::LiveRegionMode) -> Self {
741        self.stable_semantics(move |config| config.live_region = Some(mode))
742    }
743
744    /// Gives this component the text a screen reader reads for it, for a
745    /// drawing, an icon or a control with no text of its own.
746    ///
747    /// This is Compose's `Modifier.semantics { contentDescription = "..." }`.
748    pub fn content_description(self, description: impl Into<String>) -> Self {
749        let description = description.into();
750        self.stable_semantics(move |config| config.content_description = Some(description.clone()))
751    }
752
753    /// Names this component for tests, which find it by the name instead of
754    /// its text or position. Screen readers never receive the name.
755    ///
756    /// This is Compose's `Modifier.testTag(tag)`.
757    pub fn test_tag(self, tag: impl Into<String>) -> Self {
758        let tag = tag.into();
759        self.stable_semantics(move |config| config.test_tag = Some(tag.clone()))
760    }
761
762    /// Binds a [`SemanticsRequester`] to this node, so an app can mark the
763    /// node's semantics for re-collection without recomposing or laying out.
764    ///
765    /// Pair it with [`semantics`](Self::semantics) on the same node when the
766    /// recorder reads state the composition does not observe — app state behind
767    /// a `RefCell`, a game's own model — which is the case a recorder cannot
768    /// signal for itself.
769    pub fn semantics_requester(self, requester: &SemanticsRequester) -> Self {
770        let element = SemanticsRequesterElement::new(requester.clone());
771        let modifier = Self::from_dyn_element(modifier_element(element));
772        self.then(modifier)
773    }
774
775    /// Enables debug logging for this modifier chain.
776    ///
777    /// When enabled, logs the entire modifier chain structure including:
778    /// - Element types and their properties
779    /// - Inspector metadata
780    /// - Capability flags
781    ///
782    /// This is useful for debugging modifier composition issues and understanding
783    /// how the modifier chain is structured at runtime.
784    ///
785    /// Example:
786    /// ```text
787    /// Modifier::empty()
788    ///     .padding(8.0)
789    ///     .background(Color(1.0, 0.0, 0.0, 1.0))
790    ///     .debug_chain("MyWidget")
791    /// ```
792    pub fn debug_chain(self, tag: &'static str) -> Self {
793        use cranpose_foundation::{ModifierNode, ModifierNodeContext, NodeCapabilities, NodeState};
794
795        #[derive(Clone)]
796        struct DebugChainElement {
797            tag: &'static str,
798        }
799
800        impl fmt::Debug for DebugChainElement {
801            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802                f.debug_struct("DebugChainElement")
803                    .field("tag", &self.tag)
804                    .finish()
805            }
806        }
807
808        impl PartialEq for DebugChainElement {
809            fn eq(&self, other: &Self) -> bool {
810                self.tag == other.tag
811            }
812        }
813
814        impl Eq for DebugChainElement {}
815
816        impl std::hash::Hash for DebugChainElement {
817            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
818                self.tag.hash(state);
819            }
820        }
821
822        impl ModifierNodeElement for DebugChainElement {
823            type Node = DebugChainNode;
824
825            fn create(&self) -> Self::Node {
826                DebugChainNode::new(self.tag)
827            }
828
829            fn update(&self, node: &mut Self::Node) {
830                node.tag = self.tag;
831            }
832
833            fn capabilities(&self) -> NodeCapabilities {
834                NodeCapabilities::empty()
835            }
836        }
837
838        struct DebugChainNode {
839            tag: &'static str,
840            state: NodeState,
841        }
842
843        impl DebugChainNode {
844            fn new(tag: &'static str) -> Self {
845                Self {
846                    tag,
847                    state: NodeState::new(),
848                }
849            }
850        }
851
852        #[expect(clippy::print_stderr, reason = "debug_chain prints its lifecycle")]
853        impl ModifierNode for DebugChainNode {
854            fn on_attach(&mut self, _context: &mut dyn ModifierNodeContext) {
855                eprintln!("[debug_chain:{}] Modifier chain attached", self.tag);
856            }
857
858            fn on_detach(&mut self) {
859                eprintln!("[debug_chain:{}] Modifier chain detached", self.tag);
860            }
861
862            fn on_reset(&mut self) {
863                eprintln!("[debug_chain:{}] Modifier chain reset", self.tag);
864            }
865        }
866
867        impl cranpose_foundation::DelegatableNode for DebugChainNode {
868            fn node_state(&self) -> &NodeState {
869                &self.state
870            }
871        }
872
873        let element = DebugChainElement { tag };
874        let modifier = Self::from_dyn_element(modifier_element(element));
875        self.then(modifier)
876            .with_inspector_metadata(inspector_metadata("debugChain", move |info| {
877                info.add_property("tag", tag);
878            }))
879    }
880
881    /// This modifier followed by `next`.
882    ///
883    /// It takes `self`: a modifier built link by link owns its elements
884    /// alone, so each link joins them in place instead of copying the chain.
885    /// Clone a modifier first to keep using it; the join then copies it.
886    pub fn then(self, next: Self) -> Self {
887        let ModifierKind::Single {
888            elements,
889            inspector,
890        } = self.kind
891        else {
892            return next;
893        };
894        let ModifierKind::Single {
895            elements: next_elements,
896            inspector: next_inspector,
897        } = next.kind
898        else {
899            return Self {
900                kind: ModifierKind::Single {
901                    elements,
902                    inspector,
903                },
904                ..self
905            };
906        };
907
908        let inspector = if next_inspector.is_none() {
909            inspector
910        } else {
911            merged_inspector(
912                inspector_slice(&inspector),
913                inspector_slice(&next_inspector),
914            )
915        };
916        Self {
917            kind: ModifierKind::Single {
918                elements: elements.joined(next_elements),
919                inspector,
920            },
921            element_count: self.element_count + next.element_count,
922            provides_composition_locals: self.provides_composition_locals
923                || next.provides_composition_locals,
924        }
925    }
926
927    /// How many elements the modifier holds.
928    pub(crate) fn element_count(&self) -> usize {
929        self.element_count
930    }
931
932    pub(crate) fn iter_elements(&self) -> ModifierElementIterator<'_> {
933        match &self.kind {
934            ModifierKind::Empty => ModifierElementIterator { inner: [].iter() },
935            ModifierKind::Single { elements, .. } => ModifierElementIterator {
936                inner: elements.iter(),
937            },
938        }
939    }
940
941    pub(crate) fn provided_composition_locals(&self) -> Vec<ProvidedValue> {
942        if !self.provides_composition_locals {
943            return Vec::new();
944        }
945        self.iter_elements()
946            .flat_map(|element| element.provided_composition_locals())
947            .collect()
948    }
949
950    pub(crate) fn iter_inspector_metadata(&self) -> ModifierInspectorIterator<'_> {
951        match &self.kind {
952            ModifierKind::Empty => ModifierInspectorIterator { inner: [].iter() },
953            ModifierKind::Single { inspector, .. } => ModifierInspectorIterator {
954                inner: inspector_slice(inspector).iter(),
955            },
956        }
957    }
958
959    #[cfg(test)]
960    pub(crate) fn elements(&self) -> Vec<DynModifierElement> {
961        match &self.kind {
962            ModifierKind::Empty => Vec::new(),
963            ModifierKind::Single { elements, .. } => elements.to_vec(),
964        }
965    }
966
967    pub(crate) fn inspector_metadata(&self) -> Vec<InspectorMetadata> {
968        match &self.kind {
969            ModifierKind::Empty => Vec::new(),
970            ModifierKind::Single { inspector, .. } => {
971                inspector.as_deref().map(<[_]>::to_vec).unwrap_or_default()
972            }
973        }
974    }
975
976    /// Returns the largest of the four edge paddings, in logical pixels.
977    pub fn total_padding(&self) -> f32 {
978        let padding = self.padding_values();
979        padding
980            .left
981            .max(padding.right)
982            .max(padding.top)
983            .max(padding.bottom)
984    }
985
986    /// Returns the size when both width and height are fixed lengths in logical pixels, or `None`
987    /// otherwise.
988    pub fn explicit_size(&self) -> Option<Size> {
989        let props = self.layout_properties();
990        match (props.width, props.height) {
991            (DimensionConstraint::Points(width), DimensionConstraint::Points(height)) => {
992                Some(Size { width, height })
993            }
994            _ => None,
995        }
996    }
997
998    /// Returns the padding on each edge that the chain adds, in logical pixels.
999    pub fn padding_values(&self) -> EdgeInsets {
1000        self.resolved_modifiers().padding()
1001    }
1002
1003    pub(crate) fn layout_properties(&self) -> LayoutProperties {
1004        self.resolved_modifiers().layout_properties()
1005    }
1006
1007    /// Returns the alignment set by [`Self::align`] for a `Box` parent, or `None` when none is set.
1008    pub fn box_alignment(&self) -> Option<Alignment> {
1009        self.layout_properties().box_alignment()
1010    }
1011
1012    /// Returns the horizontal alignment set for a `Column` parent, or `None` when none is set.
1013    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
1014        self.layout_properties().column_alignment()
1015    }
1016
1017    /// Returns the vertical alignment set for a `Row` parent, or `None` when none is set.
1018    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
1019        self.layout_properties().row_alignment()
1020    }
1021
1022    /// Returns the draw commands of the chain in the order they draw.
1023    pub fn draw_commands(&self) -> Vec<DrawCommand> {
1024        collect_slices_from_modifier(self).draw_commands().to_vec()
1025    }
1026
1027    /// Returns whether the chain clips content to the node's bounds.
1028    pub fn clips_to_bounds(&self) -> bool {
1029        collect_slices_from_modifier(self).clip_to_bounds()
1030    }
1031
1032    /// Returns structured inspector records for each modifier element.
1033    pub fn collect_inspector_records(&self) -> Vec<ModifierInspectorRecord> {
1034        self.inspector_metadata()
1035            .iter()
1036            .map(InspectorMetadata::to_record)
1037            .collect()
1038    }
1039
1040    /// Resolves the chain into its padding, layout properties and offset.
1041    pub fn resolved_modifiers(&self) -> ResolvedModifiers {
1042        let mut handle = ModifierChainHandle::new();
1043        let _ = handle.update(self);
1044        handle.resolved_modifiers()
1045    }
1046
1047    /// A modifier of the one `element`. Platform crates build their own
1048    /// modifiers on it, the way [`Modifier::window_root`] is built.
1049    pub fn with_element<E>(element: E) -> Self
1050    where
1051        E: ModifierNodeElement,
1052    {
1053        Self::from_dyn_element(modifier_element(element))
1054    }
1055
1056    /// A modifier of the one `element`.
1057    pub(crate) fn from_dyn_element(element: DynModifierElement) -> Self {
1058        Self {
1059            provides_composition_locals: element.provides_composition_locals(),
1060            kind: ModifierKind::Single {
1061                elements: Elements::One(element),
1062                inspector: None,
1063            },
1064            element_count: 1,
1065        }
1066    }
1067
1068    pub(crate) fn with_inspector_metadata(self, metadata: InspectorMetadata) -> Self {
1069        if metadata.is_empty() {
1070            return self;
1071        }
1072        match self.kind {
1073            ModifierKind::Empty => self,
1074            ModifierKind::Single {
1075                elements,
1076                inspector,
1077            } => {
1078                let new_inspector = inspector_slice(&inspector)
1079                    .iter()
1080                    .cloned()
1081                    .chain(std::iter::once(metadata))
1082                    .collect();
1083                Self {
1084                    kind: ModifierKind::Single {
1085                        elements,
1086                        inspector: Some(new_inspector),
1087                    },
1088                    element_count: self.element_count,
1089                    provides_composition_locals: self.provides_composition_locals,
1090                }
1091            }
1092        }
1093    }
1094
1095    /// Checks whether two modifiers are structurally equivalent for layout decisions.
1096    ///
1097    /// This ignores identity-sensitive modifier elements (e.g., draw closures) so
1098    /// draw-only updates do not force measure/layout invalidation.
1099    pub fn structural_eq(&self, other: &Self) -> bool {
1100        self.eq_internal(other, false)
1101    }
1102
1103    /// The update in place to `other`, when `other` has this modifier's
1104    /// elements in the same order, equal except elements that
1105    /// [`InPlaceReach::of`] accepts: how far it reaches, and the elements it
1106    /// updates. `None` when another element differs, or the chain is too
1107    /// long to name its elements; an element that asks for an update on
1108    /// every set counts as different.
1109    pub(crate) fn in_place_update(&self, other: &Self) -> Option<InPlaceUpdate> {
1110        if self.element_count != other.element_count || self.element_count > u64::BITS as usize {
1111            return None;
1112        }
1113        let mut update = InPlaceUpdate {
1114            reach: InPlaceReach::Node,
1115            elements: 0,
1116        };
1117        for (index, (a, b)) in self.iter_elements().zip(other.iter_elements()).enumerate() {
1118            if a.requires_update() || b.requires_update() {
1119                return None;
1120            }
1121            let element_type = a.element_type();
1122            match InPlaceReach::of(element_type) {
1123                // A text updates in place whether it changed or not.
1124                Some(InPlaceReach::Node) if b.element_type() == element_type => {
1125                    update.elements |= 1 << index;
1126                }
1127                Some(InPlaceReach::Resolved) if b.element_type() == element_type => {
1128                    if !same_element(a, b) {
1129                        update.reach = InPlaceReach::Resolved;
1130                        update.elements |= 1 << index;
1131                    }
1132                }
1133                _ => {
1134                    if !same_element(a, b) {
1135                        return None;
1136                    }
1137                }
1138            }
1139        }
1140        Some(update)
1141    }
1142
1143    fn eq_internal(&self, other: &Self, consider_always_update: bool) -> bool {
1144        if self.element_count != other.element_count {
1145            return false;
1146        }
1147
1148        match (&self.kind, &other.kind) {
1149            (ModifierKind::Empty, ModifierKind::Empty) => true,
1150            (
1151                ModifierKind::Single {
1152                    elements: e1,
1153                    inspector: _,
1154                },
1155                ModifierKind::Single {
1156                    elements: e2,
1157                    inspector: _,
1158                },
1159            ) => {
1160                if e1.shares_storage(e2) {
1161                    return true;
1162                }
1163
1164                if e1.len() != e2.len() {
1165                    return false;
1166                }
1167
1168                for (a, b) in e1.iter().zip(e2.iter()) {
1169                    // One element in both, as a kept chain shares them.
1170                    if Rc::ptr_eq(a, b) {
1171                        continue;
1172                    }
1173                    if !consider_always_update
1174                        && a.element_type() == b.element_type()
1175                        && a.capabilities() == NodeCapabilities::DRAW
1176                        && b.capabilities() == NodeCapabilities::DRAW
1177                    {
1178                        continue;
1179                    }
1180
1181                    if consider_always_update && (a.requires_update() || b.requires_update()) {
1182                        if !Rc::ptr_eq(a, b) {
1183                            return false;
1184                        }
1185                        continue;
1186                    }
1187
1188                    if !a.equals_element(&**b) {
1189                        return false;
1190                    }
1191                }
1192
1193                true
1194            }
1195            _ => false,
1196        }
1197    }
1198}
1199
1200impl PartialEq for Modifier {
1201    fn eq(&self, other: &Self) -> bool {
1202        self.eq_internal(other, true)
1203    }
1204}
1205
1206impl Eq for Modifier {}
1207
1208impl fmt::Display for Modifier {
1209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1210        match &self.kind {
1211            ModifierKind::Empty => write!(f, "Modifier.empty"),
1212            ModifierKind::Single { elements, .. } => {
1213                if elements.is_empty() {
1214                    return write!(f, "Modifier.empty");
1215                }
1216                write!(f, "Modifier[")?;
1217                for (index, element) in elements.iter().enumerate() {
1218                    if index > 0 {
1219                        write!(f, ", ")?;
1220                    }
1221                    let name = element.inspector_name();
1222                    let mut properties = Vec::new();
1223                    element.record_inspector_properties(&mut |prop, value| {
1224                        properties.push(format!("{prop}={value}"));
1225                    });
1226                    if properties.is_empty() {
1227                        write!(f, "{name}")?;
1228                    } else {
1229                        write!(f, "{name}({})", properties.join(", "))?;
1230                    }
1231                }
1232                write!(f, "]")
1233            }
1234        }
1235    }
1236}
1237
1238impl fmt::Debug for Modifier {
1239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1240        fmt::Display::fmt(self, f)
1241    }
1242}
1243
1244#[cfg(test)]
1245#[path = "tests/modifier_tests.rs"]
1246mod tests;