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            // Check if event was consumed by scroll or other gesture handlers
1168            if event.is_consumed() {
1169                // Clear press state if event was consumed
1170                *press_position.borrow_mut() = None;
1171                return;
1172            }
1173
1174            match event.kind {
1175                PointerEventKind::Down => {
1176                    // Store global press position for drag detection on Up
1177                    *press_position.borrow_mut() = Some(Point {
1178                        x: event.global_position.x,
1179                        y: event.global_position.y,
1180                    });
1181                }
1182                PointerEventKind::Move => {
1183                    // Move events are tracked via press_position for drag detection
1184                }
1185                PointerEventKind::Up => {
1186                    // Check if this is a click (Up near Down) or a drag (Up far from Down)
1187                    let press_pos_value = *press_position.borrow();
1188
1189                    let should_click = if let Some(press_pos) = press_pos_value {
1190                        let dx = event.global_position.x - press_pos.x;
1191                        let dy = event.global_position.y - press_pos.y;
1192                        let distance = (dx * dx + dy * dy).sqrt();
1193                        distance <= DRAG_THRESHOLD
1194                    } else {
1195                        // No Down was tracked - fire click anyway
1196                        // This preserves the original behavior for cases where Down
1197                        // was handled by a different mechanism
1198                        true
1199                    };
1200
1201                    // Reset press position
1202                    *press_position.borrow_mut() = None;
1203
1204                    if should_click {
1205                        handler(Point {
1206                            x: event.position.x,
1207                            y: event.position.y,
1208                        });
1209                        event.consume();
1210                    }
1211                }
1212                PointerEventKind::Cancel => {
1213                    // Clear press state on cancel
1214                    *press_position.borrow_mut() = None;
1215                }
1216                PointerEventKind::Scroll | PointerEventKind::Enter | PointerEventKind::Exit => {
1217                    // These events don't affect click press state.
1218                }
1219            }
1220        })
1221    }
1222
1223    pub fn handler(&self) -> Rc<dyn Fn(Point)> {
1224        self.on_click.clone()
1225    }
1226}
1227
1228impl DelegatableNode for ClickableNode {
1229    fn node_state(&self) -> &NodeState {
1230        &self.state
1231    }
1232}
1233
1234impl ModifierNode for ClickableNode {
1235    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1236        context.invalidate(cranpose_foundation::InvalidationKind::PointerInput);
1237    }
1238
1239    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1240        Some(self)
1241    }
1242
1243    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1244        Some(self)
1245    }
1246}
1247
1248impl PointerInputNode for ClickableNode {
1249    fn on_pointer_event(
1250        &mut self,
1251        _context: &mut dyn ModifierNodeContext,
1252        event: &PointerEvent,
1253    ) -> bool {
1254        // Delegate to the cached handler - single source of truth for click logic
1255        // This avoids duplicating the press position tracking and threshold checking
1256        (self.cached_handler)(event.clone());
1257        event.is_consumed()
1258    }
1259
1260    fn hit_test(&self, _x: f32, _y: f32) -> bool {
1261        // Always participate in hit testing
1262        true
1263    }
1264
1265    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1266        // Return the cached handler - this ensures the same closure (with its press_position state)
1267        // is used across multiple calls to pointer_input_handler()
1268        Some(self.cached_handler.clone())
1269    }
1270}
1271
1272/// Element that creates and updates clickable nodes.
1273#[derive(Clone)]
1274pub struct ClickableElement {
1275    on_click: Rc<dyn Fn(Point)>,
1276}
1277
1278impl ClickableElement {
1279    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1280        Self {
1281            on_click: Rc::new(on_click),
1282        }
1283    }
1284
1285    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1286        Self { on_click }
1287    }
1288}
1289
1290impl std::fmt::Debug for ClickableElement {
1291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1292        f.debug_struct("ClickableElement").finish()
1293    }
1294}
1295
1296impl PartialEq for ClickableElement {
1297    fn eq(&self, _other: &Self) -> bool {
1298        // Type matching is sufficient - node will be updated via update() method
1299        // This matches JC behavior where nodes are reused for same-type elements,
1300        // preserving press_position state for proper drag detection
1301        true
1302    }
1303}
1304
1305impl Eq for ClickableElement {}
1306
1307impl Hash for ClickableElement {
1308    fn hash<H: Hasher>(&self, state: &mut H) {
1309        // Consistent hash for type-based matching
1310        "clickable".hash(state);
1311    }
1312}
1313
1314impl ModifierNodeElement for ClickableElement {
1315    type Node = ClickableNode;
1316
1317    fn create(&self) -> Self::Node {
1318        ClickableNode::with_handler(self.on_click.clone())
1319    }
1320
1321    // Note: key() is deliberately NOT implemented (returns None by default)
1322    // This enables type-based node reuse: the same ClickableNode instance is
1323    // reused across recompositions, preserving the cached_handler and its
1324    // captured press_position state for proper drag detection.
1325
1326    fn update(&self, node: &mut Self::Node) {
1327        // Update the handler - the cached_handler needs to be recreated
1328        // with the new on_click while preserving press_position
1329        node.on_click = self.on_click.clone();
1330        // Recreate the cached handler with the same press_position but new click handler
1331        node.cached_handler =
1332            ClickableNode::create_handler(node.on_click.clone(), node.press_position.clone());
1333    }
1334
1335    fn capabilities(&self) -> NodeCapabilities {
1336        NodeCapabilities::POINTER_INPUT
1337    }
1338
1339    fn always_update(&self) -> bool {
1340        // Always update to capture new closure while preserving node state
1341        true
1342    }
1343}
1344
1345// ============================================================================
1346// Alpha Modifier Node
1347// ============================================================================
1348
1349/// Node that applies alpha transparency to its content.
1350#[derive(Debug)]
1351pub struct AlphaNode {
1352    alpha: f32,
1353    state: NodeState,
1354}
1355
1356impl AlphaNode {
1357    pub fn new(alpha: f32) -> Self {
1358        Self {
1359            alpha: alpha.clamp(0.0, 1.0),
1360            state: NodeState::new(),
1361        }
1362    }
1363}
1364
1365impl DelegatableNode for AlphaNode {
1366    fn node_state(&self) -> &NodeState {
1367        &self.state
1368    }
1369}
1370
1371impl ModifierNode for AlphaNode {
1372    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1373        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1374    }
1375
1376    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1377        Some(self)
1378    }
1379
1380    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1381        Some(self)
1382    }
1383}
1384
1385impl DrawModifierNode for AlphaNode {
1386    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1387}
1388
1389/// Element that creates and updates alpha nodes.
1390#[derive(Debug, Clone, PartialEq)]
1391pub struct AlphaElement {
1392    alpha: f32,
1393}
1394
1395impl AlphaElement {
1396    pub fn new(alpha: f32) -> Self {
1397        Self {
1398            alpha: alpha.clamp(0.0, 1.0),
1399        }
1400    }
1401}
1402
1403impl Hash for AlphaElement {
1404    fn hash<H: Hasher>(&self, state: &mut H) {
1405        hash_f32_value(state, self.alpha);
1406    }
1407}
1408
1409impl ModifierNodeElement for AlphaElement {
1410    type Node = AlphaNode;
1411
1412    fn create(&self) -> Self::Node {
1413        AlphaNode::new(self.alpha)
1414    }
1415
1416    fn update(&self, node: &mut Self::Node) {
1417        let new_alpha = self.alpha.clamp(0.0, 1.0);
1418        if (node.alpha - new_alpha).abs() > f32::EPSILON {
1419            node.alpha = new_alpha;
1420        }
1421    }
1422
1423    fn capabilities(&self) -> NodeCapabilities {
1424        NodeCapabilities::DRAW
1425    }
1426}
1427
1428// ============================================================================
1429// Clip-To-Bounds Modifier Node
1430// ============================================================================
1431
1432/// Node that marks the subtree for clipping during rendering.
1433#[derive(Debug)]
1434pub struct ClipToBoundsNode {
1435    state: NodeState,
1436}
1437
1438impl ClipToBoundsNode {
1439    pub fn new() -> Self {
1440        Self {
1441            state: NodeState::new(),
1442        }
1443    }
1444}
1445
1446impl DelegatableNode for ClipToBoundsNode {
1447    fn node_state(&self) -> &NodeState {
1448        &self.state
1449    }
1450}
1451
1452impl ModifierNode for ClipToBoundsNode {
1453    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1454        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1455    }
1456
1457    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1458        Some(self)
1459    }
1460
1461    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1462        Some(self)
1463    }
1464}
1465
1466impl DrawModifierNode for ClipToBoundsNode {
1467    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1468}
1469
1470/// Element that creates clip-to-bounds nodes.
1471#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1472pub struct ClipToBoundsElement;
1473
1474impl ClipToBoundsElement {
1475    pub fn new() -> Self {
1476        Self
1477    }
1478}
1479
1480impl ModifierNodeElement for ClipToBoundsElement {
1481    type Node = ClipToBoundsNode;
1482
1483    fn create(&self) -> Self::Node {
1484        ClipToBoundsNode::new()
1485    }
1486
1487    fn update(&self, _node: &mut Self::Node) {}
1488
1489    fn capabilities(&self) -> NodeCapabilities {
1490        NodeCapabilities::DRAW
1491    }
1492}
1493
1494// ============================================================================
1495// Draw Command Modifier Node
1496// ============================================================================
1497
1498/// Node that stores draw commands emitted by draw modifiers.
1499pub struct DrawCommandNode {
1500    commands: Vec<DrawCommand>,
1501    node_id: Cell<Option<NodeId>>,
1502    state: NodeState,
1503}
1504
1505impl DrawCommandNode {
1506    pub fn new(commands: Vec<DrawCommand>) -> Self {
1507        Self {
1508            commands,
1509            node_id: Cell::new(None),
1510            state: NodeState::new(),
1511        }
1512    }
1513
1514    #[cfg(test)]
1515    pub fn commands(&self) -> &[DrawCommand] {
1516        &self.commands
1517    }
1518
1519    pub(crate) fn observed_commands(&self) -> Vec<DrawCommand> {
1520        let node_id = self.node_id.get();
1521        self.commands
1522            .iter()
1523            .cloned()
1524            .enumerate()
1525            .map(|(index, command)| observe_draw_command(command, node_id, index))
1526            .collect()
1527    }
1528}
1529
1530impl DelegatableNode for DrawCommandNode {
1531    fn node_state(&self) -> &NodeState {
1532        &self.state
1533    }
1534}
1535
1536impl ModifierNode for DrawCommandNode {
1537    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1538        self.node_id.set(context.node_id());
1539        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1540    }
1541
1542    fn on_detach(&mut self) {
1543        if let Some(node_id) = self.node_id.replace(None) {
1544            crate::render_state::clear_draw_observations_for_node(node_id);
1545        }
1546    }
1547
1548    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1549        Some(self)
1550    }
1551
1552    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1553        Some(self)
1554    }
1555}
1556
1557impl DrawModifierNode for DrawCommandNode {
1558    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1559}
1560
1561fn observe_draw_command(
1562    command: DrawCommand,
1563    node_id: Option<NodeId>,
1564    command_index: usize,
1565) -> DrawCommand {
1566    let Some(node_id) = node_id else {
1567        return command;
1568    };
1569    let scope = crate::render_state::DrawObservationScope::new(node_id, command_index);
1570    match command {
1571        DrawCommand::Behind(draw) => DrawCommand::Behind(Rc::new(move |size| {
1572            crate::render_state::observe_draw_reads(scope, || draw(size))
1573        })),
1574        DrawCommand::WithContent(draw) => DrawCommand::WithContent(Rc::new(move |size| {
1575            crate::render_state::observe_draw_reads(scope, || draw(size))
1576        })),
1577        DrawCommand::Overlay(draw) => DrawCommand::Overlay(Rc::new(move |size| {
1578            crate::render_state::observe_draw_reads(scope, || draw(size))
1579        })),
1580    }
1581}
1582
1583fn draw_command_tag(cmd: &DrawCommand) -> u8 {
1584    match cmd {
1585        DrawCommand::Behind(_) => 0,
1586        DrawCommand::WithContent(_) => 1,
1587        DrawCommand::Overlay(_) => 2,
1588    }
1589}
1590
1591fn draw_command_closure_identity(cmd: &DrawCommand) -> *const () {
1592    match cmd {
1593        DrawCommand::Behind(f) | DrawCommand::WithContent(f) | DrawCommand::Overlay(f) => {
1594            Rc::as_ptr(f) as *const ()
1595        }
1596    }
1597}
1598
1599/// Element that wires draw commands into the modifier node chain.
1600#[derive(Clone)]
1601pub struct DrawCommandElement {
1602    commands: Vec<DrawCommand>,
1603}
1604
1605impl DrawCommandElement {
1606    pub fn new(command: DrawCommand) -> Self {
1607        Self {
1608            commands: vec![command],
1609        }
1610    }
1611
1612    pub fn from_commands(commands: Vec<DrawCommand>) -> Self {
1613        Self { commands }
1614    }
1615}
1616
1617impl std::fmt::Debug for DrawCommandElement {
1618    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1619        f.debug_struct("DrawCommandElement")
1620            .field("commands", &self.commands.len())
1621            .finish()
1622    }
1623}
1624
1625impl PartialEq for DrawCommandElement {
1626    fn eq(&self, other: &Self) -> bool {
1627        if self.commands.len() != other.commands.len() {
1628            return false;
1629        }
1630        self.commands
1631            .iter()
1632            .zip(other.commands.iter())
1633            .all(|(a, b)| {
1634                draw_command_tag(a) == draw_command_tag(b)
1635                    && draw_command_closure_identity(a) == draw_command_closure_identity(b)
1636            })
1637    }
1638}
1639
1640impl Eq for DrawCommandElement {}
1641
1642impl std::hash::Hash for DrawCommandElement {
1643    fn hash<H: Hasher>(&self, state: &mut H) {
1644        "draw_commands".hash(state);
1645        self.commands.len().hash(state);
1646        for command in &self.commands {
1647            draw_command_tag(command).hash(state);
1648            (draw_command_closure_identity(command) as usize).hash(state);
1649        }
1650    }
1651}
1652
1653impl ModifierNodeElement for DrawCommandElement {
1654    type Node = DrawCommandNode;
1655
1656    fn create(&self) -> Self::Node {
1657        DrawCommandNode::new(self.commands.clone())
1658    }
1659
1660    fn update(&self, node: &mut Self::Node) {
1661        node.commands = self.commands.clone();
1662    }
1663
1664    fn capabilities(&self) -> NodeCapabilities {
1665        NodeCapabilities::DRAW
1666    }
1667}
1668
1669// ============================================================================
1670// Offset Modifier Node
1671// ============================================================================
1672
1673/// Node that offsets its content by a fixed (x, y) amount.
1674///
1675/// Matches Kotlin: `OffsetNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
1676#[derive(Debug)]
1677pub struct OffsetNode {
1678    x: f32,
1679    y: f32,
1680    rtl_aware: bool,
1681    state: NodeState,
1682}
1683
1684impl OffsetNode {
1685    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
1686        Self {
1687            x,
1688            y,
1689            rtl_aware,
1690            state: NodeState::new(),
1691        }
1692    }
1693
1694    pub fn offset(&self) -> Point {
1695        Point {
1696            x: self.x,
1697            y: self.y,
1698        }
1699    }
1700
1701    pub fn rtl_aware(&self) -> bool {
1702        self.rtl_aware
1703    }
1704}
1705
1706impl DelegatableNode for OffsetNode {
1707    fn node_state(&self) -> &NodeState {
1708        &self.state
1709    }
1710}
1711
1712impl ModifierNode for OffsetNode {
1713    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1714        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
1715    }
1716
1717    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1718        Some(self)
1719    }
1720
1721    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1722        Some(self)
1723    }
1724}
1725
1726impl LayoutModifierNode for OffsetNode {
1727    fn measure(
1728        &self,
1729        _context: &mut dyn ModifierNodeContext,
1730        measurable: &dyn Measurable,
1731        constraints: Constraints,
1732    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1733        // Offset doesn't affect measurement, just placement
1734        let placeable = measurable.measure(constraints);
1735
1736        // Return child size unchanged, but specify the offset for placement
1737        cranpose_ui_layout::LayoutModifierMeasureResult::new(
1738            Size {
1739                width: placeable.width(),
1740                height: placeable.height(),
1741            },
1742            self.x, // Place child offset by x
1743            self.y, // Place child offset by y
1744        )
1745    }
1746
1747    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1748        measurable.min_intrinsic_width(height)
1749    }
1750
1751    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1752        measurable.max_intrinsic_width(height)
1753    }
1754
1755    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1756        measurable.min_intrinsic_height(width)
1757    }
1758
1759    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1760        measurable.max_intrinsic_height(width)
1761    }
1762}
1763
1764/// Element that creates and updates offset nodes.
1765///
1766/// Matches Kotlin: `OffsetElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
1767#[derive(Debug, Clone, PartialEq)]
1768pub struct OffsetElement {
1769    x: f32,
1770    y: f32,
1771    rtl_aware: bool,
1772}
1773
1774impl OffsetElement {
1775    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
1776        Self { x, y, rtl_aware }
1777    }
1778}
1779
1780impl Hash for OffsetElement {
1781    fn hash<H: Hasher>(&self, state: &mut H) {
1782        hash_f32_value(state, self.x);
1783        hash_f32_value(state, self.y);
1784        self.rtl_aware.hash(state);
1785    }
1786}
1787
1788impl ModifierNodeElement for OffsetElement {
1789    type Node = OffsetNode;
1790
1791    fn create(&self) -> Self::Node {
1792        OffsetNode::new(self.x, self.y, self.rtl_aware)
1793    }
1794
1795    fn update(&self, node: &mut Self::Node) {
1796        if node.x != self.x || node.y != self.y || node.rtl_aware != self.rtl_aware {
1797            node.x = self.x;
1798            node.y = self.y;
1799            node.rtl_aware = self.rtl_aware;
1800        }
1801    }
1802
1803    fn capabilities(&self) -> NodeCapabilities {
1804        NodeCapabilities::LAYOUT
1805    }
1806
1807    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
1808        Some(InvalidationKind::Layout)
1809    }
1810}
1811
1812// ============================================================================
1813// Fill Modifier Node
1814// ============================================================================
1815
1816/// Direction for fill modifiers (horizontal, vertical, or both).
1817#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1818pub enum FillDirection {
1819    Horizontal,
1820    Vertical,
1821    Both,
1822}
1823
1824/// Node that fills the maximum available space in one or both dimensions.
1825///
1826/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1827#[derive(Debug)]
1828pub struct FillNode {
1829    direction: FillDirection,
1830    fraction: f32,
1831    state: NodeState,
1832}
1833
1834impl FillNode {
1835    pub fn new(direction: FillDirection, fraction: f32) -> Self {
1836        Self {
1837            direction,
1838            fraction,
1839            state: NodeState::new(),
1840        }
1841    }
1842
1843    pub fn direction(&self) -> FillDirection {
1844        self.direction
1845    }
1846
1847    pub fn fraction(&self) -> f32 {
1848        self.fraction
1849    }
1850}
1851
1852impl DelegatableNode for FillNode {
1853    fn node_state(&self) -> &NodeState {
1854        &self.state
1855    }
1856}
1857
1858impl ModifierNode for FillNode {
1859    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1860        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
1861    }
1862
1863    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1864        Some(self)
1865    }
1866
1867    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1868        Some(self)
1869    }
1870}
1871
1872impl LayoutModifierNode for FillNode {
1873    fn measure(
1874        &self,
1875        _context: &mut dyn ModifierNodeContext,
1876        measurable: &dyn Measurable,
1877        constraints: Constraints,
1878    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1879        // Calculate the fill size based on constraints
1880        let (fill_width, child_min_width, child_max_width) = if self.direction
1881            != FillDirection::Vertical
1882            && constraints.max_width != f32::INFINITY
1883        {
1884            let width = (constraints.max_width * self.fraction)
1885                .round()
1886                .clamp(constraints.min_width, constraints.max_width);
1887            // Tight constraint for child on this axis
1888            (width, width, width)
1889        } else {
1890            (
1891                constraints.max_width,
1892                constraints.min_width,
1893                constraints.max_width,
1894            )
1895        };
1896
1897        let (fill_height, child_min_height, child_max_height) = if self.direction
1898            != FillDirection::Horizontal
1899            && constraints.max_height != f32::INFINITY
1900        {
1901            let height = (constraints.max_height * self.fraction)
1902                .round()
1903                .clamp(constraints.min_height, constraints.max_height);
1904            // Tight constraint for child on this axis
1905            (height, height, height)
1906        } else {
1907            (
1908                constraints.max_height,
1909                constraints.min_height,
1910                constraints.max_height,
1911            )
1912        };
1913
1914        let fill_constraints = Constraints {
1915            min_width: child_min_width,
1916            max_width: child_max_width,
1917            min_height: child_min_height,
1918            max_height: child_max_height,
1919        };
1920
1921        let placeable = measurable.measure(fill_constraints);
1922
1923        // Return the FILL size, not the child size.
1924        // The child is measured within tight constraints on the fill axis,
1925        // but we report the fill size to our parent.
1926        let result_width = if self.direction != FillDirection::Vertical
1927            && constraints.max_width != f32::INFINITY
1928        {
1929            fill_width
1930        } else {
1931            placeable.width()
1932        };
1933
1934        let result_height = if self.direction != FillDirection::Horizontal
1935            && constraints.max_height != f32::INFINITY
1936        {
1937            fill_height
1938        } else {
1939            placeable.height()
1940        };
1941
1942        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
1943            width: result_width,
1944            height: result_height,
1945        })
1946    }
1947
1948    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1949        measurable.min_intrinsic_width(height)
1950    }
1951
1952    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1953        measurable.max_intrinsic_width(height)
1954    }
1955
1956    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1957        measurable.min_intrinsic_height(width)
1958    }
1959
1960    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1961        measurable.max_intrinsic_height(width)
1962    }
1963}
1964
1965/// Element that creates and updates fill nodes.
1966///
1967/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1968#[derive(Debug, Clone, PartialEq)]
1969pub struct FillElement {
1970    direction: FillDirection,
1971    fraction: f32,
1972}
1973
1974impl FillElement {
1975    pub fn width(fraction: f32) -> Self {
1976        Self {
1977            direction: FillDirection::Horizontal,
1978            fraction,
1979        }
1980    }
1981
1982    pub fn height(fraction: f32) -> Self {
1983        Self {
1984            direction: FillDirection::Vertical,
1985            fraction,
1986        }
1987    }
1988
1989    pub fn size(fraction: f32) -> Self {
1990        Self {
1991            direction: FillDirection::Both,
1992            fraction,
1993        }
1994    }
1995}
1996
1997impl Hash for FillElement {
1998    fn hash<H: Hasher>(&self, state: &mut H) {
1999        self.direction.hash(state);
2000        hash_f32_value(state, self.fraction);
2001    }
2002}
2003
2004impl ModifierNodeElement for FillElement {
2005    type Node = FillNode;
2006
2007    fn create(&self) -> Self::Node {
2008        FillNode::new(self.direction, self.fraction)
2009    }
2010
2011    fn update(&self, node: &mut Self::Node) {
2012        if node.direction != self.direction || node.fraction != self.fraction {
2013            node.direction = self.direction;
2014            node.fraction = self.fraction;
2015        }
2016    }
2017
2018    fn capabilities(&self) -> NodeCapabilities {
2019        NodeCapabilities::LAYOUT
2020    }
2021}
2022
2023// ============================================================================
2024// Weight Modifier Node
2025// ============================================================================
2026
2027/// Node that records flex weight data for Row/Column parents.
2028#[derive(Debug)]
2029pub struct WeightNode {
2030    weight: f32,
2031    fill: bool,
2032    state: NodeState,
2033}
2034
2035impl WeightNode {
2036    pub fn new(weight: f32, fill: bool) -> Self {
2037        Self {
2038            weight,
2039            fill,
2040            state: NodeState::new(),
2041        }
2042    }
2043
2044    pub fn layout_weight(&self) -> LayoutWeight {
2045        LayoutWeight {
2046            weight: self.weight,
2047            fill: self.fill,
2048        }
2049    }
2050}
2051
2052impl DelegatableNode for WeightNode {
2053    fn node_state(&self) -> &NodeState {
2054        &self.state
2055    }
2056}
2057
2058impl ModifierNode for WeightNode {
2059    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2060        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2061    }
2062}
2063
2064/// Element that creates and updates weight nodes.
2065#[derive(Debug, Clone, PartialEq)]
2066pub struct WeightElement {
2067    weight: f32,
2068    fill: bool,
2069}
2070
2071impl WeightElement {
2072    pub fn new(weight: f32, fill: bool) -> Self {
2073        Self { weight, fill }
2074    }
2075}
2076
2077impl Hash for WeightElement {
2078    fn hash<H: Hasher>(&self, state: &mut H) {
2079        hash_f32_value(state, self.weight);
2080        self.fill.hash(state);
2081    }
2082}
2083
2084impl ModifierNodeElement for WeightElement {
2085    type Node = WeightNode;
2086
2087    fn create(&self) -> Self::Node {
2088        WeightNode::new(self.weight, self.fill)
2089    }
2090
2091    fn update(&self, node: &mut Self::Node) {
2092        if node.weight != self.weight || node.fill != self.fill {
2093            node.weight = self.weight;
2094            node.fill = self.fill;
2095        }
2096    }
2097
2098    fn capabilities(&self) -> NodeCapabilities {
2099        NodeCapabilities::LAYOUT
2100    }
2101}
2102
2103// ============================================================================
2104// Alignment Modifier Node
2105// ============================================================================
2106
2107/// Node that records alignment preferences for Box/Row/Column scopes.
2108#[derive(Debug)]
2109pub struct AlignmentNode {
2110    box_alignment: Option<Alignment>,
2111    column_alignment: Option<HorizontalAlignment>,
2112    row_alignment: Option<VerticalAlignment>,
2113    state: NodeState,
2114}
2115
2116impl AlignmentNode {
2117    pub fn new(
2118        box_alignment: Option<Alignment>,
2119        column_alignment: Option<HorizontalAlignment>,
2120        row_alignment: Option<VerticalAlignment>,
2121    ) -> Self {
2122        Self {
2123            box_alignment,
2124            column_alignment,
2125            row_alignment,
2126            state: NodeState::new(),
2127        }
2128    }
2129
2130    pub fn box_alignment(&self) -> Option<Alignment> {
2131        self.box_alignment
2132    }
2133
2134    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2135        self.column_alignment
2136    }
2137
2138    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2139        self.row_alignment
2140    }
2141}
2142
2143impl DelegatableNode for AlignmentNode {
2144    fn node_state(&self) -> &NodeState {
2145        &self.state
2146    }
2147}
2148
2149impl ModifierNode for AlignmentNode {
2150    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2151        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2152    }
2153}
2154
2155/// Element that creates and updates alignment nodes.
2156#[derive(Debug, Clone, PartialEq)]
2157pub struct AlignmentElement {
2158    box_alignment: Option<Alignment>,
2159    column_alignment: Option<HorizontalAlignment>,
2160    row_alignment: Option<VerticalAlignment>,
2161}
2162
2163impl AlignmentElement {
2164    pub fn box_alignment(alignment: Alignment) -> Self {
2165        Self {
2166            box_alignment: Some(alignment),
2167            column_alignment: None,
2168            row_alignment: None,
2169        }
2170    }
2171
2172    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2173        Self {
2174            box_alignment: None,
2175            column_alignment: Some(alignment),
2176            row_alignment: None,
2177        }
2178    }
2179
2180    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2181        Self {
2182            box_alignment: None,
2183            column_alignment: None,
2184            row_alignment: Some(alignment),
2185        }
2186    }
2187}
2188
2189impl Hash for AlignmentElement {
2190    fn hash<H: Hasher>(&self, state: &mut H) {
2191        if let Some(alignment) = self.box_alignment {
2192            state.write_u8(1);
2193            hash_alignment(state, alignment);
2194        } else {
2195            state.write_u8(0);
2196        }
2197        if let Some(alignment) = self.column_alignment {
2198            state.write_u8(1);
2199            hash_horizontal_alignment(state, alignment);
2200        } else {
2201            state.write_u8(0);
2202        }
2203        if let Some(alignment) = self.row_alignment {
2204            state.write_u8(1);
2205            hash_vertical_alignment(state, alignment);
2206        } else {
2207            state.write_u8(0);
2208        }
2209    }
2210}
2211
2212impl ModifierNodeElement for AlignmentElement {
2213    type Node = AlignmentNode;
2214
2215    fn create(&self) -> Self::Node {
2216        AlignmentNode::new(
2217            self.box_alignment,
2218            self.column_alignment,
2219            self.row_alignment,
2220        )
2221    }
2222
2223    fn update(&self, node: &mut Self::Node) {
2224        if node.box_alignment != self.box_alignment {
2225            node.box_alignment = self.box_alignment;
2226        }
2227        if node.column_alignment != self.column_alignment {
2228            node.column_alignment = self.column_alignment;
2229        }
2230        if node.row_alignment != self.row_alignment {
2231            node.row_alignment = self.row_alignment;
2232        }
2233    }
2234
2235    fn capabilities(&self) -> NodeCapabilities {
2236        NodeCapabilities::LAYOUT
2237    }
2238}
2239
2240// ============================================================================
2241// Intrinsic Size Modifier Node
2242// ============================================================================
2243
2244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2245pub enum IntrinsicAxis {
2246    Width,
2247    Height,
2248}
2249
2250/// Node that records intrinsic sizing requests.
2251#[derive(Debug)]
2252pub struct IntrinsicSizeNode {
2253    axis: IntrinsicAxis,
2254    size: IntrinsicSize,
2255    state: NodeState,
2256}
2257
2258impl IntrinsicSizeNode {
2259    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2260        Self {
2261            axis,
2262            size,
2263            state: NodeState::new(),
2264        }
2265    }
2266
2267    pub fn axis(&self) -> IntrinsicAxis {
2268        self.axis
2269    }
2270
2271    pub fn intrinsic_size(&self) -> IntrinsicSize {
2272        self.size
2273    }
2274}
2275
2276impl DelegatableNode for IntrinsicSizeNode {
2277    fn node_state(&self) -> &NodeState {
2278        &self.state
2279    }
2280}
2281
2282impl ModifierNode for IntrinsicSizeNode {
2283    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2284        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2285    }
2286}
2287
2288/// Element that creates and updates intrinsic size nodes.
2289#[derive(Debug, Clone, PartialEq)]
2290pub struct IntrinsicSizeElement {
2291    axis: IntrinsicAxis,
2292    size: IntrinsicSize,
2293}
2294
2295impl IntrinsicSizeElement {
2296    pub fn width(size: IntrinsicSize) -> Self {
2297        Self {
2298            axis: IntrinsicAxis::Width,
2299            size,
2300        }
2301    }
2302
2303    pub fn height(size: IntrinsicSize) -> Self {
2304        Self {
2305            axis: IntrinsicAxis::Height,
2306            size,
2307        }
2308    }
2309}
2310
2311impl Hash for IntrinsicSizeElement {
2312    fn hash<H: Hasher>(&self, state: &mut H) {
2313        state.write_u8(match self.axis {
2314            IntrinsicAxis::Width => 0,
2315            IntrinsicAxis::Height => 1,
2316        });
2317        state.write_u8(match self.size {
2318            IntrinsicSize::Min => 0,
2319            IntrinsicSize::Max => 1,
2320        });
2321    }
2322}
2323
2324impl ModifierNodeElement for IntrinsicSizeElement {
2325    type Node = IntrinsicSizeNode;
2326
2327    fn create(&self) -> Self::Node {
2328        IntrinsicSizeNode::new(self.axis, self.size)
2329    }
2330
2331    fn update(&self, node: &mut Self::Node) {
2332        if node.axis != self.axis {
2333            node.axis = self.axis;
2334        }
2335        if node.size != self.size {
2336            node.size = self.size;
2337        }
2338    }
2339
2340    fn capabilities(&self) -> NodeCapabilities {
2341        NodeCapabilities::LAYOUT
2342    }
2343}
2344
2345#[cfg(test)]
2346#[path = "tests/modifier_nodes_tests.rs"]
2347mod tests;