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