Skip to main content

cranpose_ui/
modifier_nodes.rs

1//! Concrete implementations of modifier nodes for common modifiers.
2//!
3//! This module provides node-backed implementations of layout and draw modifiers
4//! following Jetpack Compose's Modifier.Node architecture. All modifiers are now
5//! node-based, achieving complete parity with Kotlin's modifier system.
6//!
7//! # Overview
8//!
9//! The Modifier.Node system provides excellent performance through:
10//! - **Node reuse** — Node instances are reused across recompositions (zero allocations when stable)
11//! - **Targeted invalidation** — Only affected phases (layout/draw/pointer/focus) are invalidated
12//! - **Lifecycle hooks** — `on_attach`, `on_detach`, `update` for efficient state management
13//! - **Capability-driven dispatch** — Nodes declare capabilities via `NodeCapabilities` bits
14//!
15//! # Example Usage
16//!
17//! ```text
18//! use cranpose_foundation::{modifier_element, ModifierNodeChain, BasicModifierNodeContext};
19//! use cranpose_ui::{PaddingElement, EdgeInsets};
20//!
21//! let mut chain = ModifierNodeChain::new();
22//! let mut context = BasicModifierNodeContext::new();
23//!
24//! // Create a padding modifier element
25//! let elements = vec![modifier_element(PaddingElement::new(EdgeInsets::uniform(16.0)))];
26//!
27//! // Reconcile the chain (attaches new nodes, reuses existing)
28//! chain.update_from_slice(&elements, &mut context);
29//!
30//! // Update with different padding - reuses the same node instance
31//! let elements = vec![modifier_element(PaddingElement::new(EdgeInsets::uniform(24.0)))];
32//! chain.update_from_slice(&elements, &mut context);
33//! // Zero allocations on this update!
34//! ```
35//!
36//! # Available Nodes
37//!
38//! ## Layout Modifiers
39//! - [`PaddingNode`] / [`PaddingElement`]: Adds padding around content
40//! - [`SizeNode`] / [`SizeElement`]: Enforces specific dimensions
41//! - [`FillNode`] / [`FillElement`]: Fills available space with optional fractions
42//! - [`OffsetNode`] / [`OffsetElement`]: Translates content by offset
43//! - [`WeightNode`] / [`WeightElement`]: Proportional sizing in flex containers
44//! - [`AlignmentNode`] / [`AlignmentElement`]: Alignment within parent
45//! - [`IntrinsicSizeNode`] / [`IntrinsicSizeElement`]: Intrinsic measurement
46//!
47//! ## Draw Modifiers
48//! - [`BackgroundNode`] / [`BackgroundElement`]: Draws a background color
49//! - [`AlphaNode`] / [`AlphaElement`]: Applies alpha transparency
50//! - [`CornerShapeNode`] / [`CornerShapeElement`]: Rounded corner clipping
51//! - [`GraphicsLayerNode`] / [`GraphicsLayerElement`]: Advanced transformations
52//!
53//! ## Input Modifiers
54//! - [`ClickableNode`] / [`ClickableElement`]: Handles click/tap interactions (pointer input)
55//!
56//! # Architecture Notes
57//!
58//! This is the **only** modifier implementation — there is no alternate "value-based" system.
59//! All modifier factories in `Modifier` return `ModifierNodeElement` instances that create
60//! these nodes. The system achieves complete 1:1 parity with Jetpack Compose's modifier
61//! architecture.
62
63use cranpose_core::NodeId;
64use cranpose_foundation::{
65    Constraints, DelegatableNode, DrawModifierNode, DrawScope, InvalidationKind,
66    LayoutModifierNode, Measurable, ModifierNode, ModifierNodeContext, ModifierNodeElement,
67    NodeCapabilities, NodeState, PointerEvent, PointerEventKind, PointerInputNode, Size,
68};
69use cranpose_ui_layout::{Alignment, HorizontalAlignment, IntrinsicSize, VerticalAlignment};
70
71use std::cell::Cell;
72use std::hash::{Hash, Hasher};
73use std::rc::Rc;
74
75use crate::draw::DrawCommand;
76use crate::modifier::{
77    BlendMode, Color, ColorFilter, CompositingStrategy, EdgeInsets, GraphicsLayer, LayoutWeight,
78    Point, RoundedCornerShape,
79};
80
81fn hash_f32_value<H: Hasher>(state: &mut H, value: f32) {
82    state.write_u32(value.to_bits());
83}
84
85fn hash_option_f32<H: Hasher>(state: &mut H, value: Option<f32>) {
86    match value {
87        Some(v) => {
88            state.write_u8(1);
89            hash_f32_value(state, v);
90        }
91        None => state.write_u8(0),
92    }
93}
94
95fn hash_graphics_layer<H: Hasher>(state: &mut H, layer: &GraphicsLayer) {
96    hash_f32_value(state, layer.alpha);
97    hash_f32_value(state, layer.scale);
98    hash_f32_value(state, layer.scale_x);
99    hash_f32_value(state, layer.scale_y);
100    hash_f32_value(state, layer.rotation_x);
101    hash_f32_value(state, layer.rotation_y);
102    hash_f32_value(state, layer.rotation_z);
103    hash_f32_value(state, layer.camera_distance);
104    hash_f32_value(state, layer.transform_origin.pivot_fraction_x);
105    hash_f32_value(state, layer.transform_origin.pivot_fraction_y);
106    hash_f32_value(state, layer.translation_x);
107    hash_f32_value(state, layer.translation_y);
108    hash_f32_value(state, layer.shadow_elevation);
109    hash_f32_value(state, layer.ambient_shadow_color.r());
110    hash_f32_value(state, layer.ambient_shadow_color.g());
111    hash_f32_value(state, layer.ambient_shadow_color.b());
112    hash_f32_value(state, layer.ambient_shadow_color.a());
113    hash_f32_value(state, layer.spot_shadow_color.r());
114    hash_f32_value(state, layer.spot_shadow_color.g());
115    hash_f32_value(state, layer.spot_shadow_color.b());
116    hash_f32_value(state, layer.spot_shadow_color.a());
117    match layer.shape {
118        crate::modifier::LayerShape::Rectangle => {
119            state.write_u8(0);
120        }
121        crate::modifier::LayerShape::Rounded(shape) => {
122            state.write_u8(1);
123            let radii = shape.radii();
124            hash_f32_value(state, radii.top_left);
125            hash_f32_value(state, radii.top_right);
126            hash_f32_value(state, radii.bottom_right);
127            hash_f32_value(state, radii.bottom_left);
128        }
129    }
130    state.write_u8(layer.clip as u8);
131    match layer.color_filter {
132        Some(ColorFilter::Tint(color)) => {
133            state.write_u8(1);
134            hash_f32_value(state, color.r());
135            hash_f32_value(state, color.g());
136            hash_f32_value(state, color.b());
137            hash_f32_value(state, color.a());
138        }
139        Some(ColorFilter::Modulate(color)) => {
140            state.write_u8(2);
141            hash_f32_value(state, color.r());
142            hash_f32_value(state, color.g());
143            hash_f32_value(state, color.b());
144            hash_f32_value(state, color.a());
145        }
146        Some(ColorFilter::Matrix(matrix)) => {
147            state.write_u8(3);
148            for value in matrix {
149                hash_f32_value(state, value);
150            }
151        }
152        None => state.write_u8(0),
153    }
154    state.write_u8(layer.render_effect.is_some() as u8);
155    state.write_u8(layer.backdrop_effect.is_some() as u8);
156    let compositing_tag = match layer.compositing_strategy {
157        CompositingStrategy::Auto => 0,
158        CompositingStrategy::Offscreen => 1,
159        CompositingStrategy::ModulateAlpha => 2,
160    };
161    state.write_u8(compositing_tag);
162    let blend_tag = match layer.blend_mode {
163        BlendMode::Clear => 0,
164        BlendMode::Src => 1,
165        BlendMode::Dst => 2,
166        BlendMode::SrcOver => 3,
167        BlendMode::DstOver => 4,
168        BlendMode::SrcIn => 5,
169        BlendMode::DstIn => 6,
170        BlendMode::SrcOut => 7,
171        BlendMode::DstOut => 8,
172        BlendMode::SrcAtop => 9,
173        BlendMode::DstAtop => 10,
174        BlendMode::Xor => 11,
175        BlendMode::Plus => 12,
176        BlendMode::Modulate => 13,
177        BlendMode::Screen => 14,
178        BlendMode::Overlay => 15,
179        BlendMode::Darken => 16,
180        BlendMode::Lighten => 17,
181        BlendMode::ColorDodge => 18,
182        BlendMode::ColorBurn => 19,
183        BlendMode::HardLight => 20,
184        BlendMode::SoftLight => 21,
185        BlendMode::Difference => 22,
186        BlendMode::Exclusion => 23,
187        BlendMode::Multiply => 24,
188        BlendMode::Hue => 25,
189        BlendMode::Saturation => 26,
190        BlendMode::Color => 27,
191        BlendMode::Luminosity => 28,
192    };
193    state.write_u8(blend_tag);
194}
195
196fn hash_horizontal_alignment<H: Hasher>(state: &mut H, alignment: HorizontalAlignment) {
197    let tag = match alignment {
198        HorizontalAlignment::Start => 0,
199        HorizontalAlignment::CenterHorizontally => 1,
200        HorizontalAlignment::End => 2,
201    };
202    state.write_u8(tag);
203}
204
205fn hash_vertical_alignment<H: Hasher>(state: &mut H, alignment: VerticalAlignment) {
206    let tag = match alignment {
207        VerticalAlignment::Top => 0,
208        VerticalAlignment::CenterVertically => 1,
209        VerticalAlignment::Bottom => 2,
210    };
211    state.write_u8(tag);
212}
213
214fn hash_alignment<H: Hasher>(state: &mut H, alignment: Alignment) {
215    hash_horizontal_alignment(state, alignment.horizontal);
216    hash_vertical_alignment(state, alignment.vertical);
217}
218
219// ============================================================================
220// Padding Modifier Node
221// ============================================================================
222
223/// Node that adds padding around its content.
224#[derive(Debug)]
225pub struct PaddingNode {
226    padding: EdgeInsets,
227    state: NodeState,
228}
229
230impl PaddingNode {
231    pub fn new(padding: EdgeInsets) -> Self {
232        Self {
233            padding,
234            state: NodeState::new(),
235        }
236    }
237
238    pub fn padding(&self) -> EdgeInsets {
239        self.padding
240    }
241}
242
243impl DelegatableNode for PaddingNode {
244    fn node_state(&self) -> &NodeState {
245        &self.state
246    }
247}
248
249impl ModifierNode for PaddingNode {
250    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
251        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
252    }
253
254    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
255        Some(self)
256    }
257
258    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
259        Some(self)
260    }
261}
262
263impl LayoutModifierNode for PaddingNode {
264    fn measure(
265        &self,
266        _context: &mut dyn ModifierNodeContext,
267        measurable: &dyn Measurable,
268        constraints: Constraints,
269    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
270        // Convert padding to floating point values
271        let horizontal_padding = self.padding.horizontal_sum();
272        let vertical_padding = self.padding.vertical_sum();
273
274        // Subtract padding from available space
275        let inner_constraints = Constraints {
276            min_width: (constraints.min_width - horizontal_padding).max(0.0),
277            max_width: (constraints.max_width - horizontal_padding).max(0.0),
278            min_height: (constraints.min_height - vertical_padding).max(0.0),
279            max_height: (constraints.max_height - vertical_padding).max(0.0),
280        };
281
282        // Measure the wrapped content
283        let inner_placeable = measurable.measure(inner_constraints);
284        let inner_width = inner_placeable.width();
285        let inner_height = inner_placeable.height();
286
287        let (width, height) = constraints.constrain(
288            inner_width + horizontal_padding,
289            inner_height + vertical_padding,
290        );
291
292        // Return size with padding added, and placement offset to position child inside padding
293        cranpose_ui_layout::LayoutModifierMeasureResult::new(
294            Size { width, height },
295            self.padding.left, // Place child offset by left padding
296            self.padding.top,  // Place child offset by top padding
297        )
298    }
299
300    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
301        let vertical_padding = self.padding.vertical_sum();
302        let inner_height = (height - vertical_padding).max(0.0);
303        let inner_width = measurable.min_intrinsic_width(inner_height);
304        inner_width + self.padding.horizontal_sum()
305    }
306
307    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
308        let vertical_padding = self.padding.vertical_sum();
309        let inner_height = (height - vertical_padding).max(0.0);
310        let inner_width = measurable.max_intrinsic_width(inner_height);
311        inner_width + self.padding.horizontal_sum()
312    }
313
314    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
315        let horizontal_padding = self.padding.horizontal_sum();
316        let inner_width = (width - horizontal_padding).max(0.0);
317        let inner_height = measurable.min_intrinsic_height(inner_width);
318        inner_height + self.padding.vertical_sum()
319    }
320
321    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
322        let horizontal_padding = self.padding.horizontal_sum();
323        let inner_width = (width - horizontal_padding).max(0.0);
324        let inner_height = measurable.max_intrinsic_height(inner_width);
325        inner_height + self.padding.vertical_sum()
326    }
327}
328
329/// Element that creates and updates padding nodes.
330#[derive(Debug, Clone, PartialEq)]
331pub struct PaddingElement {
332    padding: EdgeInsets,
333}
334
335impl PaddingElement {
336    pub fn new(padding: EdgeInsets) -> Self {
337        Self { padding }
338    }
339}
340
341impl Hash for PaddingElement {
342    fn hash<H: Hasher>(&self, state: &mut H) {
343        hash_f32_value(state, self.padding.left);
344        hash_f32_value(state, self.padding.top);
345        hash_f32_value(state, self.padding.right);
346        hash_f32_value(state, self.padding.bottom);
347    }
348}
349
350impl ModifierNodeElement for PaddingElement {
351    type Node = PaddingNode;
352
353    fn create(&self) -> Self::Node {
354        PaddingNode::new(self.padding)
355    }
356
357    fn update(&self, node: &mut Self::Node) {
358        if node.padding != self.padding {
359            node.padding = self.padding;
360        }
361    }
362
363    fn capabilities(&self) -> NodeCapabilities {
364        NodeCapabilities::LAYOUT
365    }
366}
367
368// ============================================================================
369// Background Modifier Node
370// ============================================================================
371
372/// Node that draws a background behind its content.
373#[derive(Debug)]
374pub struct BackgroundNode {
375    color: Color,
376    shape: Option<RoundedCornerShape>,
377    state: NodeState,
378}
379
380impl BackgroundNode {
381    pub fn new(color: Color) -> Self {
382        Self {
383            color,
384            shape: None,
385            state: NodeState::new(),
386        }
387    }
388
389    pub fn color(&self) -> Color {
390        self.color
391    }
392
393    pub fn shape(&self) -> Option<RoundedCornerShape> {
394        self.shape
395    }
396}
397
398impl DelegatableNode for BackgroundNode {
399    fn node_state(&self) -> &NodeState {
400        &self.state
401    }
402}
403
404impl ModifierNode for BackgroundNode {
405    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
406        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
407    }
408
409    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
410        Some(self)
411    }
412
413    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
414        Some(self)
415    }
416}
417
418impl DrawModifierNode for BackgroundNode {
419    fn draw(&self, _draw_scope: &mut dyn DrawScope) {
420        // Scene building consumes the retained background node directly.
421    }
422}
423
424/// Element that creates and updates background nodes.
425#[derive(Debug, Clone, PartialEq)]
426pub struct BackgroundElement {
427    color: Color,
428}
429
430impl BackgroundElement {
431    pub fn new(color: Color) -> Self {
432        Self { color }
433    }
434}
435
436impl Hash for BackgroundElement {
437    fn hash<H: Hasher>(&self, state: &mut H) {
438        hash_f32_value(state, self.color.0);
439        hash_f32_value(state, self.color.1);
440        hash_f32_value(state, self.color.2);
441        hash_f32_value(state, self.color.3);
442    }
443}
444
445impl ModifierNodeElement for BackgroundElement {
446    type Node = BackgroundNode;
447
448    fn create(&self) -> Self::Node {
449        BackgroundNode::new(self.color)
450    }
451
452    fn update(&self, node: &mut Self::Node) {
453        if node.color != self.color {
454            node.color = self.color;
455        }
456    }
457
458    fn capabilities(&self) -> NodeCapabilities {
459        NodeCapabilities::DRAW
460    }
461}
462
463// ============================================================================
464// Size Modifier Node
465// ============================================================================
466
467/// Node that tracks the latest rounded corner shape.
468#[derive(Debug)]
469pub struct CornerShapeNode {
470    shape: RoundedCornerShape,
471    state: NodeState,
472}
473
474impl CornerShapeNode {
475    pub fn new(shape: RoundedCornerShape) -> Self {
476        Self {
477            shape,
478            state: NodeState::new(),
479        }
480    }
481
482    pub fn shape(&self) -> RoundedCornerShape {
483        self.shape
484    }
485}
486
487impl DelegatableNode for CornerShapeNode {
488    fn node_state(&self) -> &NodeState {
489        &self.state
490    }
491}
492
493impl ModifierNode for CornerShapeNode {
494    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
495        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
496    }
497
498    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
499        Some(self)
500    }
501
502    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
503        Some(self)
504    }
505}
506
507impl DrawModifierNode for CornerShapeNode {
508    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
509}
510
511/// Element that creates and updates corner shape nodes.
512#[derive(Debug, Clone, PartialEq)]
513pub struct CornerShapeElement {
514    shape: RoundedCornerShape,
515}
516
517impl CornerShapeElement {
518    pub fn new(shape: RoundedCornerShape) -> Self {
519        Self { shape }
520    }
521}
522
523impl Hash for CornerShapeElement {
524    fn hash<H: Hasher>(&self, state: &mut H) {
525        let radii = self.shape.radii();
526        hash_f32_value(state, radii.top_left);
527        hash_f32_value(state, radii.top_right);
528        hash_f32_value(state, radii.bottom_right);
529        hash_f32_value(state, radii.bottom_left);
530    }
531}
532
533impl ModifierNodeElement for CornerShapeElement {
534    type Node = CornerShapeNode;
535
536    fn create(&self) -> Self::Node {
537        CornerShapeNode::new(self.shape)
538    }
539
540    fn update(&self, node: &mut Self::Node) {
541        if node.shape != self.shape {
542            node.shape = self.shape;
543        }
544    }
545
546    fn capabilities(&self) -> NodeCapabilities {
547        NodeCapabilities::DRAW
548    }
549}
550
551// ============================================================================
552// GraphicsLayer Modifier Node
553// ============================================================================
554
555/// Node that stores graphics layer state for resolved modifiers.
556pub struct GraphicsLayerNode {
557    layer: GraphicsLayer,
558    layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
559    node_id: Rc<Cell<Option<NodeId>>>,
560    state: NodeState,
561}
562
563impl GraphicsLayerNode {
564    pub fn new(layer: GraphicsLayer) -> Self {
565        Self {
566            layer,
567            layer_resolver: None,
568            node_id: Rc::new(Cell::new(None)),
569            state: NodeState::new(),
570        }
571    }
572
573    pub fn new_lazy(layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) -> Self {
574        Self {
575            layer: GraphicsLayer::default(),
576            layer_resolver: Some(layer_resolver),
577            node_id: Rc::new(Cell::new(None)),
578            state: NodeState::new(),
579        }
580    }
581
582    #[cfg(test)]
583    pub fn layer(&self) -> GraphicsLayer {
584        if let Some(resolve) = self.layer_resolver() {
585            resolve()
586        } else {
587            self.layer.clone()
588        }
589    }
590
591    pub fn layer_snapshot(&self) -> GraphicsLayer {
592        self.layer.clone()
593    }
594
595    pub fn layer_resolver(&self) -> Option<Rc<dyn Fn() -> GraphicsLayer>> {
596        self.layer_resolver.as_ref().map(|resolve| {
597            let resolve = resolve.clone();
598            let node_id = Rc::clone(&self.node_id);
599            Rc::new(move || {
600                if let Some(node_id) = node_id.get() {
601                    let scope = crate::render_state::DrawObservationScope::new(node_id, usize::MAX);
602                    crate::render_state::observe_draw_reads(scope, || resolve())
603                } else {
604                    resolve()
605                }
606            }) as Rc<dyn Fn() -> GraphicsLayer>
607        })
608    }
609
610    fn set_static(&mut self, layer: GraphicsLayer) {
611        let changed = self.layer != layer || self.layer_resolver.is_some();
612        self.layer = layer;
613        self.layer_resolver = None;
614        if changed {
615            if let Some(node_id) = self.node_id.get() {
616                crate::render_state::schedule_draw_repass(node_id);
617            }
618        }
619    }
620
621    fn set_lazy(&mut self, layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) {
622        let changed = self
623            .layer_resolver
624            .as_ref()
625            .is_none_or(|current| !Rc::ptr_eq(current, &layer_resolver));
626        self.layer_resolver = Some(layer_resolver);
627        if changed {
628            if let Some(node_id) = self.node_id.get() {
629                crate::render_state::schedule_draw_repass(node_id);
630            }
631        }
632    }
633}
634
635impl DelegatableNode for GraphicsLayerNode {
636    fn node_state(&self) -> &NodeState {
637        &self.state
638    }
639}
640
641impl ModifierNode for GraphicsLayerNode {
642    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
643        self.node_id.set(context.node_id());
644        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
645    }
646
647    fn on_detach(&mut self) {
648        if let Some(node_id) = self.node_id.replace(None) {
649            crate::render_state::clear_draw_observations_for_node(node_id);
650        }
651    }
652}
653
654impl std::fmt::Debug for GraphicsLayerNode {
655    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656        f.debug_struct("GraphicsLayerNode")
657            .field("layer", &self.layer)
658            .field("lazy", &self.layer_resolver.is_some())
659            .finish()
660    }
661}
662
663/// Element that creates and updates graphics layer nodes.
664#[derive(Debug, Clone, PartialEq)]
665pub struct GraphicsLayerElement {
666    layer: GraphicsLayer,
667}
668
669impl GraphicsLayerElement {
670    pub fn new(layer: GraphicsLayer) -> Self {
671        Self { layer }
672    }
673}
674
675impl Hash for GraphicsLayerElement {
676    fn hash<H: Hasher>(&self, state: &mut H) {
677        hash_graphics_layer(state, &self.layer);
678    }
679}
680
681impl ModifierNodeElement for GraphicsLayerElement {
682    type Node = GraphicsLayerNode;
683
684    fn create(&self) -> Self::Node {
685        GraphicsLayerNode::new(self.layer.clone())
686    }
687
688    fn update(&self, node: &mut Self::Node) {
689        node.set_static(self.layer.clone());
690    }
691
692    fn capabilities(&self) -> NodeCapabilities {
693        NodeCapabilities::DRAW
694    }
695}
696
697/// Element that evaluates a graphics layer lazily during render data collection.
698#[derive(Clone)]
699pub struct LazyGraphicsLayerElement {
700    layer_resolver: Rc<dyn Fn() -> GraphicsLayer>,
701}
702
703impl LazyGraphicsLayerElement {
704    pub fn new(layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) -> Self {
705        Self { layer_resolver }
706    }
707}
708
709impl std::fmt::Debug for LazyGraphicsLayerElement {
710    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
711        f.debug_struct("LazyGraphicsLayerElement")
712            .field("resolver", &"<closure>")
713            .finish()
714    }
715}
716
717impl PartialEq for LazyGraphicsLayerElement {
718    fn eq(&self, other: &Self) -> bool {
719        Rc::ptr_eq(&self.layer_resolver, &other.layer_resolver)
720    }
721}
722
723impl Eq for LazyGraphicsLayerElement {}
724
725impl Hash for LazyGraphicsLayerElement {
726    fn hash<H: Hasher>(&self, state: &mut H) {
727        let ptr = Rc::as_ptr(&self.layer_resolver) as *const ();
728        ptr.hash(state);
729    }
730}
731
732impl ModifierNodeElement for LazyGraphicsLayerElement {
733    type Node = GraphicsLayerNode;
734
735    fn create(&self) -> Self::Node {
736        GraphicsLayerNode::new_lazy(self.layer_resolver.clone())
737    }
738
739    fn update(&self, node: &mut Self::Node) {
740        node.set_lazy(self.layer_resolver.clone());
741    }
742
743    fn capabilities(&self) -> NodeCapabilities {
744        NodeCapabilities::DRAW
745    }
746
747    fn always_update(&self) -> bool {
748        true
749    }
750
751    fn auto_invalidate_on_update(&self) -> bool {
752        false
753    }
754}
755
756// ============================================================================
757// Size Modifier Node
758// ============================================================================
759
760/// Node that enforces size constraints on its content.
761///
762/// Matches Kotlin: `SizeNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
763#[derive(Debug)]
764pub struct SizeNode {
765    min_width: Option<f32>,
766    max_width: Option<f32>,
767    min_height: Option<f32>,
768    max_height: Option<f32>,
769    enforce_incoming: bool,
770    state: NodeState,
771}
772
773impl SizeNode {
774    pub fn new(
775        min_width: Option<f32>,
776        max_width: Option<f32>,
777        min_height: Option<f32>,
778        max_height: Option<f32>,
779        enforce_incoming: bool,
780    ) -> Self {
781        Self {
782            min_width,
783            max_width,
784            min_height,
785            max_height,
786            enforce_incoming,
787            state: NodeState::new(),
788        }
789    }
790
791    /// Helper to build target constraints from element parameters
792    fn target_constraints(&self) -> Constraints {
793        let max_width = self.max_width.map(|v| v.max(0.0)).unwrap_or(f32::INFINITY);
794        let max_height = self.max_height.map(|v| v.max(0.0)).unwrap_or(f32::INFINITY);
795
796        let min_width = self
797            .min_width
798            .map(|v| {
799                let clamped = v.clamp(0.0, max_width);
800                if clamped == f32::INFINITY {
801                    0.0
802                } else {
803                    clamped
804                }
805            })
806            .unwrap_or(0.0);
807
808        let min_height = self
809            .min_height
810            .map(|v| {
811                let clamped = v.clamp(0.0, max_height);
812                if clamped == f32::INFINITY {
813                    0.0
814                } else {
815                    clamped
816                }
817            })
818            .unwrap_or(0.0);
819
820        Constraints {
821            min_width,
822            max_width,
823            min_height,
824            max_height,
825        }
826    }
827
828    pub fn min_width(&self) -> Option<f32> {
829        self.min_width
830    }
831
832    pub fn max_width(&self) -> Option<f32> {
833        self.max_width
834    }
835
836    pub fn min_height(&self) -> Option<f32> {
837        self.min_height
838    }
839
840    pub fn max_height(&self) -> Option<f32> {
841        self.max_height
842    }
843
844    pub fn enforce_incoming(&self) -> bool {
845        self.enforce_incoming
846    }
847}
848
849impl DelegatableNode for SizeNode {
850    fn node_state(&self) -> &NodeState {
851        &self.state
852    }
853}
854
855impl ModifierNode for SizeNode {
856    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
857        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
858    }
859
860    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
861        Some(self)
862    }
863
864    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
865        Some(self)
866    }
867}
868
869impl LayoutModifierNode for SizeNode {
870    fn measure(
871        &self,
872        _context: &mut dyn ModifierNodeContext,
873        measurable: &dyn Measurable,
874        constraints: Constraints,
875    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
876        let target = self.target_constraints();
877
878        let wrapped_constraints = if self.enforce_incoming {
879            // Constrain target constraints by incoming constraints
880            Constraints {
881                min_width: target
882                    .min_width
883                    .max(constraints.min_width)
884                    .min(constraints.max_width),
885                max_width: target
886                    .max_width
887                    .min(constraints.max_width)
888                    .max(constraints.min_width),
889                min_height: target
890                    .min_height
891                    .max(constraints.min_height)
892                    .min(constraints.max_height),
893                max_height: target
894                    .max_height
895                    .min(constraints.max_height)
896                    .max(constraints.min_height),
897            }
898        } else {
899            // Required size: use target, but preserve incoming if target is unspecified
900            let resolved_min_width = if self.min_width.is_some() {
901                target.min_width
902            } else {
903                constraints.min_width.min(target.max_width)
904            };
905            let resolved_max_width = if self.max_width.is_some() {
906                target.max_width
907            } else {
908                constraints.max_width.max(target.min_width)
909            };
910            let resolved_min_height = if self.min_height.is_some() {
911                target.min_height
912            } else {
913                constraints.min_height.min(target.max_height)
914            };
915            let resolved_max_height = if self.max_height.is_some() {
916                target.max_height
917            } else {
918                constraints.max_height.max(target.min_height)
919            };
920
921            Constraints {
922                min_width: resolved_min_width,
923                max_width: resolved_max_width,
924                min_height: resolved_min_height,
925                max_height: resolved_max_height,
926            }
927        };
928
929        let placeable = measurable.measure(wrapped_constraints);
930        let measured_width = placeable.width();
931        let measured_height = placeable.height();
932
933        // Return the target size when both min==max (fixed size), but only if it satisfies
934        // the wrapped constraints we passed down. Otherwise return measured size.
935        // This handles the case where enforce_incoming=true and incoming constraints are tighter.
936        //
937        // With enforce_incoming=false (`required_size`) the node measures AND
938        // reports the required size: draw rects and effect bounds derive from
939        // the node box, so oversized overlay content (selection magnifier
940        // line, interaction lenses) must keep its full box. Hosts that must
941        // not grow pin their own size (a fixed-size ancestor wins over a
942        // wrap-content one).
943        let result_width = if self.min_width.is_some()
944            && self.max_width.is_some()
945            && self.min_width == self.max_width
946            && target.min_width >= wrapped_constraints.min_width
947            && target.min_width <= wrapped_constraints.max_width
948        {
949            target.min_width
950        } else {
951            measured_width
952        };
953
954        let result_height = if self.min_height.is_some()
955            && self.max_height.is_some()
956            && self.min_height == self.max_height
957            && target.min_height >= wrapped_constraints.min_height
958            && target.min_height <= wrapped_constraints.max_height
959        {
960            target.min_height
961        } else {
962            measured_height
963        };
964
965        // SizeNode doesn't offset placement - child is placed at (0, 0) relative to this node
966        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
967            width: result_width,
968            height: result_height,
969        })
970    }
971
972    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
973        let target = self.target_constraints();
974        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
975            target.max_width
976        } else {
977            let child_height = if self.enforce_incoming {
978                height
979            } else {
980                height.clamp(target.min_height, target.max_height)
981            };
982            measurable
983                .min_intrinsic_width(child_height)
984                .clamp(target.min_width, target.max_width)
985        }
986    }
987
988    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
989        let target = self.target_constraints();
990        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
991            target.max_width
992        } else {
993            let child_height = if self.enforce_incoming {
994                height
995            } else {
996                height.clamp(target.min_height, target.max_height)
997            };
998            measurable
999                .max_intrinsic_width(child_height)
1000                .clamp(target.min_width, target.max_width)
1001        }
1002    }
1003
1004    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1005        let target = self.target_constraints();
1006        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1007            target.max_height
1008        } else {
1009            let child_width = if self.enforce_incoming {
1010                width
1011            } else {
1012                width.clamp(target.min_width, target.max_width)
1013            };
1014            measurable
1015                .min_intrinsic_height(child_width)
1016                .clamp(target.min_height, target.max_height)
1017        }
1018    }
1019
1020    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1021        let target = self.target_constraints();
1022        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1023            target.max_height
1024        } else {
1025            let child_width = if self.enforce_incoming {
1026                width
1027            } else {
1028                width.clamp(target.min_width, target.max_width)
1029            };
1030            measurable
1031                .max_intrinsic_height(child_width)
1032                .clamp(target.min_height, target.max_height)
1033        }
1034    }
1035}
1036
1037/// Element that creates and updates size nodes.
1038///
1039/// Matches Kotlin: `SizeElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1040#[derive(Debug, Clone, PartialEq)]
1041pub struct SizeElement {
1042    min_width: Option<f32>,
1043    max_width: Option<f32>,
1044    min_height: Option<f32>,
1045    max_height: Option<f32>,
1046    enforce_incoming: bool,
1047}
1048
1049impl SizeElement {
1050    pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
1051        Self {
1052            min_width: width,
1053            max_width: width,
1054            min_height: height,
1055            max_height: height,
1056            enforce_incoming: true,
1057        }
1058    }
1059
1060    pub fn with_constraints(
1061        min_width: Option<f32>,
1062        max_width: Option<f32>,
1063        min_height: Option<f32>,
1064        max_height: Option<f32>,
1065        enforce_incoming: bool,
1066    ) -> Self {
1067        Self {
1068            min_width,
1069            max_width,
1070            min_height,
1071            max_height,
1072            enforce_incoming,
1073        }
1074    }
1075}
1076
1077impl Hash for SizeElement {
1078    fn hash<H: Hasher>(&self, state: &mut H) {
1079        hash_option_f32(state, self.min_width);
1080        hash_option_f32(state, self.max_width);
1081        hash_option_f32(state, self.min_height);
1082        hash_option_f32(state, self.max_height);
1083        self.enforce_incoming.hash(state);
1084    }
1085}
1086
1087impl ModifierNodeElement for SizeElement {
1088    type Node = SizeNode;
1089
1090    fn create(&self) -> Self::Node {
1091        SizeNode::new(
1092            self.min_width,
1093            self.max_width,
1094            self.min_height,
1095            self.max_height,
1096            self.enforce_incoming,
1097        )
1098    }
1099
1100    fn update(&self, node: &mut Self::Node) {
1101        if node.min_width != self.min_width
1102            || node.max_width != self.max_width
1103            || node.min_height != self.min_height
1104            || node.max_height != self.max_height
1105            || node.enforce_incoming != self.enforce_incoming
1106        {
1107            node.min_width = self.min_width;
1108            node.max_width = self.max_width;
1109            node.min_height = self.min_height;
1110            node.max_height = self.max_height;
1111            node.enforce_incoming = self.enforce_incoming;
1112        }
1113    }
1114
1115    fn capabilities(&self) -> NodeCapabilities {
1116        NodeCapabilities::LAYOUT
1117    }
1118
1119    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
1120        Some(InvalidationKind::Layout)
1121    }
1122}
1123
1124// ============================================================================
1125// Clickable Modifier Node
1126// ============================================================================
1127
1128/// Node that handles click/tap interactions.
1129// Drag threshold is now shared via cranpose_foundation::DRAG_THRESHOLD
1130use cranpose_foundation::DRAG_THRESHOLD;
1131
1132use std::cell::RefCell;
1133
1134// Press position is stored per-node via Rc<RefCell> for sharing with handler closure
1135// Node reuse is ensured by ClickableElement implementing key() to return a stable key
1136// The handler closure is cached to ensure the same closure (and press_position state) is returned
1137
1138pub struct ClickableNode {
1139    on_press: Option<Rc<dyn Fn(Point)>>,
1140    on_click: Rc<dyn Fn(Point)>,
1141    state: NodeState,
1142    /// Shared press position for drag detection (per-node state, accessible by handler closure)
1143    press_position: Rc<RefCell<Option<Point>>>,
1144    /// Cached handler closure - created once, returned on every pointer_input_handler() call
1145    cached_handler: Rc<dyn Fn(PointerEvent)>,
1146}
1147
1148impl std::fmt::Debug for ClickableNode {
1149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1150        f.debug_struct("ClickableNode").finish()
1151    }
1152}
1153
1154impl ClickableNode {
1155    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1156        Self::with_handler(Rc::new(on_click))
1157    }
1158
1159    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1160        Self::with_handlers(None, on_click)
1161    }
1162
1163    pub fn with_handlers(on_press: Option<Rc<dyn Fn(Point)>>, on_click: Rc<dyn Fn(Point)>) -> Self {
1164        let press_position = Rc::new(RefCell::new(None));
1165        let cached_handler =
1166            Self::create_handler(on_press.clone(), on_click.clone(), press_position.clone());
1167        Self {
1168            on_press,
1169            on_click,
1170            state: NodeState::new(),
1171            press_position,
1172            cached_handler,
1173        }
1174    }
1175
1176    fn create_handler(
1177        on_press: Option<Rc<dyn Fn(Point)>>,
1178        on_click: Rc<dyn Fn(Point)>,
1179        press_position: Rc<RefCell<Option<Point>>>,
1180    ) -> Rc<dyn Fn(PointerEvent)> {
1181        Rc::new(move |event: PointerEvent| {
1182            // Clicks track the primary pointer only; secondary pointers of a
1183            // multi-touch gesture (e.g. a pinch) must never fire clicks.
1184            if event.id != 0 {
1185                return;
1186            }
1187
1188            // Check if event was consumed by scroll or other gesture handlers
1189            if event.is_consumed() {
1190                // Clear press state if event was consumed
1191                *press_position.borrow_mut() = None;
1192                return;
1193            }
1194
1195            match event.kind {
1196                PointerEventKind::Down => {
1197                    // Store global press position for drag detection on Up
1198                    *press_position.borrow_mut() = Some(Point {
1199                        x: event.global_position.x,
1200                        y: event.global_position.y,
1201                    });
1202                    if let Some(on_press) = on_press.as_ref() {
1203                        on_press(event.position);
1204                    }
1205                }
1206                PointerEventKind::Move => {
1207                    // Move events are tracked via press_position for drag detection
1208                }
1209                PointerEventKind::Up => {
1210                    // Check if this is a click (Up near Down) or a drag (Up far from Down)
1211                    let press_pos_value = *press_position.borrow();
1212
1213                    let should_click = if let Some(press_pos) = press_pos_value {
1214                        let dx = event.global_position.x - press_pos.x;
1215                        let dy = event.global_position.y - press_pos.y;
1216                        let distance = (dx * dx + dy * dy).sqrt();
1217                        distance <= DRAG_THRESHOLD
1218                    } else {
1219                        // No Down was tracked - fire click anyway
1220                        // This preserves the original behavior for cases where Down
1221                        // was handled by a different mechanism
1222                        true
1223                    };
1224
1225                    // Reset press position
1226                    *press_position.borrow_mut() = None;
1227
1228                    if should_click {
1229                        on_click(Point {
1230                            x: event.position.x,
1231                            y: event.position.y,
1232                        });
1233                        event.consume();
1234                    }
1235                }
1236                PointerEventKind::Cancel => {
1237                    // Clear press state on cancel
1238                    *press_position.borrow_mut() = None;
1239                }
1240                PointerEventKind::Scroll
1241                | PointerEventKind::Zoom
1242                | PointerEventKind::RotaryScrollPre
1243                | PointerEventKind::RotaryScroll
1244                | PointerEventKind::Enter
1245                | PointerEventKind::Exit => {
1246                    // These events don't affect click press state.
1247                }
1248            }
1249        })
1250    }
1251
1252    pub fn handler(&self) -> Rc<dyn Fn(Point)> {
1253        self.on_click.clone()
1254    }
1255}
1256
1257impl DelegatableNode for ClickableNode {
1258    fn node_state(&self) -> &NodeState {
1259        &self.state
1260    }
1261}
1262
1263impl ModifierNode for ClickableNode {
1264    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1265        context.invalidate(cranpose_foundation::InvalidationKind::PointerInput);
1266    }
1267
1268    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1269        Some(self)
1270    }
1271
1272    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1273        Some(self)
1274    }
1275}
1276
1277impl PointerInputNode for ClickableNode {
1278    fn on_pointer_event(
1279        &mut self,
1280        _context: &mut dyn ModifierNodeContext,
1281        event: &PointerEvent,
1282    ) -> bool {
1283        // Delegate to the cached handler - single source of truth for click logic
1284        // This avoids duplicating the press position tracking and threshold checking
1285        (self.cached_handler)(event.clone());
1286        event.is_consumed()
1287    }
1288
1289    fn hit_test(&self, _x: f32, _y: f32) -> bool {
1290        // Always participate in hit testing
1291        true
1292    }
1293
1294    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1295        // Return the cached handler - this ensures the same closure (with its press_position state)
1296        // is used across multiple calls to pointer_input_handler()
1297        Some(self.cached_handler.clone())
1298    }
1299}
1300
1301/// Element that creates and updates clickable nodes.
1302#[derive(Clone)]
1303pub struct ClickableElement {
1304    on_press: Option<Rc<dyn Fn(Point)>>,
1305    on_click: Rc<dyn Fn(Point)>,
1306}
1307
1308impl ClickableElement {
1309    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1310        Self {
1311            on_press: None,
1312            on_click: Rc::new(on_click),
1313        }
1314    }
1315
1316    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1317        Self {
1318            on_press: None,
1319            on_click,
1320        }
1321    }
1322
1323    pub fn with_handlers(on_press: Rc<dyn Fn(Point)>, on_click: Rc<dyn Fn(Point)>) -> Self {
1324        Self {
1325            on_press: Some(on_press),
1326            on_click,
1327        }
1328    }
1329}
1330
1331impl std::fmt::Debug for ClickableElement {
1332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1333        f.debug_struct("ClickableElement").finish()
1334    }
1335}
1336
1337impl PartialEq for ClickableElement {
1338    fn eq(&self, _other: &Self) -> bool {
1339        // Type matching is sufficient - node will be updated via update() method
1340        // This matches JC behavior where nodes are reused for same-type elements,
1341        // preserving press_position state for proper drag detection
1342        true
1343    }
1344}
1345
1346impl Eq for ClickableElement {}
1347
1348impl Hash for ClickableElement {
1349    fn hash<H: Hasher>(&self, state: &mut H) {
1350        // Consistent hash for type-based matching
1351        "clickable".hash(state);
1352    }
1353}
1354
1355impl ModifierNodeElement for ClickableElement {
1356    type Node = ClickableNode;
1357
1358    fn create(&self) -> Self::Node {
1359        ClickableNode::with_handlers(self.on_press.clone(), self.on_click.clone())
1360    }
1361
1362    // Note: key() is deliberately NOT implemented (returns None by default)
1363    // This enables type-based node reuse: the same ClickableNode instance is
1364    // reused across recompositions, preserving the cached_handler and its
1365    // captured press_position state for proper drag detection.
1366
1367    fn update(&self, node: &mut Self::Node) {
1368        // Update the handler - the cached_handler needs to be recreated
1369        // with the new on_click while preserving press_position
1370        node.on_press = self.on_press.clone();
1371        node.on_click = self.on_click.clone();
1372        // Recreate the cached handler with the same press_position but new click handler
1373        node.cached_handler = ClickableNode::create_handler(
1374            node.on_press.clone(),
1375            node.on_click.clone(),
1376            node.press_position.clone(),
1377        );
1378    }
1379
1380    fn capabilities(&self) -> NodeCapabilities {
1381        NodeCapabilities::POINTER_INPUT
1382    }
1383
1384    fn always_update(&self) -> bool {
1385        // Always update to capture new closure while preserving node state
1386        true
1387    }
1388}
1389
1390// ============================================================================
1391// Alpha Modifier Node
1392// ============================================================================
1393
1394/// Node that applies alpha transparency to its content.
1395#[derive(Debug)]
1396pub struct AlphaNode {
1397    alpha: f32,
1398    state: NodeState,
1399}
1400
1401impl AlphaNode {
1402    pub fn new(alpha: f32) -> Self {
1403        Self {
1404            alpha: alpha.clamp(0.0, 1.0),
1405            state: NodeState::new(),
1406        }
1407    }
1408}
1409
1410impl DelegatableNode for AlphaNode {
1411    fn node_state(&self) -> &NodeState {
1412        &self.state
1413    }
1414}
1415
1416impl ModifierNode for AlphaNode {
1417    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1418        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1419    }
1420
1421    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1422        Some(self)
1423    }
1424
1425    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1426        Some(self)
1427    }
1428}
1429
1430impl DrawModifierNode for AlphaNode {
1431    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1432}
1433
1434/// Element that creates and updates alpha nodes.
1435#[derive(Debug, Clone, PartialEq)]
1436pub struct AlphaElement {
1437    alpha: f32,
1438}
1439
1440impl AlphaElement {
1441    pub fn new(alpha: f32) -> Self {
1442        Self {
1443            alpha: alpha.clamp(0.0, 1.0),
1444        }
1445    }
1446}
1447
1448impl Hash for AlphaElement {
1449    fn hash<H: Hasher>(&self, state: &mut H) {
1450        hash_f32_value(state, self.alpha);
1451    }
1452}
1453
1454impl ModifierNodeElement for AlphaElement {
1455    type Node = AlphaNode;
1456
1457    fn create(&self) -> Self::Node {
1458        AlphaNode::new(self.alpha)
1459    }
1460
1461    fn update(&self, node: &mut Self::Node) {
1462        let new_alpha = self.alpha.clamp(0.0, 1.0);
1463        if (node.alpha - new_alpha).abs() > f32::EPSILON {
1464            node.alpha = new_alpha;
1465        }
1466    }
1467
1468    fn capabilities(&self) -> NodeCapabilities {
1469        NodeCapabilities::DRAW
1470    }
1471}
1472
1473// ============================================================================
1474// Clip-To-Bounds Modifier Node
1475// ============================================================================
1476
1477/// Node that marks the subtree for clipping during rendering.
1478#[derive(Debug)]
1479pub struct ClipToBoundsNode {
1480    state: NodeState,
1481}
1482
1483impl ClipToBoundsNode {
1484    pub fn new() -> Self {
1485        Self {
1486            state: NodeState::new(),
1487        }
1488    }
1489}
1490
1491impl DelegatableNode for ClipToBoundsNode {
1492    fn node_state(&self) -> &NodeState {
1493        &self.state
1494    }
1495}
1496
1497impl ModifierNode for ClipToBoundsNode {
1498    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1499        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1500    }
1501
1502    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1503        Some(self)
1504    }
1505
1506    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1507        Some(self)
1508    }
1509}
1510
1511impl DrawModifierNode for ClipToBoundsNode {
1512    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1513}
1514
1515/// Element that creates clip-to-bounds nodes.
1516#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1517pub struct ClipToBoundsElement;
1518
1519impl ClipToBoundsElement {
1520    pub fn new() -> Self {
1521        Self
1522    }
1523}
1524
1525impl ModifierNodeElement for ClipToBoundsElement {
1526    type Node = ClipToBoundsNode;
1527
1528    fn create(&self) -> Self::Node {
1529        ClipToBoundsNode::new()
1530    }
1531
1532    fn update(&self, _node: &mut Self::Node) {}
1533
1534    fn capabilities(&self) -> NodeCapabilities {
1535        NodeCapabilities::DRAW
1536    }
1537}
1538
1539// ============================================================================
1540// Window Rect Reporter Modifier Node
1541// ============================================================================
1542
1543/// Node that publishes its layout node's composited window rect into a shared
1544/// cell. The layout `place` pass writes the node's true on-screen rect (window
1545/// coordinates, resolved through ancestor scroll placement + graphics-layer
1546/// translation) here every pass. Scroll containers use it to expose their
1547/// viewport bounds to a `BringIntoViewResponder`. Draws nothing.
1548pub struct WindowRectReporterNode {
1549    sink: Rc<Cell<cranpose_ui_graphics::Rect>>,
1550    state: NodeState,
1551}
1552
1553impl WindowRectReporterNode {
1554    pub fn new(sink: Rc<Cell<cranpose_ui_graphics::Rect>>) -> Self {
1555        Self {
1556            sink,
1557            state: NodeState::new(),
1558        }
1559    }
1560
1561    /// The cell the layout pass writes this node's window rect into.
1562    pub(crate) fn window_rect_sink(&self) -> Rc<Cell<cranpose_ui_graphics::Rect>> {
1563        self.sink.clone()
1564    }
1565}
1566
1567impl DelegatableNode for WindowRectReporterNode {
1568    fn node_state(&self) -> &NodeState {
1569        &self.state
1570    }
1571}
1572
1573impl ModifierNode for WindowRectReporterNode {
1574    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1575        Some(self)
1576    }
1577
1578    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1579        Some(self)
1580    }
1581}
1582
1583impl LayoutModifierNode for WindowRectReporterNode {
1584    /// Transparent pass-through: measure the wrapped content with the same
1585    /// constraints and place it at the origin. The reporter only exists so the
1586    /// layout `place` pass can publish this node's window rect into its sink.
1587    fn measure(
1588        &self,
1589        _context: &mut dyn ModifierNodeContext,
1590        measurable: &dyn Measurable,
1591        constraints: Constraints,
1592    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1593        let placeable = measurable.measure(constraints);
1594        cranpose_ui_layout::LayoutModifierMeasureResult::new(
1595            Size {
1596                width: placeable.width(),
1597                height: placeable.height(),
1598            },
1599            0.0,
1600            0.0,
1601        )
1602    }
1603}
1604
1605/// Element that creates [`WindowRectReporterNode`] instances. Reuses the node
1606/// across recompositions, swapping the sink cell when it changes.
1607#[derive(Clone)]
1608pub struct WindowRectReporterElement {
1609    sink: Rc<Cell<cranpose_ui_graphics::Rect>>,
1610}
1611
1612impl WindowRectReporterElement {
1613    pub fn new(sink: Rc<Cell<cranpose_ui_graphics::Rect>>) -> Self {
1614        Self { sink }
1615    }
1616}
1617
1618impl std::fmt::Debug for WindowRectReporterElement {
1619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1620        f.debug_struct("WindowRectReporterElement").finish()
1621    }
1622}
1623
1624impl PartialEq for WindowRectReporterElement {
1625    fn eq(&self, other: &Self) -> bool {
1626        Rc::ptr_eq(&self.sink, &other.sink)
1627    }
1628}
1629
1630impl Eq for WindowRectReporterElement {}
1631
1632impl Hash for WindowRectReporterElement {
1633    fn hash<H: Hasher>(&self, state: &mut H) {
1634        std::ptr::hash(Rc::as_ptr(&self.sink), state);
1635    }
1636}
1637
1638impl ModifierNodeElement for WindowRectReporterElement {
1639    type Node = WindowRectReporterNode;
1640
1641    fn create(&self) -> Self::Node {
1642        WindowRectReporterNode::new(self.sink.clone())
1643    }
1644
1645    fn update(&self, node: &mut Self::Node) {
1646        node.sink = self.sink.clone();
1647    }
1648
1649    fn capabilities(&self) -> NodeCapabilities {
1650        NodeCapabilities::LAYOUT
1651    }
1652}
1653
1654// ============================================================================
1655// Size Reporter Modifier Node
1656// ============================================================================
1657
1658/// Node that publishes its measured size (logical px) into a shared cell on
1659/// every measure pass — the Compose `onSizeChanged` seam for consumers that
1660/// need their node's resolved size outside layout (e.g. shader morph
1661/// geometry expressed in node-local pixels). Transparent for layout, draws
1662/// nothing.
1663pub struct SizeReporterNode {
1664    sink: Rc<Cell<Size>>,
1665    state: NodeState,
1666}
1667
1668impl SizeReporterNode {
1669    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1670        Self {
1671            sink,
1672            state: NodeState::new(),
1673        }
1674    }
1675}
1676
1677impl DelegatableNode for SizeReporterNode {
1678    fn node_state(&self) -> &NodeState {
1679        &self.state
1680    }
1681}
1682
1683impl ModifierNode for SizeReporterNode {
1684    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1685        Some(self)
1686    }
1687
1688    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1689        Some(self)
1690    }
1691}
1692
1693impl LayoutModifierNode for SizeReporterNode {
1694    fn measure(
1695        &self,
1696        _context: &mut dyn ModifierNodeContext,
1697        measurable: &dyn Measurable,
1698        constraints: Constraints,
1699    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1700        let placeable = measurable.measure(constraints);
1701        let size = Size {
1702            width: placeable.width(),
1703            height: placeable.height(),
1704        };
1705        self.sink.set(size);
1706        cranpose_ui_layout::LayoutModifierMeasureResult::new(size, 0.0, 0.0)
1707    }
1708}
1709
1710/// Element for [`SizeReporterNode`]; reuses the node, swapping the sink.
1711#[derive(Clone)]
1712pub struct SizeReporterElement {
1713    sink: Rc<Cell<Size>>,
1714}
1715
1716impl SizeReporterElement {
1717    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1718        Self { sink }
1719    }
1720}
1721
1722impl std::fmt::Debug for SizeReporterElement {
1723    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1724        f.debug_struct("SizeReporterElement").finish()
1725    }
1726}
1727
1728impl PartialEq for SizeReporterElement {
1729    fn eq(&self, other: &Self) -> bool {
1730        Rc::ptr_eq(&self.sink, &other.sink)
1731    }
1732}
1733
1734impl Hash for SizeReporterElement {
1735    fn hash<H: Hasher>(&self, state: &mut H) {
1736        (Rc::as_ptr(&self.sink) as usize).hash(state);
1737    }
1738}
1739
1740impl ModifierNodeElement for SizeReporterElement {
1741    type Node = SizeReporterNode;
1742
1743    fn create(&self) -> Self::Node {
1744        SizeReporterNode::new(self.sink.clone())
1745    }
1746
1747    fn update(&self, node: &mut Self::Node) {
1748        node.sink = self.sink.clone();
1749    }
1750
1751    fn capabilities(&self) -> NodeCapabilities {
1752        NodeCapabilities::LAYOUT
1753    }
1754}
1755
1756// ============================================================================
1757// Draw Command Modifier Node
1758// ============================================================================
1759
1760/// Node that stores draw commands emitted by draw modifiers.
1761pub struct DrawCommandNode {
1762    commands: Vec<DrawCommand>,
1763    node_id: Cell<Option<NodeId>>,
1764    state: NodeState,
1765}
1766
1767impl DrawCommandNode {
1768    pub fn new(commands: Vec<DrawCommand>) -> Self {
1769        Self {
1770            commands,
1771            node_id: Cell::new(None),
1772            state: NodeState::new(),
1773        }
1774    }
1775
1776    #[cfg(test)]
1777    pub fn commands(&self) -> &[DrawCommand] {
1778        &self.commands
1779    }
1780
1781    pub(crate) fn observed_commands(&self) -> Vec<DrawCommand> {
1782        let node_id = self.node_id.get();
1783        self.commands
1784            .iter()
1785            .cloned()
1786            .enumerate()
1787            .map(|(index, command)| observe_draw_command(command, node_id, index))
1788            .collect()
1789    }
1790}
1791
1792impl DelegatableNode for DrawCommandNode {
1793    fn node_state(&self) -> &NodeState {
1794        &self.state
1795    }
1796}
1797
1798impl ModifierNode for DrawCommandNode {
1799    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1800        self.node_id.set(context.node_id());
1801        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1802    }
1803
1804    fn on_detach(&mut self) {
1805        if let Some(node_id) = self.node_id.replace(None) {
1806            crate::render_state::clear_draw_observations_for_node(node_id);
1807        }
1808    }
1809
1810    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1811        Some(self)
1812    }
1813
1814    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1815        Some(self)
1816    }
1817}
1818
1819impl DrawModifierNode for DrawCommandNode {
1820    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1821}
1822
1823fn observe_draw_command(
1824    command: DrawCommand,
1825    node_id: Option<NodeId>,
1826    command_index: usize,
1827) -> DrawCommand {
1828    let Some(node_id) = node_id else {
1829        return command;
1830    };
1831    let observation = crate::render_state::DrawObservationScope::new(node_id, command_index);
1832    match command {
1833        DrawCommand::Behind(draw) => DrawCommand::Behind(Rc::new(move |scope| {
1834            crate::render_state::observe_draw_reads(observation, || draw(scope))
1835        })),
1836        DrawCommand::WithContent(draw) => DrawCommand::WithContent(Rc::new(move |scope| {
1837            crate::render_state::observe_draw_reads(observation, || draw(scope))
1838        })),
1839        DrawCommand::Overlay(draw) => DrawCommand::Overlay(Rc::new(move |scope| {
1840            crate::render_state::observe_draw_reads(observation, || draw(scope))
1841        })),
1842    }
1843}
1844
1845fn draw_command_tag(cmd: &DrawCommand) -> u8 {
1846    match cmd {
1847        DrawCommand::Behind(_) => 0,
1848        DrawCommand::WithContent(_) => 1,
1849        DrawCommand::Overlay(_) => 2,
1850    }
1851}
1852
1853fn draw_command_closure_identity(cmd: &DrawCommand) -> *const () {
1854    match cmd {
1855        DrawCommand::Behind(f) | DrawCommand::WithContent(f) | DrawCommand::Overlay(f) => {
1856            Rc::as_ptr(f) as *const ()
1857        }
1858    }
1859}
1860
1861/// Element that wires draw commands into the modifier node chain.
1862#[derive(Clone)]
1863pub struct DrawCommandElement {
1864    commands: Vec<DrawCommand>,
1865}
1866
1867impl DrawCommandElement {
1868    pub fn new(command: DrawCommand) -> Self {
1869        Self {
1870            commands: vec![command],
1871        }
1872    }
1873
1874    pub fn from_commands(commands: Vec<DrawCommand>) -> Self {
1875        Self { commands }
1876    }
1877}
1878
1879impl std::fmt::Debug for DrawCommandElement {
1880    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1881        f.debug_struct("DrawCommandElement")
1882            .field("commands", &self.commands.len())
1883            .finish()
1884    }
1885}
1886
1887impl PartialEq for DrawCommandElement {
1888    fn eq(&self, other: &Self) -> bool {
1889        if self.commands.len() != other.commands.len() {
1890            return false;
1891        }
1892        self.commands
1893            .iter()
1894            .zip(other.commands.iter())
1895            .all(|(a, b)| {
1896                draw_command_tag(a) == draw_command_tag(b)
1897                    && draw_command_closure_identity(a) == draw_command_closure_identity(b)
1898            })
1899    }
1900}
1901
1902impl Eq for DrawCommandElement {}
1903
1904impl std::hash::Hash for DrawCommandElement {
1905    fn hash<H: Hasher>(&self, state: &mut H) {
1906        "draw_commands".hash(state);
1907        self.commands.len().hash(state);
1908        for command in &self.commands {
1909            draw_command_tag(command).hash(state);
1910            (draw_command_closure_identity(command) as usize).hash(state);
1911        }
1912    }
1913}
1914
1915impl ModifierNodeElement for DrawCommandElement {
1916    type Node = DrawCommandNode;
1917
1918    fn create(&self) -> Self::Node {
1919        DrawCommandNode::new(self.commands.clone())
1920    }
1921
1922    fn update(&self, node: &mut Self::Node) {
1923        node.commands = self.commands.clone();
1924    }
1925
1926    fn capabilities(&self) -> NodeCapabilities {
1927        NodeCapabilities::DRAW
1928    }
1929}
1930
1931// ============================================================================
1932// Offset Modifier Node
1933// ============================================================================
1934
1935/// Node that offsets its content by a fixed (x, y) amount.
1936///
1937/// Matches Kotlin: `OffsetNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
1938#[derive(Debug)]
1939pub struct OffsetNode {
1940    x: f32,
1941    y: f32,
1942    rtl_aware: bool,
1943    state: NodeState,
1944}
1945
1946impl OffsetNode {
1947    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
1948        Self {
1949            x,
1950            y,
1951            rtl_aware,
1952            state: NodeState::new(),
1953        }
1954    }
1955
1956    pub fn offset(&self) -> Point {
1957        Point {
1958            x: self.x,
1959            y: self.y,
1960        }
1961    }
1962
1963    pub fn rtl_aware(&self) -> bool {
1964        self.rtl_aware
1965    }
1966}
1967
1968impl DelegatableNode for OffsetNode {
1969    fn node_state(&self) -> &NodeState {
1970        &self.state
1971    }
1972}
1973
1974impl ModifierNode for OffsetNode {
1975    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1976        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
1977    }
1978
1979    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1980        Some(self)
1981    }
1982
1983    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1984        Some(self)
1985    }
1986}
1987
1988impl LayoutModifierNode for OffsetNode {
1989    fn measure(
1990        &self,
1991        _context: &mut dyn ModifierNodeContext,
1992        measurable: &dyn Measurable,
1993        constraints: Constraints,
1994    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1995        // Offset doesn't affect measurement, just placement
1996        let placeable = measurable.measure(constraints);
1997
1998        // Return child size unchanged, but specify the offset for placement
1999        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2000            Size {
2001                width: placeable.width(),
2002                height: placeable.height(),
2003            },
2004            self.x, // Place child offset by x
2005            self.y, // Place child offset by y
2006        )
2007    }
2008
2009    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2010        measurable.min_intrinsic_width(height)
2011    }
2012
2013    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2014        measurable.max_intrinsic_width(height)
2015    }
2016
2017    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2018        measurable.min_intrinsic_height(width)
2019    }
2020
2021    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2022        measurable.max_intrinsic_height(width)
2023    }
2024}
2025
2026/// Element that creates and updates offset nodes.
2027///
2028/// Matches Kotlin: `OffsetElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
2029#[derive(Debug, Clone, PartialEq)]
2030pub struct OffsetElement {
2031    x: f32,
2032    y: f32,
2033    rtl_aware: bool,
2034}
2035
2036impl OffsetElement {
2037    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
2038        Self { x, y, rtl_aware }
2039    }
2040}
2041
2042impl Hash for OffsetElement {
2043    fn hash<H: Hasher>(&self, state: &mut H) {
2044        hash_f32_value(state, self.x);
2045        hash_f32_value(state, self.y);
2046        self.rtl_aware.hash(state);
2047    }
2048}
2049
2050impl ModifierNodeElement for OffsetElement {
2051    type Node = OffsetNode;
2052
2053    fn create(&self) -> Self::Node {
2054        OffsetNode::new(self.x, self.y, self.rtl_aware)
2055    }
2056
2057    fn update(&self, node: &mut Self::Node) {
2058        if node.x != self.x || node.y != self.y || node.rtl_aware != self.rtl_aware {
2059            node.x = self.x;
2060            node.y = self.y;
2061            node.rtl_aware = self.rtl_aware;
2062        }
2063    }
2064
2065    fn capabilities(&self) -> NodeCapabilities {
2066        NodeCapabilities::LAYOUT
2067    }
2068
2069    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2070        Some(InvalidationKind::Layout)
2071    }
2072}
2073
2074// ============================================================================
2075// Fractional Offset Modifier Node
2076// ============================================================================
2077
2078/// Node that offsets its content by a fraction of its own measured size.
2079///
2080/// There is no direct Jetpack Compose modifier equivalent; Compose's slide
2081/// transitions receive the measured size through a lambda instead. This node
2082/// backs `slide_in_vertically` / `slide_out_vertically` in
2083/// `AnimatedVisibility`, where the offset is expressed as a fraction of the
2084/// content height.
2085#[derive(Debug)]
2086pub struct FractionalOffsetNode {
2087    x_fraction: f32,
2088    y_fraction: f32,
2089    state: NodeState,
2090}
2091
2092impl FractionalOffsetNode {
2093    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2094        Self {
2095            x_fraction,
2096            y_fraction,
2097            state: NodeState::new(),
2098        }
2099    }
2100
2101    pub fn fractions(&self) -> Point {
2102        Point {
2103            x: self.x_fraction,
2104            y: self.y_fraction,
2105        }
2106    }
2107}
2108
2109impl DelegatableNode for FractionalOffsetNode {
2110    fn node_state(&self) -> &NodeState {
2111        &self.state
2112    }
2113}
2114
2115impl ModifierNode for FractionalOffsetNode {
2116    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2117        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2118    }
2119
2120    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2121        Some(self)
2122    }
2123
2124    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2125        Some(self)
2126    }
2127}
2128
2129impl LayoutModifierNode for FractionalOffsetNode {
2130    fn measure(
2131        &self,
2132        _context: &mut dyn ModifierNodeContext,
2133        measurable: &dyn Measurable,
2134        constraints: Constraints,
2135    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2136        // Offset doesn't affect measurement, just placement. The placement
2137        // offset is resolved against the measured content size.
2138        let placeable = measurable.measure(constraints);
2139
2140        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2141            Size {
2142                width: placeable.width(),
2143                height: placeable.height(),
2144            },
2145            self.x_fraction * placeable.width(),
2146            self.y_fraction * placeable.height(),
2147        )
2148    }
2149
2150    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2151        measurable.min_intrinsic_width(height)
2152    }
2153
2154    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2155        measurable.max_intrinsic_width(height)
2156    }
2157
2158    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2159        measurable.min_intrinsic_height(width)
2160    }
2161
2162    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2163        measurable.max_intrinsic_height(width)
2164    }
2165}
2166
2167/// Element that creates and updates fractional offset nodes.
2168#[derive(Debug, Clone, PartialEq)]
2169pub struct FractionalOffsetElement {
2170    x_fraction: f32,
2171    y_fraction: f32,
2172}
2173
2174impl FractionalOffsetElement {
2175    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2176        Self {
2177            x_fraction,
2178            y_fraction,
2179        }
2180    }
2181}
2182
2183impl Hash for FractionalOffsetElement {
2184    fn hash<H: Hasher>(&self, state: &mut H) {
2185        "fractional_offset".hash(state);
2186        hash_f32_value(state, self.x_fraction);
2187        hash_f32_value(state, self.y_fraction);
2188    }
2189}
2190
2191impl ModifierNodeElement for FractionalOffsetElement {
2192    type Node = FractionalOffsetNode;
2193
2194    fn create(&self) -> Self::Node {
2195        FractionalOffsetNode::new(self.x_fraction, self.y_fraction)
2196    }
2197
2198    fn update(&self, node: &mut Self::Node) {
2199        if node.x_fraction != self.x_fraction || node.y_fraction != self.y_fraction {
2200            node.x_fraction = self.x_fraction;
2201            node.y_fraction = self.y_fraction;
2202        }
2203    }
2204
2205    fn capabilities(&self) -> NodeCapabilities {
2206        NodeCapabilities::LAYOUT
2207    }
2208
2209    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2210        Some(InvalidationKind::Layout)
2211    }
2212}
2213
2214// ============================================================================
2215// Fill Modifier Node
2216// ============================================================================
2217
2218/// Direction for fill modifiers (horizontal, vertical, or both).
2219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2220pub enum FillDirection {
2221    Horizontal,
2222    Vertical,
2223    Both,
2224}
2225
2226/// Node that fills the maximum available space in one or both dimensions.
2227///
2228/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2229#[derive(Debug)]
2230pub struct FillNode {
2231    direction: FillDirection,
2232    fraction: f32,
2233    state: NodeState,
2234}
2235
2236impl FillNode {
2237    pub fn new(direction: FillDirection, fraction: f32) -> Self {
2238        Self {
2239            direction,
2240            fraction,
2241            state: NodeState::new(),
2242        }
2243    }
2244
2245    pub fn direction(&self) -> FillDirection {
2246        self.direction
2247    }
2248
2249    pub fn fraction(&self) -> f32 {
2250        self.fraction
2251    }
2252}
2253
2254impl DelegatableNode for FillNode {
2255    fn node_state(&self) -> &NodeState {
2256        &self.state
2257    }
2258}
2259
2260impl ModifierNode for FillNode {
2261    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2262        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2263    }
2264
2265    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2266        Some(self)
2267    }
2268
2269    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2270        Some(self)
2271    }
2272}
2273
2274impl LayoutModifierNode for FillNode {
2275    fn measure(
2276        &self,
2277        _context: &mut dyn ModifierNodeContext,
2278        measurable: &dyn Measurable,
2279        constraints: Constraints,
2280    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2281        // Calculate the fill size based on constraints
2282        let (fill_width, child_min_width, child_max_width) = if self.direction
2283            != FillDirection::Vertical
2284            && constraints.max_width != f32::INFINITY
2285        {
2286            let width = (constraints.max_width * self.fraction)
2287                .round()
2288                .clamp(constraints.min_width, constraints.max_width);
2289            // Tight constraint for child on this axis
2290            (width, width, width)
2291        } else {
2292            (
2293                constraints.max_width,
2294                constraints.min_width,
2295                constraints.max_width,
2296            )
2297        };
2298
2299        let (fill_height, child_min_height, child_max_height) = if self.direction
2300            != FillDirection::Horizontal
2301            && constraints.max_height != f32::INFINITY
2302        {
2303            let height = (constraints.max_height * self.fraction)
2304                .round()
2305                .clamp(constraints.min_height, constraints.max_height);
2306            // Tight constraint for child on this axis
2307            (height, height, height)
2308        } else {
2309            (
2310                constraints.max_height,
2311                constraints.min_height,
2312                constraints.max_height,
2313            )
2314        };
2315
2316        let fill_constraints = Constraints {
2317            min_width: child_min_width,
2318            max_width: child_max_width,
2319            min_height: child_min_height,
2320            max_height: child_max_height,
2321        };
2322
2323        let placeable = measurable.measure(fill_constraints);
2324
2325        // Return the FILL size, not the child size.
2326        // The child is measured within tight constraints on the fill axis,
2327        // but we report the fill size to our parent.
2328        let result_width = if self.direction != FillDirection::Vertical
2329            && constraints.max_width != f32::INFINITY
2330        {
2331            fill_width
2332        } else {
2333            placeable.width()
2334        };
2335
2336        let result_height = if self.direction != FillDirection::Horizontal
2337            && constraints.max_height != f32::INFINITY
2338        {
2339            fill_height
2340        } else {
2341            placeable.height()
2342        };
2343
2344        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
2345            width: result_width,
2346            height: result_height,
2347        })
2348    }
2349
2350    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2351        measurable.min_intrinsic_width(height)
2352    }
2353
2354    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2355        measurable.max_intrinsic_width(height)
2356    }
2357
2358    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2359        measurable.min_intrinsic_height(width)
2360    }
2361
2362    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2363        measurable.max_intrinsic_height(width)
2364    }
2365}
2366
2367/// Element that creates and updates fill nodes.
2368///
2369/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2370#[derive(Debug, Clone, PartialEq)]
2371pub struct FillElement {
2372    direction: FillDirection,
2373    fraction: f32,
2374}
2375
2376impl FillElement {
2377    pub fn width(fraction: f32) -> Self {
2378        Self {
2379            direction: FillDirection::Horizontal,
2380            fraction,
2381        }
2382    }
2383
2384    pub fn height(fraction: f32) -> Self {
2385        Self {
2386            direction: FillDirection::Vertical,
2387            fraction,
2388        }
2389    }
2390
2391    pub fn size(fraction: f32) -> Self {
2392        Self {
2393            direction: FillDirection::Both,
2394            fraction,
2395        }
2396    }
2397}
2398
2399impl Hash for FillElement {
2400    fn hash<H: Hasher>(&self, state: &mut H) {
2401        self.direction.hash(state);
2402        hash_f32_value(state, self.fraction);
2403    }
2404}
2405
2406impl ModifierNodeElement for FillElement {
2407    type Node = FillNode;
2408
2409    fn create(&self) -> Self::Node {
2410        FillNode::new(self.direction, self.fraction)
2411    }
2412
2413    fn update(&self, node: &mut Self::Node) {
2414        if node.direction != self.direction || node.fraction != self.fraction {
2415            node.direction = self.direction;
2416            node.fraction = self.fraction;
2417        }
2418    }
2419
2420    fn capabilities(&self) -> NodeCapabilities {
2421        NodeCapabilities::LAYOUT
2422    }
2423}
2424
2425// ============================================================================
2426// Weight Modifier Node
2427// ============================================================================
2428
2429/// Node that records flex weight data for Row/Column parents.
2430#[derive(Debug)]
2431pub struct WeightNode {
2432    weight: f32,
2433    fill: bool,
2434    state: NodeState,
2435}
2436
2437impl WeightNode {
2438    pub fn new(weight: f32, fill: bool) -> Self {
2439        Self {
2440            weight,
2441            fill,
2442            state: NodeState::new(),
2443        }
2444    }
2445
2446    pub fn layout_weight(&self) -> LayoutWeight {
2447        LayoutWeight {
2448            weight: self.weight,
2449            fill: self.fill,
2450        }
2451    }
2452}
2453
2454impl DelegatableNode for WeightNode {
2455    fn node_state(&self) -> &NodeState {
2456        &self.state
2457    }
2458}
2459
2460impl ModifierNode for WeightNode {
2461    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2462        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2463    }
2464}
2465
2466/// Element that creates and updates weight nodes.
2467#[derive(Debug, Clone, PartialEq)]
2468pub struct WeightElement {
2469    weight: f32,
2470    fill: bool,
2471}
2472
2473impl WeightElement {
2474    pub fn new(weight: f32, fill: bool) -> Self {
2475        Self { weight, fill }
2476    }
2477}
2478
2479impl Hash for WeightElement {
2480    fn hash<H: Hasher>(&self, state: &mut H) {
2481        hash_f32_value(state, self.weight);
2482        self.fill.hash(state);
2483    }
2484}
2485
2486impl ModifierNodeElement for WeightElement {
2487    type Node = WeightNode;
2488
2489    fn create(&self) -> Self::Node {
2490        WeightNode::new(self.weight, self.fill)
2491    }
2492
2493    fn update(&self, node: &mut Self::Node) {
2494        if node.weight != self.weight || node.fill != self.fill {
2495            node.weight = self.weight;
2496            node.fill = self.fill;
2497        }
2498    }
2499
2500    fn capabilities(&self) -> NodeCapabilities {
2501        NodeCapabilities::LAYOUT
2502    }
2503}
2504
2505// ============================================================================
2506// Alignment Modifier Node
2507// ============================================================================
2508
2509/// Node that records alignment preferences for Box/Row/Column scopes.
2510#[derive(Debug)]
2511pub struct AlignmentNode {
2512    box_alignment: Option<Alignment>,
2513    column_alignment: Option<HorizontalAlignment>,
2514    row_alignment: Option<VerticalAlignment>,
2515    state: NodeState,
2516}
2517
2518impl AlignmentNode {
2519    pub fn new(
2520        box_alignment: Option<Alignment>,
2521        column_alignment: Option<HorizontalAlignment>,
2522        row_alignment: Option<VerticalAlignment>,
2523    ) -> Self {
2524        Self {
2525            box_alignment,
2526            column_alignment,
2527            row_alignment,
2528            state: NodeState::new(),
2529        }
2530    }
2531
2532    pub fn box_alignment(&self) -> Option<Alignment> {
2533        self.box_alignment
2534    }
2535
2536    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2537        self.column_alignment
2538    }
2539
2540    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2541        self.row_alignment
2542    }
2543}
2544
2545impl DelegatableNode for AlignmentNode {
2546    fn node_state(&self) -> &NodeState {
2547        &self.state
2548    }
2549}
2550
2551impl ModifierNode for AlignmentNode {
2552    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2553        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2554    }
2555}
2556
2557/// Element that creates and updates alignment nodes.
2558#[derive(Debug, Clone, PartialEq)]
2559pub struct AlignmentElement {
2560    box_alignment: Option<Alignment>,
2561    column_alignment: Option<HorizontalAlignment>,
2562    row_alignment: Option<VerticalAlignment>,
2563}
2564
2565impl AlignmentElement {
2566    pub fn box_alignment(alignment: Alignment) -> Self {
2567        Self {
2568            box_alignment: Some(alignment),
2569            column_alignment: None,
2570            row_alignment: None,
2571        }
2572    }
2573
2574    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2575        Self {
2576            box_alignment: None,
2577            column_alignment: Some(alignment),
2578            row_alignment: None,
2579        }
2580    }
2581
2582    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2583        Self {
2584            box_alignment: None,
2585            column_alignment: None,
2586            row_alignment: Some(alignment),
2587        }
2588    }
2589}
2590
2591impl Hash for AlignmentElement {
2592    fn hash<H: Hasher>(&self, state: &mut H) {
2593        if let Some(alignment) = self.box_alignment {
2594            state.write_u8(1);
2595            hash_alignment(state, alignment);
2596        } else {
2597            state.write_u8(0);
2598        }
2599        if let Some(alignment) = self.column_alignment {
2600            state.write_u8(1);
2601            hash_horizontal_alignment(state, alignment);
2602        } else {
2603            state.write_u8(0);
2604        }
2605        if let Some(alignment) = self.row_alignment {
2606            state.write_u8(1);
2607            hash_vertical_alignment(state, alignment);
2608        } else {
2609            state.write_u8(0);
2610        }
2611    }
2612}
2613
2614impl ModifierNodeElement for AlignmentElement {
2615    type Node = AlignmentNode;
2616
2617    fn create(&self) -> Self::Node {
2618        AlignmentNode::new(
2619            self.box_alignment,
2620            self.column_alignment,
2621            self.row_alignment,
2622        )
2623    }
2624
2625    fn update(&self, node: &mut Self::Node) {
2626        if node.box_alignment != self.box_alignment {
2627            node.box_alignment = self.box_alignment;
2628        }
2629        if node.column_alignment != self.column_alignment {
2630            node.column_alignment = self.column_alignment;
2631        }
2632        if node.row_alignment != self.row_alignment {
2633            node.row_alignment = self.row_alignment;
2634        }
2635    }
2636
2637    fn capabilities(&self) -> NodeCapabilities {
2638        NodeCapabilities::LAYOUT
2639    }
2640}
2641
2642// ============================================================================
2643// Intrinsic Size Modifier Node
2644// ============================================================================
2645
2646#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2647pub enum IntrinsicAxis {
2648    Width,
2649    Height,
2650}
2651
2652/// Node that records intrinsic sizing requests.
2653#[derive(Debug)]
2654pub struct IntrinsicSizeNode {
2655    axis: IntrinsicAxis,
2656    size: IntrinsicSize,
2657    state: NodeState,
2658}
2659
2660impl IntrinsicSizeNode {
2661    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2662        Self {
2663            axis,
2664            size,
2665            state: NodeState::new(),
2666        }
2667    }
2668
2669    pub fn axis(&self) -> IntrinsicAxis {
2670        self.axis
2671    }
2672
2673    pub fn intrinsic_size(&self) -> IntrinsicSize {
2674        self.size
2675    }
2676}
2677
2678impl DelegatableNode for IntrinsicSizeNode {
2679    fn node_state(&self) -> &NodeState {
2680        &self.state
2681    }
2682}
2683
2684impl ModifierNode for IntrinsicSizeNode {
2685    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2686        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2687    }
2688}
2689
2690/// Element that creates and updates intrinsic size nodes.
2691#[derive(Debug, Clone, PartialEq)]
2692pub struct IntrinsicSizeElement {
2693    axis: IntrinsicAxis,
2694    size: IntrinsicSize,
2695}
2696
2697impl IntrinsicSizeElement {
2698    pub fn width(size: IntrinsicSize) -> Self {
2699        Self {
2700            axis: IntrinsicAxis::Width,
2701            size,
2702        }
2703    }
2704
2705    pub fn height(size: IntrinsicSize) -> Self {
2706        Self {
2707            axis: IntrinsicAxis::Height,
2708            size,
2709        }
2710    }
2711}
2712
2713impl Hash for IntrinsicSizeElement {
2714    fn hash<H: Hasher>(&self, state: &mut H) {
2715        state.write_u8(match self.axis {
2716            IntrinsicAxis::Width => 0,
2717            IntrinsicAxis::Height => 1,
2718        });
2719        state.write_u8(match self.size {
2720            IntrinsicSize::Min => 0,
2721            IntrinsicSize::Max => 1,
2722        });
2723    }
2724}
2725
2726impl ModifierNodeElement for IntrinsicSizeElement {
2727    type Node = IntrinsicSizeNode;
2728
2729    fn create(&self) -> Self::Node {
2730        IntrinsicSizeNode::new(self.axis, self.size)
2731    }
2732
2733    fn update(&self, node: &mut Self::Node) {
2734        if node.axis != self.axis {
2735            node.axis = self.axis;
2736        }
2737        if node.size != self.size {
2738            node.size = self.size;
2739        }
2740    }
2741
2742    fn capabilities(&self) -> NodeCapabilities {
2743        NodeCapabilities::LAYOUT
2744    }
2745}
2746
2747#[cfg(test)]
2748#[path = "tests/modifier_nodes_tests.rs"]
2749mod tests;