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    ModifierNode, ModifierNodeContext, ModifierNodeElement, NodeCapabilities, NodeState,
67    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
219const LAZY_GRAPHICS_LAYER_SCOPE_ID: usize = 1;
220
221// ============================================================================
222// Padding Modifier Node
223// ============================================================================
224
225/// Node that adds padding around its content.
226#[derive(Debug)]
227pub struct PaddingNode {
228    padding: EdgeInsets,
229    state: NodeState,
230}
231
232impl PaddingNode {
233    pub fn new(padding: EdgeInsets) -> Self {
234        Self {
235            padding,
236            state: NodeState::new(),
237        }
238    }
239
240    pub fn padding(&self) -> EdgeInsets {
241        self.padding
242    }
243}
244
245impl DelegatableNode for PaddingNode {
246    fn node_state(&self) -> &NodeState {
247        &self.state
248    }
249}
250
251impl ModifierNode for PaddingNode {
252    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
253        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
254    }
255
256    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
257        Some(self)
258    }
259
260    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
261        Some(self)
262    }
263}
264
265impl LayoutModifierNode for PaddingNode {
266    fn measure(
267        &self,
268        _context: &mut dyn ModifierNodeContext,
269        measurable: &dyn Measurable,
270        constraints: Constraints,
271    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
272        // Convert padding to floating point values
273        let horizontal_padding = self.padding.horizontal_sum();
274        let vertical_padding = self.padding.vertical_sum();
275
276        // Subtract padding from available space
277        let inner_constraints = Constraints {
278            min_width: (constraints.min_width - horizontal_padding).max(0.0),
279            max_width: (constraints.max_width - horizontal_padding).max(0.0),
280            min_height: (constraints.min_height - vertical_padding).max(0.0),
281            max_height: (constraints.max_height - vertical_padding).max(0.0),
282        };
283
284        // Measure the wrapped content
285        let inner_placeable = measurable.measure(inner_constraints);
286        let inner_width = inner_placeable.width();
287        let inner_height = inner_placeable.height();
288
289        let (width, height) = constraints.constrain(
290            inner_width + horizontal_padding,
291            inner_height + vertical_padding,
292        );
293
294        // Return size with padding added, and placement offset to position child inside padding
295        cranpose_ui_layout::LayoutModifierMeasureResult::new(
296            Size { width, height },
297            self.padding.left, // Place child offset by left padding
298            self.padding.top,  // Place child offset by top padding
299        )
300    }
301
302    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
303        let vertical_padding = self.padding.vertical_sum();
304        let inner_height = (height - vertical_padding).max(0.0);
305        let inner_width = measurable.min_intrinsic_width(inner_height);
306        inner_width + self.padding.horizontal_sum()
307    }
308
309    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
310        let vertical_padding = self.padding.vertical_sum();
311        let inner_height = (height - vertical_padding).max(0.0);
312        let inner_width = measurable.max_intrinsic_width(inner_height);
313        inner_width + self.padding.horizontal_sum()
314    }
315
316    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
317        let horizontal_padding = self.padding.horizontal_sum();
318        let inner_width = (width - horizontal_padding).max(0.0);
319        let inner_height = measurable.min_intrinsic_height(inner_width);
320        inner_height + self.padding.vertical_sum()
321    }
322
323    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
324        let horizontal_padding = self.padding.horizontal_sum();
325        let inner_width = (width - horizontal_padding).max(0.0);
326        let inner_height = measurable.max_intrinsic_height(inner_width);
327        inner_height + self.padding.vertical_sum()
328    }
329}
330
331/// Element that creates and updates padding nodes.
332#[derive(Debug, Clone, PartialEq)]
333pub struct PaddingElement {
334    padding: EdgeInsets,
335}
336
337impl PaddingElement {
338    pub fn new(padding: EdgeInsets) -> Self {
339        Self { padding }
340    }
341}
342
343impl Hash for PaddingElement {
344    fn hash<H: Hasher>(&self, state: &mut H) {
345        hash_f32_value(state, self.padding.left);
346        hash_f32_value(state, self.padding.top);
347        hash_f32_value(state, self.padding.right);
348        hash_f32_value(state, self.padding.bottom);
349    }
350}
351
352impl ModifierNodeElement for PaddingElement {
353    type Node = PaddingNode;
354
355    fn create(&self) -> Self::Node {
356        PaddingNode::new(self.padding)
357    }
358
359    fn update(&self, node: &mut Self::Node) {
360        if node.padding != self.padding {
361            node.padding = self.padding;
362        }
363    }
364
365    fn capabilities(&self) -> NodeCapabilities {
366        NodeCapabilities::LAYOUT
367    }
368}
369
370// ============================================================================
371// Background Modifier Node
372// ============================================================================
373
374/// Node that draws a background behind its content.
375#[derive(Debug)]
376pub struct BackgroundNode {
377    color: Color,
378    shape: Option<RoundedCornerShape>,
379    state: NodeState,
380}
381
382impl BackgroundNode {
383    pub fn new(color: Color) -> Self {
384        Self {
385            color,
386            shape: None,
387            state: NodeState::new(),
388        }
389    }
390
391    pub fn color(&self) -> Color {
392        self.color
393    }
394
395    pub fn shape(&self) -> Option<RoundedCornerShape> {
396        self.shape
397    }
398}
399
400impl DelegatableNode for BackgroundNode {
401    fn node_state(&self) -> &NodeState {
402        &self.state
403    }
404}
405
406impl ModifierNode for BackgroundNode {
407    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
408        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
409    }
410
411    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
412        Some(self)
413    }
414
415    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
416        Some(self)
417    }
418}
419
420impl DrawModifierNode for BackgroundNode {
421    fn draw(&self, _draw_scope: &mut dyn DrawScope) {
422        // Scene building consumes the retained background node directly.
423    }
424}
425
426/// Element that creates and updates background nodes.
427#[derive(Debug, Clone, PartialEq)]
428pub struct BackgroundElement {
429    color: Color,
430}
431
432impl BackgroundElement {
433    pub fn new(color: Color) -> Self {
434        Self { color }
435    }
436}
437
438impl Hash for BackgroundElement {
439    fn hash<H: Hasher>(&self, state: &mut H) {
440        hash_f32_value(state, self.color.0);
441        hash_f32_value(state, self.color.1);
442        hash_f32_value(state, self.color.2);
443        hash_f32_value(state, self.color.3);
444    }
445}
446
447impl ModifierNodeElement for BackgroundElement {
448    type Node = BackgroundNode;
449
450    fn create(&self) -> Self::Node {
451        BackgroundNode::new(self.color)
452    }
453
454    fn update(&self, node: &mut Self::Node) {
455        if node.color != self.color {
456            node.color = self.color;
457        }
458    }
459
460    fn capabilities(&self) -> NodeCapabilities {
461        NodeCapabilities::DRAW
462    }
463}
464
465// ============================================================================
466// Size Modifier Node
467// ============================================================================
468
469/// Node that tracks the latest rounded corner shape.
470#[derive(Debug)]
471pub struct CornerShapeNode {
472    shape: RoundedCornerShape,
473    state: NodeState,
474}
475
476impl CornerShapeNode {
477    pub fn new(shape: RoundedCornerShape) -> Self {
478        Self {
479            shape,
480            state: NodeState::new(),
481        }
482    }
483
484    pub fn shape(&self) -> RoundedCornerShape {
485        self.shape
486    }
487}
488
489impl DelegatableNode for CornerShapeNode {
490    fn node_state(&self) -> &NodeState {
491        &self.state
492    }
493}
494
495impl ModifierNode for CornerShapeNode {
496    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
497        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
498    }
499
500    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
501        Some(self)
502    }
503
504    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
505        Some(self)
506    }
507}
508
509impl DrawModifierNode for CornerShapeNode {
510    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
511}
512
513/// Element that creates and updates corner shape nodes.
514#[derive(Debug, Clone, PartialEq)]
515pub struct CornerShapeElement {
516    shape: RoundedCornerShape,
517}
518
519impl CornerShapeElement {
520    pub fn new(shape: RoundedCornerShape) -> Self {
521        Self { shape }
522    }
523}
524
525impl Hash for CornerShapeElement {
526    fn hash<H: Hasher>(&self, state: &mut H) {
527        let radii = self.shape.radii();
528        hash_f32_value(state, radii.top_left);
529        hash_f32_value(state, radii.top_right);
530        hash_f32_value(state, radii.bottom_right);
531        hash_f32_value(state, radii.bottom_left);
532    }
533}
534
535impl ModifierNodeElement for CornerShapeElement {
536    type Node = CornerShapeNode;
537
538    fn create(&self) -> Self::Node {
539        CornerShapeNode::new(self.shape)
540    }
541
542    fn update(&self, node: &mut Self::Node) {
543        if node.shape != self.shape {
544            node.shape = self.shape;
545        }
546    }
547
548    fn capabilities(&self) -> NodeCapabilities {
549        NodeCapabilities::DRAW
550    }
551}
552
553// ============================================================================
554// GraphicsLayer Modifier Node
555// ============================================================================
556
557/// Node that stores graphics layer state for resolved modifiers.
558pub struct GraphicsLayerNode {
559    layer: GraphicsLayer,
560    layer_resolver: Option<Rc<dyn Fn() -> GraphicsLayer>>,
561    lazy_scope_id: Option<usize>,
562    lazy_observer: Option<cranpose_core::SnapshotStateObserver>,
563    state: NodeState,
564}
565
566impl GraphicsLayerNode {
567    pub fn new(layer: GraphicsLayer) -> Self {
568        Self {
569            layer,
570            layer_resolver: None,
571            lazy_scope_id: None,
572            lazy_observer: None,
573            state: NodeState::new(),
574        }
575    }
576
577    pub fn new_lazy(layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) -> Self {
578        let mut node = Self {
579            layer: GraphicsLayer::default(),
580            layer_resolver: Some(layer_resolver),
581            lazy_scope_id: None,
582            lazy_observer: None,
583            state: NodeState::new(),
584        };
585        node.ensure_lazy_observation();
586        node.layer = node.layer();
587        node
588    }
589
590    pub fn layer(&self) -> GraphicsLayer {
591        if let Some(resolve) = self.layer_resolver() {
592            resolve()
593        } else {
594            self.layer.clone()
595        }
596    }
597
598    pub fn layer_snapshot(&self) -> GraphicsLayer {
599        self.layer.clone()
600    }
601
602    pub fn layer_resolver(&self) -> Option<Rc<dyn Fn() -> GraphicsLayer>> {
603        self.layer_resolver.as_ref().map(|resolve| {
604            let resolve = resolve.clone();
605            match (&self.lazy_observer, self.lazy_scope_id) {
606                (Some(observer), Some(scope_id)) => {
607                    let observer = observer.clone();
608                    Rc::new(move || {
609                        observer.observe_reads(
610                            scope_id,
611                            |_| crate::request_render_invalidation(),
612                            || resolve(),
613                        )
614                    }) as Rc<dyn Fn() -> GraphicsLayer>
615                }
616                _ => resolve,
617            }
618        })
619    }
620
621    fn set_static(&mut self, layer: GraphicsLayer) {
622        self.layer = layer;
623        self.layer_resolver = None;
624        self.clear_lazy_observation();
625    }
626
627    fn set_lazy(&mut self, layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) {
628        self.layer_resolver = Some(layer_resolver);
629        self.ensure_lazy_observation();
630        self.layer = self.layer();
631    }
632
633    fn ensure_lazy_observation(&mut self) {
634        if self.layer_resolver.is_none() {
635            self.clear_lazy_observation();
636            return;
637        }
638        if self.lazy_observer.is_some() {
639            return;
640        }
641        let observer = cranpose_core::SnapshotStateObserver::new(|callback| callback());
642        observer.start();
643        self.lazy_scope_id = Some(LAZY_GRAPHICS_LAYER_SCOPE_ID);
644        self.lazy_observer = Some(observer);
645    }
646
647    fn clear_lazy_observation(&mut self) {
648        if let Some(observer) = self.lazy_observer.take() {
649            observer.stop();
650        }
651        self.lazy_scope_id = None;
652    }
653}
654
655impl DelegatableNode for GraphicsLayerNode {
656    fn node_state(&self) -> &NodeState {
657        &self.state
658    }
659}
660
661impl ModifierNode for GraphicsLayerNode {
662    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
663        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
664    }
665
666    fn on_detach(&mut self) {
667        self.clear_lazy_observation();
668    }
669}
670
671impl std::fmt::Debug for GraphicsLayerNode {
672    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673        f.debug_struct("GraphicsLayerNode")
674            .field("layer", &self.layer)
675            .field("lazy", &self.layer_resolver.is_some())
676            .finish()
677    }
678}
679
680/// Element that creates and updates graphics layer nodes.
681#[derive(Debug, Clone, PartialEq)]
682pub struct GraphicsLayerElement {
683    layer: GraphicsLayer,
684}
685
686impl GraphicsLayerElement {
687    pub fn new(layer: GraphicsLayer) -> Self {
688        Self { layer }
689    }
690}
691
692impl Hash for GraphicsLayerElement {
693    fn hash<H: Hasher>(&self, state: &mut H) {
694        hash_graphics_layer(state, &self.layer);
695    }
696}
697
698impl ModifierNodeElement for GraphicsLayerElement {
699    type Node = GraphicsLayerNode;
700
701    fn create(&self) -> Self::Node {
702        GraphicsLayerNode::new(self.layer.clone())
703    }
704
705    fn update(&self, node: &mut Self::Node) {
706        node.set_static(self.layer.clone());
707    }
708
709    fn capabilities(&self) -> NodeCapabilities {
710        NodeCapabilities::DRAW
711    }
712}
713
714/// Element that evaluates a graphics layer lazily during render data collection.
715#[derive(Clone)]
716pub struct LazyGraphicsLayerElement {
717    layer_resolver: Rc<dyn Fn() -> GraphicsLayer>,
718}
719
720impl LazyGraphicsLayerElement {
721    pub fn new(layer_resolver: Rc<dyn Fn() -> GraphicsLayer>) -> Self {
722        Self { layer_resolver }
723    }
724}
725
726impl std::fmt::Debug for LazyGraphicsLayerElement {
727    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728        f.debug_struct("LazyGraphicsLayerElement")
729            .field("resolver", &"<closure>")
730            .finish()
731    }
732}
733
734impl PartialEq for LazyGraphicsLayerElement {
735    fn eq(&self, other: &Self) -> bool {
736        Rc::ptr_eq(&self.layer_resolver, &other.layer_resolver)
737    }
738}
739
740impl Eq for LazyGraphicsLayerElement {}
741
742impl Hash for LazyGraphicsLayerElement {
743    fn hash<H: Hasher>(&self, state: &mut H) {
744        let ptr = Rc::as_ptr(&self.layer_resolver) as *const ();
745        ptr.hash(state);
746    }
747}
748
749impl ModifierNodeElement for LazyGraphicsLayerElement {
750    type Node = GraphicsLayerNode;
751
752    fn create(&self) -> Self::Node {
753        GraphicsLayerNode::new_lazy(self.layer_resolver.clone())
754    }
755
756    fn update(&self, node: &mut Self::Node) {
757        node.set_lazy(self.layer_resolver.clone());
758    }
759
760    fn capabilities(&self) -> NodeCapabilities {
761        NodeCapabilities::DRAW
762    }
763
764    fn always_update(&self) -> bool {
765        true
766    }
767}
768
769// ============================================================================
770// Size Modifier Node
771// ============================================================================
772
773/// Node that enforces size constraints on its content.
774///
775/// Matches Kotlin: `SizeNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
776#[derive(Debug)]
777pub struct SizeNode {
778    min_width: Option<f32>,
779    max_width: Option<f32>,
780    min_height: Option<f32>,
781    max_height: Option<f32>,
782    enforce_incoming: bool,
783    state: NodeState,
784}
785
786impl SizeNode {
787    pub fn new(
788        min_width: Option<f32>,
789        max_width: Option<f32>,
790        min_height: Option<f32>,
791        max_height: Option<f32>,
792        enforce_incoming: bool,
793    ) -> Self {
794        Self {
795            min_width,
796            max_width,
797            min_height,
798            max_height,
799            enforce_incoming,
800            state: NodeState::new(),
801        }
802    }
803
804    /// Helper to build target constraints from element parameters
805    fn target_constraints(&self) -> Constraints {
806        let max_width = self.max_width.map(|v| v.max(0.0)).unwrap_or(f32::INFINITY);
807        let max_height = self.max_height.map(|v| v.max(0.0)).unwrap_or(f32::INFINITY);
808
809        let min_width = self
810            .min_width
811            .map(|v| {
812                let clamped = v.clamp(0.0, max_width);
813                if clamped == f32::INFINITY {
814                    0.0
815                } else {
816                    clamped
817                }
818            })
819            .unwrap_or(0.0);
820
821        let min_height = self
822            .min_height
823            .map(|v| {
824                let clamped = v.clamp(0.0, max_height);
825                if clamped == f32::INFINITY {
826                    0.0
827                } else {
828                    clamped
829                }
830            })
831            .unwrap_or(0.0);
832
833        Constraints {
834            min_width,
835            max_width,
836            min_height,
837            max_height,
838        }
839    }
840
841    pub fn min_width(&self) -> Option<f32> {
842        self.min_width
843    }
844
845    pub fn max_width(&self) -> Option<f32> {
846        self.max_width
847    }
848
849    pub fn min_height(&self) -> Option<f32> {
850        self.min_height
851    }
852
853    pub fn max_height(&self) -> Option<f32> {
854        self.max_height
855    }
856
857    pub fn enforce_incoming(&self) -> bool {
858        self.enforce_incoming
859    }
860}
861
862impl DelegatableNode for SizeNode {
863    fn node_state(&self) -> &NodeState {
864        &self.state
865    }
866}
867
868impl ModifierNode for SizeNode {
869    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
870        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
871    }
872
873    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
874        Some(self)
875    }
876
877    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
878        Some(self)
879    }
880}
881
882impl LayoutModifierNode for SizeNode {
883    fn measure(
884        &self,
885        _context: &mut dyn ModifierNodeContext,
886        measurable: &dyn Measurable,
887        constraints: Constraints,
888    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
889        let target = self.target_constraints();
890
891        let wrapped_constraints = if self.enforce_incoming {
892            // Constrain target constraints by incoming constraints
893            Constraints {
894                min_width: target
895                    .min_width
896                    .max(constraints.min_width)
897                    .min(constraints.max_width),
898                max_width: target
899                    .max_width
900                    .min(constraints.max_width)
901                    .max(constraints.min_width),
902                min_height: target
903                    .min_height
904                    .max(constraints.min_height)
905                    .min(constraints.max_height),
906                max_height: target
907                    .max_height
908                    .min(constraints.max_height)
909                    .max(constraints.min_height),
910            }
911        } else {
912            // Required size: use target, but preserve incoming if target is unspecified
913            let resolved_min_width = if self.min_width.is_some() {
914                target.min_width
915            } else {
916                constraints.min_width.min(target.max_width)
917            };
918            let resolved_max_width = if self.max_width.is_some() {
919                target.max_width
920            } else {
921                constraints.max_width.max(target.min_width)
922            };
923            let resolved_min_height = if self.min_height.is_some() {
924                target.min_height
925            } else {
926                constraints.min_height.min(target.max_height)
927            };
928            let resolved_max_height = if self.max_height.is_some() {
929                target.max_height
930            } else {
931                constraints.max_height.max(target.min_height)
932            };
933
934            Constraints {
935                min_width: resolved_min_width,
936                max_width: resolved_max_width,
937                min_height: resolved_min_height,
938                max_height: resolved_max_height,
939            }
940        };
941
942        let placeable = measurable.measure(wrapped_constraints);
943        let measured_width = placeable.width();
944        let measured_height = placeable.height();
945
946        // Return the target size when both min==max (fixed size), but only if it satisfies
947        // the wrapped constraints we passed down. Otherwise return measured size.
948        // This handles the case where enforce_incoming=true and incoming constraints are tighter.
949        let result_width = if self.min_width.is_some()
950            && self.max_width.is_some()
951            && self.min_width == self.max_width
952            && target.min_width >= wrapped_constraints.min_width
953            && target.min_width <= wrapped_constraints.max_width
954        {
955            target.min_width
956        } else {
957            measured_width
958        };
959
960        let result_height = if self.min_height.is_some()
961            && self.max_height.is_some()
962            && self.min_height == self.max_height
963            && target.min_height >= wrapped_constraints.min_height
964            && target.min_height <= wrapped_constraints.max_height
965        {
966            target.min_height
967        } else {
968            measured_height
969        };
970
971        // SizeNode doesn't offset placement - child is placed at (0, 0) relative to this node
972        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
973            width: result_width,
974            height: result_height,
975        })
976    }
977
978    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
979        let target = self.target_constraints();
980        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
981            target.max_width
982        } else {
983            let child_height = if self.enforce_incoming {
984                height
985            } else {
986                height.clamp(target.min_height, target.max_height)
987            };
988            measurable
989                .min_intrinsic_width(child_height)
990                .clamp(target.min_width, target.max_width)
991        }
992    }
993
994    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
995        let target = self.target_constraints();
996        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
997            target.max_width
998        } else {
999            let child_height = if self.enforce_incoming {
1000                height
1001            } else {
1002                height.clamp(target.min_height, target.max_height)
1003            };
1004            measurable
1005                .max_intrinsic_width(child_height)
1006                .clamp(target.min_width, target.max_width)
1007        }
1008    }
1009
1010    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1011        let target = self.target_constraints();
1012        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1013            target.max_height
1014        } else {
1015            let child_width = if self.enforce_incoming {
1016                width
1017            } else {
1018                width.clamp(target.min_width, target.max_width)
1019            };
1020            measurable
1021                .min_intrinsic_height(child_width)
1022                .clamp(target.min_height, target.max_height)
1023        }
1024    }
1025
1026    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1027        let target = self.target_constraints();
1028        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1029            target.max_height
1030        } else {
1031            let child_width = if self.enforce_incoming {
1032                width
1033            } else {
1034                width.clamp(target.min_width, target.max_width)
1035            };
1036            measurable
1037                .max_intrinsic_height(child_width)
1038                .clamp(target.min_height, target.max_height)
1039        }
1040    }
1041}
1042
1043/// Element that creates and updates size nodes.
1044///
1045/// Matches Kotlin: `SizeElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1046#[derive(Debug, Clone, PartialEq)]
1047pub struct SizeElement {
1048    min_width: Option<f32>,
1049    max_width: Option<f32>,
1050    min_height: Option<f32>,
1051    max_height: Option<f32>,
1052    enforce_incoming: bool,
1053}
1054
1055impl SizeElement {
1056    pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
1057        Self {
1058            min_width: width,
1059            max_width: width,
1060            min_height: height,
1061            max_height: height,
1062            enforce_incoming: true,
1063        }
1064    }
1065
1066    pub fn with_constraints(
1067        min_width: Option<f32>,
1068        max_width: Option<f32>,
1069        min_height: Option<f32>,
1070        max_height: Option<f32>,
1071        enforce_incoming: bool,
1072    ) -> Self {
1073        Self {
1074            min_width,
1075            max_width,
1076            min_height,
1077            max_height,
1078            enforce_incoming,
1079        }
1080    }
1081}
1082
1083impl Hash for SizeElement {
1084    fn hash<H: Hasher>(&self, state: &mut H) {
1085        hash_option_f32(state, self.min_width);
1086        hash_option_f32(state, self.max_width);
1087        hash_option_f32(state, self.min_height);
1088        hash_option_f32(state, self.max_height);
1089        self.enforce_incoming.hash(state);
1090    }
1091}
1092
1093impl ModifierNodeElement for SizeElement {
1094    type Node = SizeNode;
1095
1096    fn create(&self) -> Self::Node {
1097        SizeNode::new(
1098            self.min_width,
1099            self.max_width,
1100            self.min_height,
1101            self.max_height,
1102            self.enforce_incoming,
1103        )
1104    }
1105
1106    fn update(&self, node: &mut Self::Node) {
1107        if node.min_width != self.min_width
1108            || node.max_width != self.max_width
1109            || node.min_height != self.min_height
1110            || node.max_height != self.max_height
1111            || node.enforce_incoming != self.enforce_incoming
1112        {
1113            node.min_width = self.min_width;
1114            node.max_width = self.max_width;
1115            node.min_height = self.min_height;
1116            node.max_height = self.max_height;
1117            node.enforce_incoming = self.enforce_incoming;
1118        }
1119    }
1120
1121    fn capabilities(&self) -> NodeCapabilities {
1122        NodeCapabilities::LAYOUT
1123    }
1124}
1125
1126// ============================================================================
1127// Clickable Modifier Node
1128// ============================================================================
1129
1130/// Node that handles click/tap interactions.
1131// Drag threshold is now shared via cranpose_foundation::DRAG_THRESHOLD
1132use cranpose_foundation::DRAG_THRESHOLD;
1133
1134use std::cell::RefCell;
1135
1136// Press position is stored per-node via Rc<RefCell> for sharing with handler closure
1137// Node reuse is ensured by ClickableElement implementing key() to return a stable key
1138// The handler closure is cached to ensure the same closure (and press_position state) is returned
1139
1140pub struct ClickableNode {
1141    on_click: Rc<dyn Fn(Point)>,
1142    state: NodeState,
1143    /// Shared press position for drag detection (per-node state, accessible by handler closure)
1144    press_position: Rc<RefCell<Option<Point>>>,
1145    /// Cached handler closure - created once, returned on every pointer_input_handler() call
1146    cached_handler: Rc<dyn Fn(PointerEvent)>,
1147}
1148
1149impl std::fmt::Debug for ClickableNode {
1150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151        f.debug_struct("ClickableNode").finish()
1152    }
1153}
1154
1155impl ClickableNode {
1156    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1157        Self::with_handler(Rc::new(on_click))
1158    }
1159
1160    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1161        let press_position = Rc::new(RefCell::new(None));
1162        let cached_handler = Self::create_handler(on_click.clone(), press_position.clone());
1163        Self {
1164            on_click,
1165            state: NodeState::new(),
1166            press_position,
1167            cached_handler,
1168        }
1169    }
1170
1171    fn create_handler(
1172        handler: Rc<dyn Fn(Point)>,
1173        press_position: Rc<RefCell<Option<Point>>>,
1174    ) -> Rc<dyn Fn(PointerEvent)> {
1175        Rc::new(move |event: PointerEvent| {
1176            // Check if event was consumed by scroll or other gesture handlers
1177            if event.is_consumed() {
1178                // Clear press state if event was consumed
1179                *press_position.borrow_mut() = None;
1180                return;
1181            }
1182
1183            match event.kind {
1184                PointerEventKind::Down => {
1185                    // Store global press position for drag detection on Up
1186                    *press_position.borrow_mut() = Some(Point {
1187                        x: event.global_position.x,
1188                        y: event.global_position.y,
1189                    });
1190                }
1191                PointerEventKind::Move => {
1192                    // Move events are tracked via press_position for drag detection
1193                }
1194                PointerEventKind::Up => {
1195                    // Check if this is a click (Up near Down) or a drag (Up far from Down)
1196                    let press_pos_value = *press_position.borrow();
1197
1198                    let should_click = if let Some(press_pos) = press_pos_value {
1199                        let dx = event.global_position.x - press_pos.x;
1200                        let dy = event.global_position.y - press_pos.y;
1201                        let distance = (dx * dx + dy * dy).sqrt();
1202                        distance <= DRAG_THRESHOLD
1203                    } else {
1204                        // No Down was tracked - fire click anyway
1205                        // This preserves the original behavior for cases where Down
1206                        // was handled by a different mechanism
1207                        true
1208                    };
1209
1210                    // Reset press position
1211                    *press_position.borrow_mut() = None;
1212
1213                    if should_click {
1214                        handler(Point {
1215                            x: event.position.x,
1216                            y: event.position.y,
1217                        });
1218                        event.consume();
1219                    }
1220                }
1221                PointerEventKind::Cancel => {
1222                    // Clear press state on cancel
1223                    *press_position.borrow_mut() = None;
1224                }
1225                PointerEventKind::Scroll | PointerEventKind::Enter | 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
1817// ============================================================================
1818// Fill Modifier Node
1819// ============================================================================
1820
1821/// Direction for fill modifiers (horizontal, vertical, or both).
1822#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1823pub enum FillDirection {
1824    Horizontal,
1825    Vertical,
1826    Both,
1827}
1828
1829/// Node that fills the maximum available space in one or both dimensions.
1830///
1831/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1832#[derive(Debug)]
1833pub struct FillNode {
1834    direction: FillDirection,
1835    fraction: f32,
1836    state: NodeState,
1837}
1838
1839impl FillNode {
1840    pub fn new(direction: FillDirection, fraction: f32) -> Self {
1841        Self {
1842            direction,
1843            fraction,
1844            state: NodeState::new(),
1845        }
1846    }
1847
1848    pub fn direction(&self) -> FillDirection {
1849        self.direction
1850    }
1851
1852    pub fn fraction(&self) -> f32 {
1853        self.fraction
1854    }
1855}
1856
1857impl DelegatableNode for FillNode {
1858    fn node_state(&self) -> &NodeState {
1859        &self.state
1860    }
1861}
1862
1863impl ModifierNode for FillNode {
1864    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1865        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
1866    }
1867
1868    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1869        Some(self)
1870    }
1871
1872    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1873        Some(self)
1874    }
1875}
1876
1877impl LayoutModifierNode for FillNode {
1878    fn measure(
1879        &self,
1880        _context: &mut dyn ModifierNodeContext,
1881        measurable: &dyn Measurable,
1882        constraints: Constraints,
1883    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1884        // Calculate the fill size based on constraints
1885        let (fill_width, child_min_width, child_max_width) = if self.direction
1886            != FillDirection::Vertical
1887            && constraints.max_width != f32::INFINITY
1888        {
1889            let width = (constraints.max_width * self.fraction)
1890                .round()
1891                .clamp(constraints.min_width, constraints.max_width);
1892            // Tight constraint for child on this axis
1893            (width, width, width)
1894        } else {
1895            (
1896                constraints.max_width,
1897                constraints.min_width,
1898                constraints.max_width,
1899            )
1900        };
1901
1902        let (fill_height, child_min_height, child_max_height) = if self.direction
1903            != FillDirection::Horizontal
1904            && constraints.max_height != f32::INFINITY
1905        {
1906            let height = (constraints.max_height * self.fraction)
1907                .round()
1908                .clamp(constraints.min_height, constraints.max_height);
1909            // Tight constraint for child on this axis
1910            (height, height, height)
1911        } else {
1912            (
1913                constraints.max_height,
1914                constraints.min_height,
1915                constraints.max_height,
1916            )
1917        };
1918
1919        let fill_constraints = Constraints {
1920            min_width: child_min_width,
1921            max_width: child_max_width,
1922            min_height: child_min_height,
1923            max_height: child_max_height,
1924        };
1925
1926        let placeable = measurable.measure(fill_constraints);
1927
1928        // Return the FILL size, not the child size.
1929        // The child is measured within tight constraints on the fill axis,
1930        // but we report the fill size to our parent.
1931        let result_width = if self.direction != FillDirection::Vertical
1932            && constraints.max_width != f32::INFINITY
1933        {
1934            fill_width
1935        } else {
1936            placeable.width()
1937        };
1938
1939        let result_height = if self.direction != FillDirection::Horizontal
1940            && constraints.max_height != f32::INFINITY
1941        {
1942            fill_height
1943        } else {
1944            placeable.height()
1945        };
1946
1947        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
1948            width: result_width,
1949            height: result_height,
1950        })
1951    }
1952
1953    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1954        measurable.min_intrinsic_width(height)
1955    }
1956
1957    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1958        measurable.max_intrinsic_width(height)
1959    }
1960
1961    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1962        measurable.min_intrinsic_height(width)
1963    }
1964
1965    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1966        measurable.max_intrinsic_height(width)
1967    }
1968}
1969
1970/// Element that creates and updates fill nodes.
1971///
1972/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1973#[derive(Debug, Clone, PartialEq)]
1974pub struct FillElement {
1975    direction: FillDirection,
1976    fraction: f32,
1977}
1978
1979impl FillElement {
1980    pub fn width(fraction: f32) -> Self {
1981        Self {
1982            direction: FillDirection::Horizontal,
1983            fraction,
1984        }
1985    }
1986
1987    pub fn height(fraction: f32) -> Self {
1988        Self {
1989            direction: FillDirection::Vertical,
1990            fraction,
1991        }
1992    }
1993
1994    pub fn size(fraction: f32) -> Self {
1995        Self {
1996            direction: FillDirection::Both,
1997            fraction,
1998        }
1999    }
2000}
2001
2002impl Hash for FillElement {
2003    fn hash<H: Hasher>(&self, state: &mut H) {
2004        self.direction.hash(state);
2005        hash_f32_value(state, self.fraction);
2006    }
2007}
2008
2009impl ModifierNodeElement for FillElement {
2010    type Node = FillNode;
2011
2012    fn create(&self) -> Self::Node {
2013        FillNode::new(self.direction, self.fraction)
2014    }
2015
2016    fn update(&self, node: &mut Self::Node) {
2017        if node.direction != self.direction || node.fraction != self.fraction {
2018            node.direction = self.direction;
2019            node.fraction = self.fraction;
2020        }
2021    }
2022
2023    fn capabilities(&self) -> NodeCapabilities {
2024        NodeCapabilities::LAYOUT
2025    }
2026}
2027
2028// ============================================================================
2029// Weight Modifier Node
2030// ============================================================================
2031
2032/// Node that records flex weight data for Row/Column parents.
2033#[derive(Debug)]
2034pub struct WeightNode {
2035    weight: f32,
2036    fill: bool,
2037    state: NodeState,
2038}
2039
2040impl WeightNode {
2041    pub fn new(weight: f32, fill: bool) -> Self {
2042        Self {
2043            weight,
2044            fill,
2045            state: NodeState::new(),
2046        }
2047    }
2048
2049    pub fn layout_weight(&self) -> LayoutWeight {
2050        LayoutWeight {
2051            weight: self.weight,
2052            fill: self.fill,
2053        }
2054    }
2055}
2056
2057impl DelegatableNode for WeightNode {
2058    fn node_state(&self) -> &NodeState {
2059        &self.state
2060    }
2061}
2062
2063impl ModifierNode for WeightNode {
2064    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2065        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2066    }
2067}
2068
2069/// Element that creates and updates weight nodes.
2070#[derive(Debug, Clone, PartialEq)]
2071pub struct WeightElement {
2072    weight: f32,
2073    fill: bool,
2074}
2075
2076impl WeightElement {
2077    pub fn new(weight: f32, fill: bool) -> Self {
2078        Self { weight, fill }
2079    }
2080}
2081
2082impl Hash for WeightElement {
2083    fn hash<H: Hasher>(&self, state: &mut H) {
2084        hash_f32_value(state, self.weight);
2085        self.fill.hash(state);
2086    }
2087}
2088
2089impl ModifierNodeElement for WeightElement {
2090    type Node = WeightNode;
2091
2092    fn create(&self) -> Self::Node {
2093        WeightNode::new(self.weight, self.fill)
2094    }
2095
2096    fn update(&self, node: &mut Self::Node) {
2097        if node.weight != self.weight || node.fill != self.fill {
2098            node.weight = self.weight;
2099            node.fill = self.fill;
2100        }
2101    }
2102
2103    fn capabilities(&self) -> NodeCapabilities {
2104        NodeCapabilities::LAYOUT
2105    }
2106}
2107
2108// ============================================================================
2109// Alignment Modifier Node
2110// ============================================================================
2111
2112/// Node that records alignment preferences for Box/Row/Column scopes.
2113#[derive(Debug)]
2114pub struct AlignmentNode {
2115    box_alignment: Option<Alignment>,
2116    column_alignment: Option<HorizontalAlignment>,
2117    row_alignment: Option<VerticalAlignment>,
2118    state: NodeState,
2119}
2120
2121impl AlignmentNode {
2122    pub fn new(
2123        box_alignment: Option<Alignment>,
2124        column_alignment: Option<HorizontalAlignment>,
2125        row_alignment: Option<VerticalAlignment>,
2126    ) -> Self {
2127        Self {
2128            box_alignment,
2129            column_alignment,
2130            row_alignment,
2131            state: NodeState::new(),
2132        }
2133    }
2134
2135    pub fn box_alignment(&self) -> Option<Alignment> {
2136        self.box_alignment
2137    }
2138
2139    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2140        self.column_alignment
2141    }
2142
2143    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2144        self.row_alignment
2145    }
2146}
2147
2148impl DelegatableNode for AlignmentNode {
2149    fn node_state(&self) -> &NodeState {
2150        &self.state
2151    }
2152}
2153
2154impl ModifierNode for AlignmentNode {
2155    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2156        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2157    }
2158}
2159
2160/// Element that creates and updates alignment nodes.
2161#[derive(Debug, Clone, PartialEq)]
2162pub struct AlignmentElement {
2163    box_alignment: Option<Alignment>,
2164    column_alignment: Option<HorizontalAlignment>,
2165    row_alignment: Option<VerticalAlignment>,
2166}
2167
2168impl AlignmentElement {
2169    pub fn box_alignment(alignment: Alignment) -> Self {
2170        Self {
2171            box_alignment: Some(alignment),
2172            column_alignment: None,
2173            row_alignment: None,
2174        }
2175    }
2176
2177    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2178        Self {
2179            box_alignment: None,
2180            column_alignment: Some(alignment),
2181            row_alignment: None,
2182        }
2183    }
2184
2185    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2186        Self {
2187            box_alignment: None,
2188            column_alignment: None,
2189            row_alignment: Some(alignment),
2190        }
2191    }
2192}
2193
2194impl Hash for AlignmentElement {
2195    fn hash<H: Hasher>(&self, state: &mut H) {
2196        if let Some(alignment) = self.box_alignment {
2197            state.write_u8(1);
2198            hash_alignment(state, alignment);
2199        } else {
2200            state.write_u8(0);
2201        }
2202        if let Some(alignment) = self.column_alignment {
2203            state.write_u8(1);
2204            hash_horizontal_alignment(state, alignment);
2205        } else {
2206            state.write_u8(0);
2207        }
2208        if let Some(alignment) = self.row_alignment {
2209            state.write_u8(1);
2210            hash_vertical_alignment(state, alignment);
2211        } else {
2212            state.write_u8(0);
2213        }
2214    }
2215}
2216
2217impl ModifierNodeElement for AlignmentElement {
2218    type Node = AlignmentNode;
2219
2220    fn create(&self) -> Self::Node {
2221        AlignmentNode::new(
2222            self.box_alignment,
2223            self.column_alignment,
2224            self.row_alignment,
2225        )
2226    }
2227
2228    fn update(&self, node: &mut Self::Node) {
2229        if node.box_alignment != self.box_alignment {
2230            node.box_alignment = self.box_alignment;
2231        }
2232        if node.column_alignment != self.column_alignment {
2233            node.column_alignment = self.column_alignment;
2234        }
2235        if node.row_alignment != self.row_alignment {
2236            node.row_alignment = self.row_alignment;
2237        }
2238    }
2239
2240    fn capabilities(&self) -> NodeCapabilities {
2241        NodeCapabilities::LAYOUT
2242    }
2243}
2244
2245// ============================================================================
2246// Intrinsic Size Modifier Node
2247// ============================================================================
2248
2249#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2250pub enum IntrinsicAxis {
2251    Width,
2252    Height,
2253}
2254
2255/// Node that records intrinsic sizing requests.
2256#[derive(Debug)]
2257pub struct IntrinsicSizeNode {
2258    axis: IntrinsicAxis,
2259    size: IntrinsicSize,
2260    state: NodeState,
2261}
2262
2263impl IntrinsicSizeNode {
2264    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2265        Self {
2266            axis,
2267            size,
2268            state: NodeState::new(),
2269        }
2270    }
2271
2272    pub fn axis(&self) -> IntrinsicAxis {
2273        self.axis
2274    }
2275
2276    pub fn intrinsic_size(&self) -> IntrinsicSize {
2277        self.size
2278    }
2279}
2280
2281impl DelegatableNode for IntrinsicSizeNode {
2282    fn node_state(&self) -> &NodeState {
2283        &self.state
2284    }
2285}
2286
2287impl ModifierNode for IntrinsicSizeNode {
2288    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2289        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2290    }
2291}
2292
2293/// Element that creates and updates intrinsic size nodes.
2294#[derive(Debug, Clone, PartialEq)]
2295pub struct IntrinsicSizeElement {
2296    axis: IntrinsicAxis,
2297    size: IntrinsicSize,
2298}
2299
2300impl IntrinsicSizeElement {
2301    pub fn width(size: IntrinsicSize) -> Self {
2302        Self {
2303            axis: IntrinsicAxis::Width,
2304            size,
2305        }
2306    }
2307
2308    pub fn height(size: IntrinsicSize) -> Self {
2309        Self {
2310            axis: IntrinsicAxis::Height,
2311            size,
2312        }
2313    }
2314}
2315
2316impl Hash for IntrinsicSizeElement {
2317    fn hash<H: Hasher>(&self, state: &mut H) {
2318        state.write_u8(match self.axis {
2319            IntrinsicAxis::Width => 0,
2320            IntrinsicAxis::Height => 1,
2321        });
2322        state.write_u8(match self.size {
2323            IntrinsicSize::Min => 0,
2324            IntrinsicSize::Max => 1,
2325        });
2326    }
2327}
2328
2329impl ModifierNodeElement for IntrinsicSizeElement {
2330    type Node = IntrinsicSizeNode;
2331
2332    fn create(&self) -> Self::Node {
2333        IntrinsicSizeNode::new(self.axis, self.size)
2334    }
2335
2336    fn update(&self, node: &mut Self::Node) {
2337        if node.axis != self.axis {
2338            node.axis = self.axis;
2339        }
2340        if node.size != self.size {
2341            node.size = self.size;
2342        }
2343    }
2344
2345    fn capabilities(&self) -> NodeCapabilities {
2346        NodeCapabilities::LAYOUT
2347    }
2348}
2349
2350#[cfg(test)]
2351#[path = "tests/modifier_nodes_tests.rs"]
2352mod tests;