Skip to main content

cranpose_ui/
modifier_nodes.rs

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