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        if let Some(resolve) = node.layer_resolver() {
675            node.layer = resolve();
676        }
677        node
678    }
679
680    pub fn layer(&self) -> GraphicsLayer {
681        if let Some(resolve) = self.layer_resolver() {
682            resolve()
683        } else {
684            self.layer.clone()
685        }
686    }
687
688    pub fn layer_resolver(&self) -> Option<Rc<dyn Fn() -> GraphicsLayer>> {
689        self.layer_resolver.as_ref().map(|resolve| {
690            let resolve = resolve.clone();
691            match (&self.lazy_observer, self.lazy_scope_id) {
692                (Some(observer), Some(scope_id)) => {
693                    let observer = observer.clone();
694                    Rc::new(move || {
695                        observer.observe_reads(
696                            scope_id,
697                            |_| crate::request_render_invalidation(),
698                            || resolve(),
699                        )
700                    }) as Rc<dyn Fn() -> GraphicsLayer>
701                }
702                _ => resolve,
703            }
704        })
705    }
706
707    fn set_static(&mut self, layer: GraphicsLayer) {
708        self.layer = layer;
709        self.layer_resolver = None;
710        self.clear_lazy_observation();
711    }
712
713    fn set_lazy(&mut self, layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) {
714        self.layer_resolver = Some(layer_resolver);
715        self.ensure_lazy_observation();
716        if let Some(resolve) = self.layer_resolver() {
717            self.layer = resolve();
718        }
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 => {
1537                    // Wheel/trackpad scrolling should not 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// Pointer Input Modifier Node
1667// ============================================================================
1668
1669/// Node that dispatches pointer events to a user-provided handler.
1670pub struct PointerEventHandlerNode {
1671    handler: Rc<dyn Fn(PointerEvent)>,
1672    state: NodeState,
1673}
1674
1675impl std::fmt::Debug for PointerEventHandlerNode {
1676    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1677        f.debug_struct("PointerEventHandlerNode").finish()
1678    }
1679}
1680
1681impl PointerEventHandlerNode {
1682    pub fn new(handler: Rc<dyn Fn(PointerEvent)>) -> Self {
1683        Self {
1684            handler,
1685            state: NodeState::new(),
1686        }
1687    }
1688
1689    #[allow(dead_code)] // TODO: pointer input implementation
1690    pub fn handler(&self) -> Rc<dyn Fn(PointerEvent)> {
1691        self.handler.clone()
1692    }
1693}
1694
1695impl DelegatableNode for PointerEventHandlerNode {
1696    fn node_state(&self) -> &NodeState {
1697        &self.state
1698    }
1699}
1700
1701impl ModifierNode for PointerEventHandlerNode {
1702    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1703        context.invalidate(cranpose_foundation::InvalidationKind::PointerInput);
1704    }
1705
1706    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1707        Some(self)
1708    }
1709
1710    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1711        Some(self)
1712    }
1713}
1714
1715impl PointerInputNode for PointerEventHandlerNode {
1716    fn on_pointer_event(
1717        &mut self,
1718        _context: &mut dyn ModifierNodeContext,
1719        event: &PointerEvent,
1720    ) -> bool {
1721        (self.handler)(event.clone());
1722        false
1723    }
1724
1725    fn hit_test(&self, _x: f32, _y: f32) -> bool {
1726        true
1727    }
1728
1729    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1730        Some(self.handler.clone())
1731    }
1732}
1733
1734/// Element that wires pointer input handlers into the node chain.
1735#[derive(Clone)]
1736pub struct PointerEventHandlerElement {
1737    handler: Rc<dyn Fn(PointerEvent)>,
1738}
1739
1740impl PointerEventHandlerElement {
1741    #[allow(dead_code)] // TODO: pointer input implementation
1742    pub fn new(handler: Rc<dyn Fn(PointerEvent)>) -> Self {
1743        Self { handler }
1744    }
1745}
1746
1747impl std::fmt::Debug for PointerEventHandlerElement {
1748    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1749        f.debug_struct("PointerEventHandlerElement").finish()
1750    }
1751}
1752
1753impl PartialEq for PointerEventHandlerElement {
1754    fn eq(&self, other: &Self) -> bool {
1755        Rc::ptr_eq(&self.handler, &other.handler)
1756    }
1757}
1758
1759impl Eq for PointerEventHandlerElement {}
1760
1761impl Hash for PointerEventHandlerElement {
1762    fn hash<H: Hasher>(&self, state: &mut H) {
1763        let ptr = Rc::as_ptr(&self.handler) as *const ();
1764        (ptr as usize).hash(state);
1765    }
1766}
1767
1768impl ModifierNodeElement for PointerEventHandlerElement {
1769    type Node = PointerEventHandlerNode;
1770
1771    fn create(&self) -> Self::Node {
1772        PointerEventHandlerNode::new(self.handler.clone())
1773    }
1774
1775    fn update(&self, node: &mut Self::Node) {
1776        node.handler = self.handler.clone();
1777    }
1778
1779    fn capabilities(&self) -> NodeCapabilities {
1780        NodeCapabilities::POINTER_INPUT
1781    }
1782}
1783
1784// ============================================================================
1785// Alpha Modifier Node
1786// ============================================================================
1787
1788/// Node that applies alpha transparency to its content.
1789#[derive(Debug)]
1790pub struct AlphaNode {
1791    alpha: f32,
1792    state: NodeState,
1793}
1794
1795impl AlphaNode {
1796    pub fn new(alpha: f32) -> Self {
1797        Self {
1798            alpha: alpha.clamp(0.0, 1.0),
1799            state: NodeState::new(),
1800        }
1801    }
1802}
1803
1804impl DelegatableNode for AlphaNode {
1805    fn node_state(&self) -> &NodeState {
1806        &self.state
1807    }
1808}
1809
1810impl ModifierNode for AlphaNode {
1811    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1812        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1813    }
1814
1815    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1816        Some(self)
1817    }
1818
1819    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1820        Some(self)
1821    }
1822}
1823
1824impl DrawModifierNode for AlphaNode {
1825    fn draw(&self, _draw_scope: &mut dyn DrawScope) {
1826        // In a full implementation, this would:
1827        // 1. Save the current alpha/layer state
1828        // 2. Apply the alpha value to the graphics context
1829        // 3. Draw content via draw_scope.draw_content()
1830        // 4. Restore previous state
1831        //
1832        // For now this is a placeholder showing the structure
1833    }
1834}
1835
1836/// Element that creates and updates alpha nodes.
1837#[derive(Debug, Clone, PartialEq)]
1838pub struct AlphaElement {
1839    alpha: f32,
1840}
1841
1842impl AlphaElement {
1843    pub fn new(alpha: f32) -> Self {
1844        Self {
1845            alpha: alpha.clamp(0.0, 1.0),
1846        }
1847    }
1848}
1849
1850impl Hash for AlphaElement {
1851    fn hash<H: Hasher>(&self, state: &mut H) {
1852        hash_f32_value(state, self.alpha);
1853    }
1854}
1855
1856impl ModifierNodeElement for AlphaElement {
1857    type Node = AlphaNode;
1858
1859    fn create(&self) -> Self::Node {
1860        AlphaNode::new(self.alpha)
1861    }
1862
1863    fn update(&self, node: &mut Self::Node) {
1864        let new_alpha = self.alpha.clamp(0.0, 1.0);
1865        if (node.alpha - new_alpha).abs() > f32::EPSILON {
1866            node.alpha = new_alpha;
1867            // In a full implementation, would invalidate draw here
1868        }
1869    }
1870
1871    fn capabilities(&self) -> NodeCapabilities {
1872        NodeCapabilities::DRAW
1873    }
1874}
1875
1876// ============================================================================
1877// Clip-To-Bounds Modifier Node
1878// ============================================================================
1879
1880/// Node that marks the subtree for clipping during rendering.
1881#[derive(Debug)]
1882pub struct ClipToBoundsNode {
1883    state: NodeState,
1884}
1885
1886impl ClipToBoundsNode {
1887    pub fn new() -> Self {
1888        Self {
1889            state: NodeState::new(),
1890        }
1891    }
1892}
1893
1894impl DelegatableNode for ClipToBoundsNode {
1895    fn node_state(&self) -> &NodeState {
1896        &self.state
1897    }
1898}
1899
1900impl ModifierNode for ClipToBoundsNode {
1901    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1902        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1903    }
1904
1905    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1906        Some(self)
1907    }
1908
1909    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1910        Some(self)
1911    }
1912}
1913
1914impl DrawModifierNode for ClipToBoundsNode {
1915    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1916}
1917
1918/// Element that creates clip-to-bounds nodes.
1919#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1920pub struct ClipToBoundsElement;
1921
1922impl ClipToBoundsElement {
1923    pub fn new() -> Self {
1924        Self
1925    }
1926}
1927
1928impl ModifierNodeElement for ClipToBoundsElement {
1929    type Node = ClipToBoundsNode;
1930
1931    fn create(&self) -> Self::Node {
1932        ClipToBoundsNode::new()
1933    }
1934
1935    fn update(&self, _node: &mut Self::Node) {}
1936
1937    fn capabilities(&self) -> NodeCapabilities {
1938        NodeCapabilities::DRAW
1939    }
1940}
1941
1942// ============================================================================
1943// Draw Command Modifier Node
1944// ============================================================================
1945
1946/// Node that stores draw commands emitted by draw modifiers.
1947pub struct DrawCommandNode {
1948    commands: Vec<DrawCommand>,
1949    state: NodeState,
1950}
1951
1952impl DrawCommandNode {
1953    pub fn new(commands: Vec<DrawCommand>) -> Self {
1954        Self {
1955            commands,
1956            state: NodeState::new(),
1957        }
1958    }
1959
1960    pub fn commands(&self) -> &[DrawCommand] {
1961        &self.commands
1962    }
1963}
1964
1965impl DelegatableNode for DrawCommandNode {
1966    fn node_state(&self) -> &NodeState {
1967        &self.state
1968    }
1969}
1970
1971impl ModifierNode for DrawCommandNode {
1972    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1973        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1974    }
1975
1976    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1977        Some(self)
1978    }
1979
1980    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1981        Some(self)
1982    }
1983}
1984
1985impl DrawModifierNode for DrawCommandNode {
1986    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1987}
1988
1989fn draw_command_tag(cmd: &DrawCommand) -> u8 {
1990    match cmd {
1991        DrawCommand::Behind(_) => 0,
1992        DrawCommand::WithContent(_) => 1,
1993        DrawCommand::Overlay(_) => 2,
1994    }
1995}
1996
1997fn draw_command_closure_identity(cmd: &DrawCommand) -> *const () {
1998    match cmd {
1999        DrawCommand::Behind(f) | DrawCommand::WithContent(f) | DrawCommand::Overlay(f) => {
2000            Rc::as_ptr(f) as *const ()
2001        }
2002    }
2003}
2004
2005/// Element that wires draw commands into the modifier node chain.
2006#[derive(Clone)]
2007pub struct DrawCommandElement {
2008    commands: Vec<DrawCommand>,
2009}
2010
2011impl DrawCommandElement {
2012    pub fn new(command: DrawCommand) -> Self {
2013        Self {
2014            commands: vec![command],
2015        }
2016    }
2017
2018    pub fn from_commands(commands: Vec<DrawCommand>) -> Self {
2019        Self { commands }
2020    }
2021}
2022
2023impl std::fmt::Debug for DrawCommandElement {
2024    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2025        f.debug_struct("DrawCommandElement")
2026            .field("commands", &self.commands.len())
2027            .finish()
2028    }
2029}
2030
2031impl PartialEq for DrawCommandElement {
2032    fn eq(&self, other: &Self) -> bool {
2033        if self.commands.len() != other.commands.len() {
2034            return false;
2035        }
2036        self.commands
2037            .iter()
2038            .zip(other.commands.iter())
2039            .all(|(a, b)| {
2040                draw_command_tag(a) == draw_command_tag(b)
2041                    && draw_command_closure_identity(a) == draw_command_closure_identity(b)
2042            })
2043    }
2044}
2045
2046impl Eq for DrawCommandElement {}
2047
2048impl std::hash::Hash for DrawCommandElement {
2049    fn hash<H: Hasher>(&self, state: &mut H) {
2050        "draw_commands".hash(state);
2051        self.commands.len().hash(state);
2052        for command in &self.commands {
2053            draw_command_tag(command).hash(state);
2054            (draw_command_closure_identity(command) as usize).hash(state);
2055        }
2056    }
2057}
2058
2059impl ModifierNodeElement for DrawCommandElement {
2060    type Node = DrawCommandNode;
2061
2062    fn create(&self) -> Self::Node {
2063        DrawCommandNode::new(self.commands.clone())
2064    }
2065
2066    fn update(&self, node: &mut Self::Node) {
2067        node.commands = self.commands.clone();
2068    }
2069
2070    fn capabilities(&self) -> NodeCapabilities {
2071        NodeCapabilities::DRAW
2072    }
2073}
2074
2075// ============================================================================
2076// Offset Modifier Node
2077// ============================================================================
2078
2079/// Node that offsets its content by a fixed (x, y) amount.
2080///
2081/// Matches Kotlin: `OffsetNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
2082#[derive(Debug)]
2083pub struct OffsetNode {
2084    x: f32,
2085    y: f32,
2086    rtl_aware: bool,
2087    state: NodeState,
2088}
2089
2090impl OffsetNode {
2091    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
2092        Self {
2093            x,
2094            y,
2095            rtl_aware,
2096            state: NodeState::new(),
2097        }
2098    }
2099
2100    pub fn offset(&self) -> Point {
2101        Point {
2102            x: self.x,
2103            y: self.y,
2104        }
2105    }
2106
2107    pub fn rtl_aware(&self) -> bool {
2108        self.rtl_aware
2109    }
2110}
2111
2112impl DelegatableNode for OffsetNode {
2113    fn node_state(&self) -> &NodeState {
2114        &self.state
2115    }
2116}
2117
2118impl ModifierNode for OffsetNode {
2119    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2120        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2121    }
2122
2123    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2124        Some(self)
2125    }
2126
2127    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2128        Some(self)
2129    }
2130}
2131
2132impl LayoutModifierNode for OffsetNode {
2133    fn measure(
2134        &self,
2135        _context: &mut dyn ModifierNodeContext,
2136        measurable: &dyn Measurable,
2137        constraints: Constraints,
2138    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2139        // Offset doesn't affect measurement, just placement
2140        let placeable = measurable.measure(constraints);
2141
2142        // Return child size unchanged, but specify the offset for placement
2143        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2144            Size {
2145                width: placeable.width(),
2146                height: placeable.height(),
2147            },
2148            self.x, // Place child offset by x
2149            self.y, // Place child offset by y
2150        )
2151    }
2152
2153    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2154        measurable.min_intrinsic_width(height)
2155    }
2156
2157    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2158        measurable.max_intrinsic_width(height)
2159    }
2160
2161    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2162        measurable.min_intrinsic_height(width)
2163    }
2164
2165    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2166        measurable.max_intrinsic_height(width)
2167    }
2168
2169    fn create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
2170        Some(Box::new(OffsetMeasurementProxy {
2171            x: self.x,
2172            y: self.y,
2173            rtl_aware: self.rtl_aware,
2174        }))
2175    }
2176}
2177
2178/// Measurement proxy for OffsetNode that snapshots live state.
2179///
2180/// Phase 2: Instead of reconstructing nodes via `OffsetNode::new()`, this proxy
2181/// directly implements measurement logic. Since offset doesn't affect measurement
2182/// (only placement), this is a simple passthrough.
2183struct OffsetMeasurementProxy {
2184    x: f32,
2185    y: f32,
2186    #[allow(dead_code)]
2187    rtl_aware: bool,
2188}
2189
2190impl MeasurementProxy for OffsetMeasurementProxy {
2191    fn measure_proxy(
2192        &self,
2193        _context: &mut dyn ModifierNodeContext,
2194        wrapped: &dyn Measurable,
2195        constraints: Constraints,
2196    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2197        // Offset doesn't affect measurement, just placement - simple passthrough
2198        let placeable = wrapped.measure(constraints);
2199
2200        // Return child size unchanged, but specify the offset for placement
2201        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2202            Size {
2203                width: placeable.width(),
2204                height: placeable.height(),
2205            },
2206            self.x, // Place child offset by x
2207            self.y, // Place child offset by y
2208        )
2209    }
2210
2211    fn min_intrinsic_width_proxy(&self, wrapped: &dyn Measurable, height: f32) -> f32 {
2212        wrapped.min_intrinsic_width(height)
2213    }
2214
2215    fn max_intrinsic_width_proxy(&self, wrapped: &dyn Measurable, height: f32) -> f32 {
2216        wrapped.max_intrinsic_width(height)
2217    }
2218
2219    fn min_intrinsic_height_proxy(&self, wrapped: &dyn Measurable, width: f32) -> f32 {
2220        wrapped.min_intrinsic_height(width)
2221    }
2222
2223    fn max_intrinsic_height_proxy(&self, wrapped: &dyn Measurable, width: f32) -> f32 {
2224        wrapped.max_intrinsic_height(width)
2225    }
2226}
2227
2228/// Element that creates and updates offset nodes.
2229///
2230/// Matches Kotlin: `OffsetElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
2231#[derive(Debug, Clone, PartialEq)]
2232pub struct OffsetElement {
2233    x: f32,
2234    y: f32,
2235    rtl_aware: bool,
2236}
2237
2238impl OffsetElement {
2239    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
2240        Self { x, y, rtl_aware }
2241    }
2242}
2243
2244impl Hash for OffsetElement {
2245    fn hash<H: Hasher>(&self, state: &mut H) {
2246        hash_f32_value(state, self.x);
2247        hash_f32_value(state, self.y);
2248        self.rtl_aware.hash(state);
2249    }
2250}
2251
2252impl ModifierNodeElement for OffsetElement {
2253    type Node = OffsetNode;
2254
2255    fn create(&self) -> Self::Node {
2256        OffsetNode::new(self.x, self.y, self.rtl_aware)
2257    }
2258
2259    fn update(&self, node: &mut Self::Node) {
2260        if node.x != self.x || node.y != self.y || node.rtl_aware != self.rtl_aware {
2261            node.x = self.x;
2262            node.y = self.y;
2263            node.rtl_aware = self.rtl_aware;
2264        }
2265    }
2266
2267    fn capabilities(&self) -> NodeCapabilities {
2268        NodeCapabilities::LAYOUT
2269    }
2270}
2271
2272// ============================================================================
2273// Fill Modifier Node
2274// ============================================================================
2275
2276/// Direction for fill modifiers (horizontal, vertical, or both).
2277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2278pub enum FillDirection {
2279    Horizontal,
2280    Vertical,
2281    Both,
2282}
2283
2284/// Node that fills the maximum available space in one or both dimensions.
2285///
2286/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2287#[derive(Debug)]
2288pub struct FillNode {
2289    direction: FillDirection,
2290    fraction: f32,
2291    state: NodeState,
2292}
2293
2294impl FillNode {
2295    pub fn new(direction: FillDirection, fraction: f32) -> Self {
2296        Self {
2297            direction,
2298            fraction,
2299            state: NodeState::new(),
2300        }
2301    }
2302
2303    pub fn direction(&self) -> FillDirection {
2304        self.direction
2305    }
2306
2307    pub fn fraction(&self) -> f32 {
2308        self.fraction
2309    }
2310}
2311
2312impl DelegatableNode for FillNode {
2313    fn node_state(&self) -> &NodeState {
2314        &self.state
2315    }
2316}
2317
2318impl ModifierNode for FillNode {
2319    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2320        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2321    }
2322
2323    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2324        Some(self)
2325    }
2326
2327    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2328        Some(self)
2329    }
2330}
2331
2332impl LayoutModifierNode for FillNode {
2333    fn measure(
2334        &self,
2335        _context: &mut dyn ModifierNodeContext,
2336        measurable: &dyn Measurable,
2337        constraints: Constraints,
2338    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2339        // Calculate the fill size based on constraints
2340        let (fill_width, child_min_width, child_max_width) = if self.direction
2341            != FillDirection::Vertical
2342            && constraints.max_width != f32::INFINITY
2343        {
2344            let width = (constraints.max_width * self.fraction)
2345                .round()
2346                .clamp(constraints.min_width, constraints.max_width);
2347            // Tight constraint for child on this axis
2348            (width, width, width)
2349        } else {
2350            (
2351                constraints.max_width,
2352                constraints.min_width,
2353                constraints.max_width,
2354            )
2355        };
2356
2357        let (fill_height, child_min_height, child_max_height) = if self.direction
2358            != FillDirection::Horizontal
2359            && constraints.max_height != f32::INFINITY
2360        {
2361            let height = (constraints.max_height * self.fraction)
2362                .round()
2363                .clamp(constraints.min_height, constraints.max_height);
2364            // Tight constraint for child on this axis
2365            (height, height, height)
2366        } else {
2367            (
2368                constraints.max_height,
2369                constraints.min_height,
2370                constraints.max_height,
2371            )
2372        };
2373
2374        let fill_constraints = Constraints {
2375            min_width: child_min_width,
2376            max_width: child_max_width,
2377            min_height: child_min_height,
2378            max_height: child_max_height,
2379        };
2380
2381        let placeable = measurable.measure(fill_constraints);
2382
2383        // Return the FILL size, not the child size.
2384        // The child is measured within tight constraints on the fill axis,
2385        // but we report the fill size to our parent.
2386        let result_width = if self.direction != FillDirection::Vertical
2387            && constraints.max_width != f32::INFINITY
2388        {
2389            fill_width
2390        } else {
2391            placeable.width()
2392        };
2393
2394        let result_height = if self.direction != FillDirection::Horizontal
2395            && constraints.max_height != f32::INFINITY
2396        {
2397            fill_height
2398        } else {
2399            placeable.height()
2400        };
2401
2402        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
2403            width: result_width,
2404            height: result_height,
2405        })
2406    }
2407
2408    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2409        measurable.min_intrinsic_width(height)
2410    }
2411
2412    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2413        measurable.max_intrinsic_width(height)
2414    }
2415
2416    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2417        measurable.min_intrinsic_height(width)
2418    }
2419
2420    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2421        measurable.max_intrinsic_height(width)
2422    }
2423
2424    fn create_measurement_proxy(&self) -> Option<Box<dyn MeasurementProxy>> {
2425        Some(Box::new(FillMeasurementProxy {
2426            direction: self.direction,
2427            fraction: self.fraction,
2428        }))
2429    }
2430}
2431
2432/// Measurement proxy for FillNode that snapshots live state.
2433///
2434/// Phase 2: Instead of reconstructing nodes via `FillNode::new()`, this proxy
2435/// directly implements measurement logic using the snapshotted fill configuration.
2436struct FillMeasurementProxy {
2437    direction: FillDirection,
2438    fraction: f32,
2439}
2440
2441impl MeasurementProxy for FillMeasurementProxy {
2442    fn measure_proxy(
2443        &self,
2444        _context: &mut dyn ModifierNodeContext,
2445        wrapped: &dyn Measurable,
2446        constraints: Constraints,
2447    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2448        // Calculate the fill size based on constraints
2449        let (fill_width, child_min_width, child_max_width) = if self.direction
2450            != FillDirection::Vertical
2451            && constraints.max_width != f32::INFINITY
2452        {
2453            let width = (constraints.max_width * self.fraction)
2454                .round()
2455                .clamp(constraints.min_width, constraints.max_width);
2456            (width, width, width)
2457        } else {
2458            (
2459                constraints.max_width,
2460                constraints.min_width,
2461                constraints.max_width,
2462            )
2463        };
2464
2465        let (fill_height, child_min_height, child_max_height) = if self.direction
2466            != FillDirection::Horizontal
2467            && constraints.max_height != f32::INFINITY
2468        {
2469            let height = (constraints.max_height * self.fraction)
2470                .round()
2471                .clamp(constraints.min_height, constraints.max_height);
2472            (height, height, height)
2473        } else {
2474            (
2475                constraints.max_height,
2476                constraints.min_height,
2477                constraints.max_height,
2478            )
2479        };
2480
2481        let fill_constraints = Constraints {
2482            min_width: child_min_width,
2483            max_width: child_max_width,
2484            min_height: child_min_height,
2485            max_height: child_max_height,
2486        };
2487
2488        let placeable = wrapped.measure(fill_constraints);
2489
2490        // Return the FILL size, not the child size
2491        let result_width = if self.direction != FillDirection::Vertical
2492            && constraints.max_width != f32::INFINITY
2493        {
2494            fill_width
2495        } else {
2496            placeable.width()
2497        };
2498
2499        let result_height = if self.direction != FillDirection::Horizontal
2500            && constraints.max_height != f32::INFINITY
2501        {
2502            fill_height
2503        } else {
2504            placeable.height()
2505        };
2506
2507        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
2508            width: result_width,
2509            height: result_height,
2510        })
2511    }
2512
2513    fn min_intrinsic_width_proxy(&self, wrapped: &dyn Measurable, height: f32) -> f32 {
2514        wrapped.min_intrinsic_width(height)
2515    }
2516
2517    fn max_intrinsic_width_proxy(&self, wrapped: &dyn Measurable, height: f32) -> f32 {
2518        wrapped.max_intrinsic_width(height)
2519    }
2520
2521    fn min_intrinsic_height_proxy(&self, wrapped: &dyn Measurable, width: f32) -> f32 {
2522        wrapped.min_intrinsic_height(width)
2523    }
2524
2525    fn max_intrinsic_height_proxy(&self, wrapped: &dyn Measurable, width: f32) -> f32 {
2526        wrapped.max_intrinsic_height(width)
2527    }
2528}
2529
2530/// Element that creates and updates fill nodes.
2531///
2532/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2533#[derive(Debug, Clone, PartialEq)]
2534pub struct FillElement {
2535    direction: FillDirection,
2536    fraction: f32,
2537}
2538
2539impl FillElement {
2540    pub fn width(fraction: f32) -> Self {
2541        Self {
2542            direction: FillDirection::Horizontal,
2543            fraction,
2544        }
2545    }
2546
2547    pub fn height(fraction: f32) -> Self {
2548        Self {
2549            direction: FillDirection::Vertical,
2550            fraction,
2551        }
2552    }
2553
2554    pub fn size(fraction: f32) -> Self {
2555        Self {
2556            direction: FillDirection::Both,
2557            fraction,
2558        }
2559    }
2560}
2561
2562impl Hash for FillElement {
2563    fn hash<H: Hasher>(&self, state: &mut H) {
2564        self.direction.hash(state);
2565        hash_f32_value(state, self.fraction);
2566    }
2567}
2568
2569impl ModifierNodeElement for FillElement {
2570    type Node = FillNode;
2571
2572    fn create(&self) -> Self::Node {
2573        FillNode::new(self.direction, self.fraction)
2574    }
2575
2576    fn update(&self, node: &mut Self::Node) {
2577        if node.direction != self.direction || node.fraction != self.fraction {
2578            node.direction = self.direction;
2579            node.fraction = self.fraction;
2580        }
2581    }
2582
2583    fn capabilities(&self) -> NodeCapabilities {
2584        NodeCapabilities::LAYOUT
2585    }
2586}
2587
2588// ============================================================================
2589// Weight Modifier Node
2590// ============================================================================
2591
2592/// Node that records flex weight data for Row/Column parents.
2593#[derive(Debug)]
2594pub struct WeightNode {
2595    weight: f32,
2596    fill: bool,
2597    state: NodeState,
2598}
2599
2600impl WeightNode {
2601    pub fn new(weight: f32, fill: bool) -> Self {
2602        Self {
2603            weight,
2604            fill,
2605            state: NodeState::new(),
2606        }
2607    }
2608
2609    pub fn layout_weight(&self) -> LayoutWeight {
2610        LayoutWeight {
2611            weight: self.weight,
2612            fill: self.fill,
2613        }
2614    }
2615}
2616
2617impl DelegatableNode for WeightNode {
2618    fn node_state(&self) -> &NodeState {
2619        &self.state
2620    }
2621}
2622
2623impl ModifierNode for WeightNode {
2624    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2625        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2626    }
2627}
2628
2629/// Element that creates and updates weight nodes.
2630#[derive(Debug, Clone, PartialEq)]
2631pub struct WeightElement {
2632    weight: f32,
2633    fill: bool,
2634}
2635
2636impl WeightElement {
2637    pub fn new(weight: f32, fill: bool) -> Self {
2638        Self { weight, fill }
2639    }
2640}
2641
2642impl Hash for WeightElement {
2643    fn hash<H: Hasher>(&self, state: &mut H) {
2644        hash_f32_value(state, self.weight);
2645        self.fill.hash(state);
2646    }
2647}
2648
2649impl ModifierNodeElement for WeightElement {
2650    type Node = WeightNode;
2651
2652    fn create(&self) -> Self::Node {
2653        WeightNode::new(self.weight, self.fill)
2654    }
2655
2656    fn update(&self, node: &mut Self::Node) {
2657        if node.weight != self.weight || node.fill != self.fill {
2658            node.weight = self.weight;
2659            node.fill = self.fill;
2660        }
2661    }
2662
2663    fn capabilities(&self) -> NodeCapabilities {
2664        NodeCapabilities::LAYOUT
2665    }
2666}
2667
2668// ============================================================================
2669// Alignment Modifier Node
2670// ============================================================================
2671
2672/// Node that records alignment preferences for Box/Row/Column scopes.
2673#[derive(Debug)]
2674pub struct AlignmentNode {
2675    box_alignment: Option<Alignment>,
2676    column_alignment: Option<HorizontalAlignment>,
2677    row_alignment: Option<VerticalAlignment>,
2678    state: NodeState,
2679}
2680
2681impl AlignmentNode {
2682    pub fn new(
2683        box_alignment: Option<Alignment>,
2684        column_alignment: Option<HorizontalAlignment>,
2685        row_alignment: Option<VerticalAlignment>,
2686    ) -> Self {
2687        Self {
2688            box_alignment,
2689            column_alignment,
2690            row_alignment,
2691            state: NodeState::new(),
2692        }
2693    }
2694
2695    pub fn box_alignment(&self) -> Option<Alignment> {
2696        self.box_alignment
2697    }
2698
2699    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2700        self.column_alignment
2701    }
2702
2703    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2704        self.row_alignment
2705    }
2706}
2707
2708impl DelegatableNode for AlignmentNode {
2709    fn node_state(&self) -> &NodeState {
2710        &self.state
2711    }
2712}
2713
2714impl ModifierNode for AlignmentNode {
2715    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2716        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2717    }
2718}
2719
2720/// Element that creates and updates alignment nodes.
2721#[derive(Debug, Clone, PartialEq)]
2722pub struct AlignmentElement {
2723    box_alignment: Option<Alignment>,
2724    column_alignment: Option<HorizontalAlignment>,
2725    row_alignment: Option<VerticalAlignment>,
2726}
2727
2728impl AlignmentElement {
2729    pub fn box_alignment(alignment: Alignment) -> Self {
2730        Self {
2731            box_alignment: Some(alignment),
2732            column_alignment: None,
2733            row_alignment: None,
2734        }
2735    }
2736
2737    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2738        Self {
2739            box_alignment: None,
2740            column_alignment: Some(alignment),
2741            row_alignment: None,
2742        }
2743    }
2744
2745    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2746        Self {
2747            box_alignment: None,
2748            column_alignment: None,
2749            row_alignment: Some(alignment),
2750        }
2751    }
2752}
2753
2754impl Hash for AlignmentElement {
2755    fn hash<H: Hasher>(&self, state: &mut H) {
2756        if let Some(alignment) = self.box_alignment {
2757            state.write_u8(1);
2758            hash_alignment(state, alignment);
2759        } else {
2760            state.write_u8(0);
2761        }
2762        if let Some(alignment) = self.column_alignment {
2763            state.write_u8(1);
2764            hash_horizontal_alignment(state, alignment);
2765        } else {
2766            state.write_u8(0);
2767        }
2768        if let Some(alignment) = self.row_alignment {
2769            state.write_u8(1);
2770            hash_vertical_alignment(state, alignment);
2771        } else {
2772            state.write_u8(0);
2773        }
2774    }
2775}
2776
2777impl ModifierNodeElement for AlignmentElement {
2778    type Node = AlignmentNode;
2779
2780    fn create(&self) -> Self::Node {
2781        AlignmentNode::new(
2782            self.box_alignment,
2783            self.column_alignment,
2784            self.row_alignment,
2785        )
2786    }
2787
2788    fn update(&self, node: &mut Self::Node) {
2789        if node.box_alignment != self.box_alignment {
2790            node.box_alignment = self.box_alignment;
2791        }
2792        if node.column_alignment != self.column_alignment {
2793            node.column_alignment = self.column_alignment;
2794        }
2795        if node.row_alignment != self.row_alignment {
2796            node.row_alignment = self.row_alignment;
2797        }
2798    }
2799
2800    fn capabilities(&self) -> NodeCapabilities {
2801        NodeCapabilities::LAYOUT
2802    }
2803}
2804
2805// ============================================================================
2806// Intrinsic Size Modifier Node
2807// ============================================================================
2808
2809#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2810pub enum IntrinsicAxis {
2811    Width,
2812    Height,
2813}
2814
2815/// Node that records intrinsic sizing requests.
2816#[derive(Debug)]
2817pub struct IntrinsicSizeNode {
2818    axis: IntrinsicAxis,
2819    size: IntrinsicSize,
2820    state: NodeState,
2821}
2822
2823impl IntrinsicSizeNode {
2824    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2825        Self {
2826            axis,
2827            size,
2828            state: NodeState::new(),
2829        }
2830    }
2831
2832    pub fn axis(&self) -> IntrinsicAxis {
2833        self.axis
2834    }
2835
2836    pub fn intrinsic_size(&self) -> IntrinsicSize {
2837        self.size
2838    }
2839}
2840
2841impl DelegatableNode for IntrinsicSizeNode {
2842    fn node_state(&self) -> &NodeState {
2843        &self.state
2844    }
2845}
2846
2847impl ModifierNode for IntrinsicSizeNode {
2848    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2849        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2850    }
2851}
2852
2853/// Element that creates and updates intrinsic size nodes.
2854#[derive(Debug, Clone, PartialEq)]
2855pub struct IntrinsicSizeElement {
2856    axis: IntrinsicAxis,
2857    size: IntrinsicSize,
2858}
2859
2860impl IntrinsicSizeElement {
2861    pub fn width(size: IntrinsicSize) -> Self {
2862        Self {
2863            axis: IntrinsicAxis::Width,
2864            size,
2865        }
2866    }
2867
2868    pub fn height(size: IntrinsicSize) -> Self {
2869        Self {
2870            axis: IntrinsicAxis::Height,
2871            size,
2872        }
2873    }
2874}
2875
2876impl Hash for IntrinsicSizeElement {
2877    fn hash<H: Hasher>(&self, state: &mut H) {
2878        state.write_u8(match self.axis {
2879            IntrinsicAxis::Width => 0,
2880            IntrinsicAxis::Height => 1,
2881        });
2882        state.write_u8(match self.size {
2883            IntrinsicSize::Min => 0,
2884            IntrinsicSize::Max => 1,
2885        });
2886    }
2887}
2888
2889impl ModifierNodeElement for IntrinsicSizeElement {
2890    type Node = IntrinsicSizeNode;
2891
2892    fn create(&self) -> Self::Node {
2893        IntrinsicSizeNode::new(self.axis, self.size)
2894    }
2895
2896    fn update(&self, node: &mut Self::Node) {
2897        if node.axis != self.axis {
2898            node.axis = self.axis;
2899        }
2900        if node.size != self.size {
2901            node.size = self.size;
2902        }
2903    }
2904
2905    fn capabilities(&self) -> NodeCapabilities {
2906        NodeCapabilities::LAYOUT
2907    }
2908}
2909
2910#[cfg(test)]
2911#[path = "tests/modifier_nodes_tests.rs"]
2912mod tests;