Skip to main content

cranpose_ui/
modifier_nodes.rs

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