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        //
937        // With enforce_incoming=false (`required_size`) the node measures AND
938        // reports the required size: draw rects and effect bounds derive from
939        // the node box, so oversized overlay content (selection magnifier
940        // line, interaction lenses) must keep its full box. Hosts that must
941        // not grow pin their own size (a fixed-size ancestor wins over a
942        // wrap-content one).
943        let result_width = if self.min_width.is_some()
944            && self.max_width.is_some()
945            && self.min_width == self.max_width
946            && target.min_width >= wrapped_constraints.min_width
947            && target.min_width <= wrapped_constraints.max_width
948        {
949            target.min_width
950        } else {
951            measured_width
952        };
953
954        let result_height = if self.min_height.is_some()
955            && self.max_height.is_some()
956            && self.min_height == self.max_height
957            && target.min_height >= wrapped_constraints.min_height
958            && target.min_height <= wrapped_constraints.max_height
959        {
960            target.min_height
961        } else {
962            measured_height
963        };
964
965        // SizeNode doesn't offset placement - child is placed at (0, 0) relative to this node
966        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
967            width: result_width,
968            height: result_height,
969        })
970    }
971
972    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
973        let target = self.target_constraints();
974        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
975            target.max_width
976        } else {
977            let child_height = if self.enforce_incoming {
978                height
979            } else {
980                height.clamp(target.min_height, target.max_height)
981            };
982            measurable
983                .min_intrinsic_width(child_height)
984                .clamp(target.min_width, target.max_width)
985        }
986    }
987
988    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
989        let target = self.target_constraints();
990        if target.min_width == target.max_width && target.max_width != f32::INFINITY {
991            target.max_width
992        } else {
993            let child_height = if self.enforce_incoming {
994                height
995            } else {
996                height.clamp(target.min_height, target.max_height)
997            };
998            measurable
999                .max_intrinsic_width(child_height)
1000                .clamp(target.min_width, target.max_width)
1001        }
1002    }
1003
1004    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1005        let target = self.target_constraints();
1006        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1007            target.max_height
1008        } else {
1009            let child_width = if self.enforce_incoming {
1010                width
1011            } else {
1012                width.clamp(target.min_width, target.max_width)
1013            };
1014            measurable
1015                .min_intrinsic_height(child_width)
1016                .clamp(target.min_height, target.max_height)
1017        }
1018    }
1019
1020    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1021        let target = self.target_constraints();
1022        if target.min_height == target.max_height && target.max_height != f32::INFINITY {
1023            target.max_height
1024        } else {
1025            let child_width = if self.enforce_incoming {
1026                width
1027            } else {
1028                width.clamp(target.min_width, target.max_width)
1029            };
1030            measurable
1031                .max_intrinsic_height(child_width)
1032                .clamp(target.min_height, target.max_height)
1033        }
1034    }
1035}
1036
1037/// Element that creates and updates size nodes.
1038///
1039/// Matches Kotlin: `SizeElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
1040#[derive(Debug, Clone, PartialEq)]
1041pub struct SizeElement {
1042    min_width: Option<f32>,
1043    max_width: Option<f32>,
1044    min_height: Option<f32>,
1045    max_height: Option<f32>,
1046    enforce_incoming: bool,
1047}
1048
1049impl SizeElement {
1050    pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
1051        Self {
1052            min_width: width,
1053            max_width: width,
1054            min_height: height,
1055            max_height: height,
1056            enforce_incoming: true,
1057        }
1058    }
1059
1060    pub fn with_constraints(
1061        min_width: Option<f32>,
1062        max_width: Option<f32>,
1063        min_height: Option<f32>,
1064        max_height: Option<f32>,
1065        enforce_incoming: bool,
1066    ) -> Self {
1067        Self {
1068            min_width,
1069            max_width,
1070            min_height,
1071            max_height,
1072            enforce_incoming,
1073        }
1074    }
1075}
1076
1077impl Hash for SizeElement {
1078    fn hash<H: Hasher>(&self, state: &mut H) {
1079        hash_option_f32(state, self.min_width);
1080        hash_option_f32(state, self.max_width);
1081        hash_option_f32(state, self.min_height);
1082        hash_option_f32(state, self.max_height);
1083        self.enforce_incoming.hash(state);
1084    }
1085}
1086
1087impl ModifierNodeElement for SizeElement {
1088    type Node = SizeNode;
1089
1090    fn create(&self) -> Self::Node {
1091        SizeNode::new(
1092            self.min_width,
1093            self.max_width,
1094            self.min_height,
1095            self.max_height,
1096            self.enforce_incoming,
1097        )
1098    }
1099
1100    fn update(&self, node: &mut Self::Node) {
1101        if node.min_width != self.min_width
1102            || node.max_width != self.max_width
1103            || node.min_height != self.min_height
1104            || node.max_height != self.max_height
1105            || node.enforce_incoming != self.enforce_incoming
1106        {
1107            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    }
1114
1115    fn capabilities(&self) -> NodeCapabilities {
1116        NodeCapabilities::LAYOUT
1117    }
1118
1119    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
1120        Some(InvalidationKind::Layout)
1121    }
1122}
1123
1124// ============================================================================
1125// Clickable Modifier Node
1126// ============================================================================
1127
1128/// Node that handles click/tap interactions.
1129// Drag threshold is now shared via cranpose_foundation::DRAG_THRESHOLD
1130use cranpose_foundation::DRAG_THRESHOLD;
1131
1132use std::cell::RefCell;
1133
1134// Press position is stored per-node via Rc<RefCell> for sharing with handler closure
1135// Node reuse is ensured by ClickableElement implementing key() to return a stable key
1136// The handler closure is cached to ensure the same closure (and press_position state) is returned
1137
1138pub struct ClickableNode {
1139    on_click: Rc<dyn Fn(Point)>,
1140    state: NodeState,
1141    /// Shared press position for drag detection (per-node state, accessible by handler closure)
1142    press_position: Rc<RefCell<Option<Point>>>,
1143    /// Cached handler closure - created once, returned on every pointer_input_handler() call
1144    cached_handler: Rc<dyn Fn(PointerEvent)>,
1145}
1146
1147impl std::fmt::Debug for ClickableNode {
1148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1149        f.debug_struct("ClickableNode").finish()
1150    }
1151}
1152
1153impl ClickableNode {
1154    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1155        Self::with_handler(Rc::new(on_click))
1156    }
1157
1158    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1159        let press_position = Rc::new(RefCell::new(None));
1160        let cached_handler = Self::create_handler(on_click.clone(), press_position.clone());
1161        Self {
1162            on_click,
1163            state: NodeState::new(),
1164            press_position,
1165            cached_handler,
1166        }
1167    }
1168
1169    fn create_handler(
1170        handler: Rc<dyn Fn(Point)>,
1171        press_position: Rc<RefCell<Option<Point>>>,
1172    ) -> Rc<dyn Fn(PointerEvent)> {
1173        Rc::new(move |event: PointerEvent| {
1174            // Clicks track the primary pointer only; secondary pointers of a
1175            // multi-touch gesture (e.g. a pinch) must never fire clicks.
1176            if event.id != 0 {
1177                return;
1178            }
1179
1180            // Check if event was consumed by scroll or other gesture handlers
1181            if event.is_consumed() {
1182                // Clear press state if event was consumed
1183                *press_position.borrow_mut() = None;
1184                return;
1185            }
1186
1187            match event.kind {
1188                PointerEventKind::Down => {
1189                    // Store global press position for drag detection on Up
1190                    *press_position.borrow_mut() = Some(Point {
1191                        x: event.global_position.x,
1192                        y: event.global_position.y,
1193                    });
1194                }
1195                PointerEventKind::Move => {
1196                    // Move events are tracked via press_position for drag detection
1197                }
1198                PointerEventKind::Up => {
1199                    // Check if this is a click (Up near Down) or a drag (Up far from Down)
1200                    let press_pos_value = *press_position.borrow();
1201
1202                    let should_click = if let Some(press_pos) = press_pos_value {
1203                        let dx = event.global_position.x - press_pos.x;
1204                        let dy = event.global_position.y - press_pos.y;
1205                        let distance = (dx * dx + dy * dy).sqrt();
1206                        distance <= DRAG_THRESHOLD
1207                    } else {
1208                        // No Down was tracked - fire click anyway
1209                        // This preserves the original behavior for cases where Down
1210                        // was handled by a different mechanism
1211                        true
1212                    };
1213
1214                    // Reset press position
1215                    *press_position.borrow_mut() = None;
1216
1217                    if should_click {
1218                        handler(Point {
1219                            x: event.position.x,
1220                            y: event.position.y,
1221                        });
1222                        event.consume();
1223                    }
1224                }
1225                PointerEventKind::Cancel => {
1226                    // Clear press state on cancel
1227                    *press_position.borrow_mut() = None;
1228                }
1229                PointerEventKind::Scroll
1230                | PointerEventKind::Zoom
1231                | PointerEventKind::Enter
1232                | PointerEventKind::Exit => {
1233                    // These events don't affect click press state.
1234                }
1235            }
1236        })
1237    }
1238
1239    pub fn handler(&self) -> Rc<dyn Fn(Point)> {
1240        self.on_click.clone()
1241    }
1242}
1243
1244impl DelegatableNode for ClickableNode {
1245    fn node_state(&self) -> &NodeState {
1246        &self.state
1247    }
1248}
1249
1250impl ModifierNode for ClickableNode {
1251    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1252        context.invalidate(cranpose_foundation::InvalidationKind::PointerInput);
1253    }
1254
1255    fn as_pointer_input_node(&self) -> Option<&dyn PointerInputNode> {
1256        Some(self)
1257    }
1258
1259    fn as_pointer_input_node_mut(&mut self) -> Option<&mut dyn PointerInputNode> {
1260        Some(self)
1261    }
1262}
1263
1264impl PointerInputNode for ClickableNode {
1265    fn on_pointer_event(
1266        &mut self,
1267        _context: &mut dyn ModifierNodeContext,
1268        event: &PointerEvent,
1269    ) -> bool {
1270        // Delegate to the cached handler - single source of truth for click logic
1271        // This avoids duplicating the press position tracking and threshold checking
1272        (self.cached_handler)(event.clone());
1273        event.is_consumed()
1274    }
1275
1276    fn hit_test(&self, _x: f32, _y: f32) -> bool {
1277        // Always participate in hit testing
1278        true
1279    }
1280
1281    fn pointer_input_handler(&self) -> Option<Rc<dyn Fn(PointerEvent)>> {
1282        // Return the cached handler - this ensures the same closure (with its press_position state)
1283        // is used across multiple calls to pointer_input_handler()
1284        Some(self.cached_handler.clone())
1285    }
1286}
1287
1288/// Element that creates and updates clickable nodes.
1289#[derive(Clone)]
1290pub struct ClickableElement {
1291    on_click: Rc<dyn Fn(Point)>,
1292}
1293
1294impl ClickableElement {
1295    pub fn new(on_click: impl Fn(Point) + 'static) -> Self {
1296        Self {
1297            on_click: Rc::new(on_click),
1298        }
1299    }
1300
1301    pub fn with_handler(on_click: Rc<dyn Fn(Point)>) -> Self {
1302        Self { on_click }
1303    }
1304}
1305
1306impl std::fmt::Debug for ClickableElement {
1307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1308        f.debug_struct("ClickableElement").finish()
1309    }
1310}
1311
1312impl PartialEq for ClickableElement {
1313    fn eq(&self, _other: &Self) -> bool {
1314        // Type matching is sufficient - node will be updated via update() method
1315        // This matches JC behavior where nodes are reused for same-type elements,
1316        // preserving press_position state for proper drag detection
1317        true
1318    }
1319}
1320
1321impl Eq for ClickableElement {}
1322
1323impl Hash for ClickableElement {
1324    fn hash<H: Hasher>(&self, state: &mut H) {
1325        // Consistent hash for type-based matching
1326        "clickable".hash(state);
1327    }
1328}
1329
1330impl ModifierNodeElement for ClickableElement {
1331    type Node = ClickableNode;
1332
1333    fn create(&self) -> Self::Node {
1334        ClickableNode::with_handler(self.on_click.clone())
1335    }
1336
1337    // Note: key() is deliberately NOT implemented (returns None by default)
1338    // This enables type-based node reuse: the same ClickableNode instance is
1339    // reused across recompositions, preserving the cached_handler and its
1340    // captured press_position state for proper drag detection.
1341
1342    fn update(&self, node: &mut Self::Node) {
1343        // Update the handler - the cached_handler needs to be recreated
1344        // with the new on_click while preserving press_position
1345        node.on_click = self.on_click.clone();
1346        // Recreate the cached handler with the same press_position but new click handler
1347        node.cached_handler =
1348            ClickableNode::create_handler(node.on_click.clone(), node.press_position.clone());
1349    }
1350
1351    fn capabilities(&self) -> NodeCapabilities {
1352        NodeCapabilities::POINTER_INPUT
1353    }
1354
1355    fn always_update(&self) -> bool {
1356        // Always update to capture new closure while preserving node state
1357        true
1358    }
1359}
1360
1361// ============================================================================
1362// Alpha Modifier Node
1363// ============================================================================
1364
1365/// Node that applies alpha transparency to its content.
1366#[derive(Debug)]
1367pub struct AlphaNode {
1368    alpha: f32,
1369    state: NodeState,
1370}
1371
1372impl AlphaNode {
1373    pub fn new(alpha: f32) -> Self {
1374        Self {
1375            alpha: alpha.clamp(0.0, 1.0),
1376            state: NodeState::new(),
1377        }
1378    }
1379}
1380
1381impl DelegatableNode for AlphaNode {
1382    fn node_state(&self) -> &NodeState {
1383        &self.state
1384    }
1385}
1386
1387impl ModifierNode for AlphaNode {
1388    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1389        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1390    }
1391
1392    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1393        Some(self)
1394    }
1395
1396    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1397        Some(self)
1398    }
1399}
1400
1401impl DrawModifierNode for AlphaNode {
1402    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1403}
1404
1405/// Element that creates and updates alpha nodes.
1406#[derive(Debug, Clone, PartialEq)]
1407pub struct AlphaElement {
1408    alpha: f32,
1409}
1410
1411impl AlphaElement {
1412    pub fn new(alpha: f32) -> Self {
1413        Self {
1414            alpha: alpha.clamp(0.0, 1.0),
1415        }
1416    }
1417}
1418
1419impl Hash for AlphaElement {
1420    fn hash<H: Hasher>(&self, state: &mut H) {
1421        hash_f32_value(state, self.alpha);
1422    }
1423}
1424
1425impl ModifierNodeElement for AlphaElement {
1426    type Node = AlphaNode;
1427
1428    fn create(&self) -> Self::Node {
1429        AlphaNode::new(self.alpha)
1430    }
1431
1432    fn update(&self, node: &mut Self::Node) {
1433        let new_alpha = self.alpha.clamp(0.0, 1.0);
1434        if (node.alpha - new_alpha).abs() > f32::EPSILON {
1435            node.alpha = new_alpha;
1436        }
1437    }
1438
1439    fn capabilities(&self) -> NodeCapabilities {
1440        NodeCapabilities::DRAW
1441    }
1442}
1443
1444// ============================================================================
1445// Clip-To-Bounds Modifier Node
1446// ============================================================================
1447
1448/// Node that marks the subtree for clipping during rendering.
1449#[derive(Debug)]
1450pub struct ClipToBoundsNode {
1451    state: NodeState,
1452}
1453
1454impl ClipToBoundsNode {
1455    pub fn new() -> Self {
1456        Self {
1457            state: NodeState::new(),
1458        }
1459    }
1460}
1461
1462impl DelegatableNode for ClipToBoundsNode {
1463    fn node_state(&self) -> &NodeState {
1464        &self.state
1465    }
1466}
1467
1468impl ModifierNode for ClipToBoundsNode {
1469    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1470        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1471    }
1472
1473    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1474        Some(self)
1475    }
1476
1477    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1478        Some(self)
1479    }
1480}
1481
1482impl DrawModifierNode for ClipToBoundsNode {
1483    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1484}
1485
1486/// Element that creates clip-to-bounds nodes.
1487#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1488pub struct ClipToBoundsElement;
1489
1490impl ClipToBoundsElement {
1491    pub fn new() -> Self {
1492        Self
1493    }
1494}
1495
1496impl ModifierNodeElement for ClipToBoundsElement {
1497    type Node = ClipToBoundsNode;
1498
1499    fn create(&self) -> Self::Node {
1500        ClipToBoundsNode::new()
1501    }
1502
1503    fn update(&self, _node: &mut Self::Node) {}
1504
1505    fn capabilities(&self) -> NodeCapabilities {
1506        NodeCapabilities::DRAW
1507    }
1508}
1509
1510// ============================================================================
1511// Window Rect Reporter Modifier Node
1512// ============================================================================
1513
1514/// Node that publishes its layout node's composited window rect into a shared
1515/// cell. The layout `place` pass writes the node's true on-screen rect (window
1516/// coordinates, resolved through ancestor scroll placement + graphics-layer
1517/// translation) here every pass. Scroll containers use it to expose their
1518/// viewport bounds to a `BringIntoViewResponder`. Draws nothing.
1519pub struct WindowRectReporterNode {
1520    sink: Rc<Cell<cranpose_ui_graphics::Rect>>,
1521    state: NodeState,
1522}
1523
1524impl WindowRectReporterNode {
1525    pub fn new(sink: Rc<Cell<cranpose_ui_graphics::Rect>>) -> Self {
1526        Self {
1527            sink,
1528            state: NodeState::new(),
1529        }
1530    }
1531
1532    /// The cell the layout pass writes this node's window rect into.
1533    pub(crate) fn window_rect_sink(&self) -> Rc<Cell<cranpose_ui_graphics::Rect>> {
1534        self.sink.clone()
1535    }
1536}
1537
1538impl DelegatableNode for WindowRectReporterNode {
1539    fn node_state(&self) -> &NodeState {
1540        &self.state
1541    }
1542}
1543
1544impl ModifierNode for WindowRectReporterNode {
1545    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1546        Some(self)
1547    }
1548
1549    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1550        Some(self)
1551    }
1552}
1553
1554impl LayoutModifierNode for WindowRectReporterNode {
1555    /// Transparent pass-through: measure the wrapped content with the same
1556    /// constraints and place it at the origin. The reporter only exists so the
1557    /// layout `place` pass can publish this node's window rect into its sink.
1558    fn measure(
1559        &self,
1560        _context: &mut dyn ModifierNodeContext,
1561        measurable: &dyn Measurable,
1562        constraints: Constraints,
1563    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1564        let placeable = measurable.measure(constraints);
1565        cranpose_ui_layout::LayoutModifierMeasureResult::new(
1566            Size {
1567                width: placeable.width(),
1568                height: placeable.height(),
1569            },
1570            0.0,
1571            0.0,
1572        )
1573    }
1574}
1575
1576/// Element that creates [`WindowRectReporterNode`] instances. Reuses the node
1577/// across recompositions, swapping the sink cell when it changes.
1578#[derive(Clone)]
1579pub struct WindowRectReporterElement {
1580    sink: Rc<Cell<cranpose_ui_graphics::Rect>>,
1581}
1582
1583impl WindowRectReporterElement {
1584    pub fn new(sink: Rc<Cell<cranpose_ui_graphics::Rect>>) -> Self {
1585        Self { sink }
1586    }
1587}
1588
1589impl std::fmt::Debug for WindowRectReporterElement {
1590    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1591        f.debug_struct("WindowRectReporterElement").finish()
1592    }
1593}
1594
1595impl PartialEq for WindowRectReporterElement {
1596    fn eq(&self, other: &Self) -> bool {
1597        Rc::ptr_eq(&self.sink, &other.sink)
1598    }
1599}
1600
1601impl Eq for WindowRectReporterElement {}
1602
1603impl Hash for WindowRectReporterElement {
1604    fn hash<H: Hasher>(&self, state: &mut H) {
1605        std::ptr::hash(Rc::as_ptr(&self.sink), state);
1606    }
1607}
1608
1609impl ModifierNodeElement for WindowRectReporterElement {
1610    type Node = WindowRectReporterNode;
1611
1612    fn create(&self) -> Self::Node {
1613        WindowRectReporterNode::new(self.sink.clone())
1614    }
1615
1616    fn update(&self, node: &mut Self::Node) {
1617        node.sink = self.sink.clone();
1618    }
1619
1620    fn capabilities(&self) -> NodeCapabilities {
1621        NodeCapabilities::LAYOUT
1622    }
1623}
1624
1625// ============================================================================
1626// Size Reporter Modifier Node
1627// ============================================================================
1628
1629/// Node that publishes its measured size (logical px) into a shared cell on
1630/// every measure pass — the Compose `onSizeChanged` seam for consumers that
1631/// need their node's resolved size outside layout (e.g. shader morph
1632/// geometry expressed in node-local pixels). Transparent for layout, draws
1633/// nothing.
1634pub struct SizeReporterNode {
1635    sink: Rc<Cell<Size>>,
1636    state: NodeState,
1637}
1638
1639impl SizeReporterNode {
1640    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1641        Self {
1642            sink,
1643            state: NodeState::new(),
1644        }
1645    }
1646}
1647
1648impl DelegatableNode for SizeReporterNode {
1649    fn node_state(&self) -> &NodeState {
1650        &self.state
1651    }
1652}
1653
1654impl ModifierNode for SizeReporterNode {
1655    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1656        Some(self)
1657    }
1658
1659    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1660        Some(self)
1661    }
1662}
1663
1664impl LayoutModifierNode for SizeReporterNode {
1665    fn measure(
1666        &self,
1667        _context: &mut dyn ModifierNodeContext,
1668        measurable: &dyn Measurable,
1669        constraints: Constraints,
1670    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1671        let placeable = measurable.measure(constraints);
1672        let size = Size {
1673            width: placeable.width(),
1674            height: placeable.height(),
1675        };
1676        self.sink.set(size);
1677        cranpose_ui_layout::LayoutModifierMeasureResult::new(size, 0.0, 0.0)
1678    }
1679}
1680
1681/// Element for [`SizeReporterNode`]; reuses the node, swapping the sink.
1682#[derive(Clone)]
1683pub struct SizeReporterElement {
1684    sink: Rc<Cell<Size>>,
1685}
1686
1687impl SizeReporterElement {
1688    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1689        Self { sink }
1690    }
1691}
1692
1693impl std::fmt::Debug for SizeReporterElement {
1694    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1695        f.debug_struct("SizeReporterElement").finish()
1696    }
1697}
1698
1699impl PartialEq for SizeReporterElement {
1700    fn eq(&self, other: &Self) -> bool {
1701        Rc::ptr_eq(&self.sink, &other.sink)
1702    }
1703}
1704
1705impl Hash for SizeReporterElement {
1706    fn hash<H: Hasher>(&self, state: &mut H) {
1707        (Rc::as_ptr(&self.sink) as usize).hash(state);
1708    }
1709}
1710
1711impl ModifierNodeElement for SizeReporterElement {
1712    type Node = SizeReporterNode;
1713
1714    fn create(&self) -> Self::Node {
1715        SizeReporterNode::new(self.sink.clone())
1716    }
1717
1718    fn update(&self, node: &mut Self::Node) {
1719        node.sink = self.sink.clone();
1720    }
1721
1722    fn capabilities(&self) -> NodeCapabilities {
1723        NodeCapabilities::LAYOUT
1724    }
1725}
1726
1727// ============================================================================
1728// Draw Command Modifier Node
1729// ============================================================================
1730
1731/// Node that stores draw commands emitted by draw modifiers.
1732pub struct DrawCommandNode {
1733    commands: Vec<DrawCommand>,
1734    node_id: Cell<Option<NodeId>>,
1735    state: NodeState,
1736}
1737
1738impl DrawCommandNode {
1739    pub fn new(commands: Vec<DrawCommand>) -> Self {
1740        Self {
1741            commands,
1742            node_id: Cell::new(None),
1743            state: NodeState::new(),
1744        }
1745    }
1746
1747    #[cfg(test)]
1748    pub fn commands(&self) -> &[DrawCommand] {
1749        &self.commands
1750    }
1751
1752    pub(crate) fn observed_commands(&self) -> Vec<DrawCommand> {
1753        let node_id = self.node_id.get();
1754        self.commands
1755            .iter()
1756            .cloned()
1757            .enumerate()
1758            .map(|(index, command)| observe_draw_command(command, node_id, index))
1759            .collect()
1760    }
1761}
1762
1763impl DelegatableNode for DrawCommandNode {
1764    fn node_state(&self) -> &NodeState {
1765        &self.state
1766    }
1767}
1768
1769impl ModifierNode for DrawCommandNode {
1770    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1771        self.node_id.set(context.node_id());
1772        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1773    }
1774
1775    fn on_detach(&mut self) {
1776        if let Some(node_id) = self.node_id.replace(None) {
1777            crate::render_state::clear_draw_observations_for_node(node_id);
1778        }
1779    }
1780
1781    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1782        Some(self)
1783    }
1784
1785    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1786        Some(self)
1787    }
1788}
1789
1790impl DrawModifierNode for DrawCommandNode {
1791    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1792}
1793
1794fn observe_draw_command(
1795    command: DrawCommand,
1796    node_id: Option<NodeId>,
1797    command_index: usize,
1798) -> DrawCommand {
1799    let Some(node_id) = node_id else {
1800        return command;
1801    };
1802    let scope = crate::render_state::DrawObservationScope::new(node_id, command_index);
1803    match command {
1804        DrawCommand::Behind(draw) => DrawCommand::Behind(Rc::new(move |size| {
1805            crate::render_state::observe_draw_reads(scope, || draw(size))
1806        })),
1807        DrawCommand::WithContent(draw) => DrawCommand::WithContent(Rc::new(move |size| {
1808            crate::render_state::observe_draw_reads(scope, || draw(size))
1809        })),
1810        DrawCommand::Overlay(draw) => DrawCommand::Overlay(Rc::new(move |size| {
1811            crate::render_state::observe_draw_reads(scope, || draw(size))
1812        })),
1813    }
1814}
1815
1816fn draw_command_tag(cmd: &DrawCommand) -> u8 {
1817    match cmd {
1818        DrawCommand::Behind(_) => 0,
1819        DrawCommand::WithContent(_) => 1,
1820        DrawCommand::Overlay(_) => 2,
1821    }
1822}
1823
1824fn draw_command_closure_identity(cmd: &DrawCommand) -> *const () {
1825    match cmd {
1826        DrawCommand::Behind(f) | DrawCommand::WithContent(f) | DrawCommand::Overlay(f) => {
1827            Rc::as_ptr(f) as *const ()
1828        }
1829    }
1830}
1831
1832/// Element that wires draw commands into the modifier node chain.
1833#[derive(Clone)]
1834pub struct DrawCommandElement {
1835    commands: Vec<DrawCommand>,
1836}
1837
1838impl DrawCommandElement {
1839    pub fn new(command: DrawCommand) -> Self {
1840        Self {
1841            commands: vec![command],
1842        }
1843    }
1844
1845    pub fn from_commands(commands: Vec<DrawCommand>) -> Self {
1846        Self { commands }
1847    }
1848}
1849
1850impl std::fmt::Debug for DrawCommandElement {
1851    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1852        f.debug_struct("DrawCommandElement")
1853            .field("commands", &self.commands.len())
1854            .finish()
1855    }
1856}
1857
1858impl PartialEq for DrawCommandElement {
1859    fn eq(&self, other: &Self) -> bool {
1860        if self.commands.len() != other.commands.len() {
1861            return false;
1862        }
1863        self.commands
1864            .iter()
1865            .zip(other.commands.iter())
1866            .all(|(a, b)| {
1867                draw_command_tag(a) == draw_command_tag(b)
1868                    && draw_command_closure_identity(a) == draw_command_closure_identity(b)
1869            })
1870    }
1871}
1872
1873impl Eq for DrawCommandElement {}
1874
1875impl std::hash::Hash for DrawCommandElement {
1876    fn hash<H: Hasher>(&self, state: &mut H) {
1877        "draw_commands".hash(state);
1878        self.commands.len().hash(state);
1879        for command in &self.commands {
1880            draw_command_tag(command).hash(state);
1881            (draw_command_closure_identity(command) as usize).hash(state);
1882        }
1883    }
1884}
1885
1886impl ModifierNodeElement for DrawCommandElement {
1887    type Node = DrawCommandNode;
1888
1889    fn create(&self) -> Self::Node {
1890        DrawCommandNode::new(self.commands.clone())
1891    }
1892
1893    fn update(&self, node: &mut Self::Node) {
1894        node.commands = self.commands.clone();
1895    }
1896
1897    fn capabilities(&self) -> NodeCapabilities {
1898        NodeCapabilities::DRAW
1899    }
1900}
1901
1902// ============================================================================
1903// Offset Modifier Node
1904// ============================================================================
1905
1906/// Node that offsets its content by a fixed (x, y) amount.
1907///
1908/// Matches Kotlin: `OffsetNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
1909#[derive(Debug)]
1910pub struct OffsetNode {
1911    x: f32,
1912    y: f32,
1913    rtl_aware: bool,
1914    state: NodeState,
1915}
1916
1917impl OffsetNode {
1918    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
1919        Self {
1920            x,
1921            y,
1922            rtl_aware,
1923            state: NodeState::new(),
1924        }
1925    }
1926
1927    pub fn offset(&self) -> Point {
1928        Point {
1929            x: self.x,
1930            y: self.y,
1931        }
1932    }
1933
1934    pub fn rtl_aware(&self) -> bool {
1935        self.rtl_aware
1936    }
1937}
1938
1939impl DelegatableNode for OffsetNode {
1940    fn node_state(&self) -> &NodeState {
1941        &self.state
1942    }
1943}
1944
1945impl ModifierNode for OffsetNode {
1946    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1947        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
1948    }
1949
1950    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1951        Some(self)
1952    }
1953
1954    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1955        Some(self)
1956    }
1957}
1958
1959impl LayoutModifierNode for OffsetNode {
1960    fn measure(
1961        &self,
1962        _context: &mut dyn ModifierNodeContext,
1963        measurable: &dyn Measurable,
1964        constraints: Constraints,
1965    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1966        // Offset doesn't affect measurement, just placement
1967        let placeable = measurable.measure(constraints);
1968
1969        // Return child size unchanged, but specify the offset for placement
1970        cranpose_ui_layout::LayoutModifierMeasureResult::new(
1971            Size {
1972                width: placeable.width(),
1973                height: placeable.height(),
1974            },
1975            self.x, // Place child offset by x
1976            self.y, // Place child offset by y
1977        )
1978    }
1979
1980    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1981        measurable.min_intrinsic_width(height)
1982    }
1983
1984    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
1985        measurable.max_intrinsic_width(height)
1986    }
1987
1988    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1989        measurable.min_intrinsic_height(width)
1990    }
1991
1992    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
1993        measurable.max_intrinsic_height(width)
1994    }
1995}
1996
1997/// Element that creates and updates offset nodes.
1998///
1999/// Matches Kotlin: `OffsetElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
2000#[derive(Debug, Clone, PartialEq)]
2001pub struct OffsetElement {
2002    x: f32,
2003    y: f32,
2004    rtl_aware: bool,
2005}
2006
2007impl OffsetElement {
2008    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
2009        Self { x, y, rtl_aware }
2010    }
2011}
2012
2013impl Hash for OffsetElement {
2014    fn hash<H: Hasher>(&self, state: &mut H) {
2015        hash_f32_value(state, self.x);
2016        hash_f32_value(state, self.y);
2017        self.rtl_aware.hash(state);
2018    }
2019}
2020
2021impl ModifierNodeElement for OffsetElement {
2022    type Node = OffsetNode;
2023
2024    fn create(&self) -> Self::Node {
2025        OffsetNode::new(self.x, self.y, self.rtl_aware)
2026    }
2027
2028    fn update(&self, node: &mut Self::Node) {
2029        if node.x != self.x || node.y != self.y || node.rtl_aware != self.rtl_aware {
2030            node.x = self.x;
2031            node.y = self.y;
2032            node.rtl_aware = self.rtl_aware;
2033        }
2034    }
2035
2036    fn capabilities(&self) -> NodeCapabilities {
2037        NodeCapabilities::LAYOUT
2038    }
2039
2040    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2041        Some(InvalidationKind::Layout)
2042    }
2043}
2044
2045// ============================================================================
2046// Fractional Offset Modifier Node
2047// ============================================================================
2048
2049/// Node that offsets its content by a fraction of its own measured size.
2050///
2051/// There is no direct Jetpack Compose modifier equivalent; Compose's slide
2052/// transitions receive the measured size through a lambda instead. This node
2053/// backs `slide_in_vertically` / `slide_out_vertically` in
2054/// `AnimatedVisibility`, where the offset is expressed as a fraction of the
2055/// content height.
2056#[derive(Debug)]
2057pub struct FractionalOffsetNode {
2058    x_fraction: f32,
2059    y_fraction: f32,
2060    state: NodeState,
2061}
2062
2063impl FractionalOffsetNode {
2064    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2065        Self {
2066            x_fraction,
2067            y_fraction,
2068            state: NodeState::new(),
2069        }
2070    }
2071
2072    pub fn fractions(&self) -> Point {
2073        Point {
2074            x: self.x_fraction,
2075            y: self.y_fraction,
2076        }
2077    }
2078}
2079
2080impl DelegatableNode for FractionalOffsetNode {
2081    fn node_state(&self) -> &NodeState {
2082        &self.state
2083    }
2084}
2085
2086impl ModifierNode for FractionalOffsetNode {
2087    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2088        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2089    }
2090
2091    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2092        Some(self)
2093    }
2094
2095    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2096        Some(self)
2097    }
2098}
2099
2100impl LayoutModifierNode for FractionalOffsetNode {
2101    fn measure(
2102        &self,
2103        _context: &mut dyn ModifierNodeContext,
2104        measurable: &dyn Measurable,
2105        constraints: Constraints,
2106    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2107        // Offset doesn't affect measurement, just placement. The placement
2108        // offset is resolved against the measured content size.
2109        let placeable = measurable.measure(constraints);
2110
2111        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2112            Size {
2113                width: placeable.width(),
2114                height: placeable.height(),
2115            },
2116            self.x_fraction * placeable.width(),
2117            self.y_fraction * placeable.height(),
2118        )
2119    }
2120
2121    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2122        measurable.min_intrinsic_width(height)
2123    }
2124
2125    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2126        measurable.max_intrinsic_width(height)
2127    }
2128
2129    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2130        measurable.min_intrinsic_height(width)
2131    }
2132
2133    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2134        measurable.max_intrinsic_height(width)
2135    }
2136}
2137
2138/// Element that creates and updates fractional offset nodes.
2139#[derive(Debug, Clone, PartialEq)]
2140pub struct FractionalOffsetElement {
2141    x_fraction: f32,
2142    y_fraction: f32,
2143}
2144
2145impl FractionalOffsetElement {
2146    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2147        Self {
2148            x_fraction,
2149            y_fraction,
2150        }
2151    }
2152}
2153
2154impl Hash for FractionalOffsetElement {
2155    fn hash<H: Hasher>(&self, state: &mut H) {
2156        "fractional_offset".hash(state);
2157        hash_f32_value(state, self.x_fraction);
2158        hash_f32_value(state, self.y_fraction);
2159    }
2160}
2161
2162impl ModifierNodeElement for FractionalOffsetElement {
2163    type Node = FractionalOffsetNode;
2164
2165    fn create(&self) -> Self::Node {
2166        FractionalOffsetNode::new(self.x_fraction, self.y_fraction)
2167    }
2168
2169    fn update(&self, node: &mut Self::Node) {
2170        if node.x_fraction != self.x_fraction || node.y_fraction != self.y_fraction {
2171            node.x_fraction = self.x_fraction;
2172            node.y_fraction = self.y_fraction;
2173        }
2174    }
2175
2176    fn capabilities(&self) -> NodeCapabilities {
2177        NodeCapabilities::LAYOUT
2178    }
2179
2180    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2181        Some(InvalidationKind::Layout)
2182    }
2183}
2184
2185// ============================================================================
2186// Fill Modifier Node
2187// ============================================================================
2188
2189/// Direction for fill modifiers (horizontal, vertical, or both).
2190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2191pub enum FillDirection {
2192    Horizontal,
2193    Vertical,
2194    Both,
2195}
2196
2197/// Node that fills the maximum available space in one or both dimensions.
2198///
2199/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2200#[derive(Debug)]
2201pub struct FillNode {
2202    direction: FillDirection,
2203    fraction: f32,
2204    state: NodeState,
2205}
2206
2207impl FillNode {
2208    pub fn new(direction: FillDirection, fraction: f32) -> Self {
2209        Self {
2210            direction,
2211            fraction,
2212            state: NodeState::new(),
2213        }
2214    }
2215
2216    pub fn direction(&self) -> FillDirection {
2217        self.direction
2218    }
2219
2220    pub fn fraction(&self) -> f32 {
2221        self.fraction
2222    }
2223}
2224
2225impl DelegatableNode for FillNode {
2226    fn node_state(&self) -> &NodeState {
2227        &self.state
2228    }
2229}
2230
2231impl ModifierNode for FillNode {
2232    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2233        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2234    }
2235
2236    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2237        Some(self)
2238    }
2239
2240    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2241        Some(self)
2242    }
2243}
2244
2245impl LayoutModifierNode for FillNode {
2246    fn measure(
2247        &self,
2248        _context: &mut dyn ModifierNodeContext,
2249        measurable: &dyn Measurable,
2250        constraints: Constraints,
2251    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2252        // Calculate the fill size based on constraints
2253        let (fill_width, child_min_width, child_max_width) = if self.direction
2254            != FillDirection::Vertical
2255            && constraints.max_width != f32::INFINITY
2256        {
2257            let width = (constraints.max_width * self.fraction)
2258                .round()
2259                .clamp(constraints.min_width, constraints.max_width);
2260            // Tight constraint for child on this axis
2261            (width, width, width)
2262        } else {
2263            (
2264                constraints.max_width,
2265                constraints.min_width,
2266                constraints.max_width,
2267            )
2268        };
2269
2270        let (fill_height, child_min_height, child_max_height) = if self.direction
2271            != FillDirection::Horizontal
2272            && constraints.max_height != f32::INFINITY
2273        {
2274            let height = (constraints.max_height * self.fraction)
2275                .round()
2276                .clamp(constraints.min_height, constraints.max_height);
2277            // Tight constraint for child on this axis
2278            (height, height, height)
2279        } else {
2280            (
2281                constraints.max_height,
2282                constraints.min_height,
2283                constraints.max_height,
2284            )
2285        };
2286
2287        let fill_constraints = Constraints {
2288            min_width: child_min_width,
2289            max_width: child_max_width,
2290            min_height: child_min_height,
2291            max_height: child_max_height,
2292        };
2293
2294        let placeable = measurable.measure(fill_constraints);
2295
2296        // Return the FILL size, not the child size.
2297        // The child is measured within tight constraints on the fill axis,
2298        // but we report the fill size to our parent.
2299        let result_width = if self.direction != FillDirection::Vertical
2300            && constraints.max_width != f32::INFINITY
2301        {
2302            fill_width
2303        } else {
2304            placeable.width()
2305        };
2306
2307        let result_height = if self.direction != FillDirection::Horizontal
2308            && constraints.max_height != f32::INFINITY
2309        {
2310            fill_height
2311        } else {
2312            placeable.height()
2313        };
2314
2315        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
2316            width: result_width,
2317            height: result_height,
2318        })
2319    }
2320
2321    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2322        measurable.min_intrinsic_width(height)
2323    }
2324
2325    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2326        measurable.max_intrinsic_width(height)
2327    }
2328
2329    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2330        measurable.min_intrinsic_height(width)
2331    }
2332
2333    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2334        measurable.max_intrinsic_height(width)
2335    }
2336}
2337
2338/// Element that creates and updates fill nodes.
2339///
2340/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2341#[derive(Debug, Clone, PartialEq)]
2342pub struct FillElement {
2343    direction: FillDirection,
2344    fraction: f32,
2345}
2346
2347impl FillElement {
2348    pub fn width(fraction: f32) -> Self {
2349        Self {
2350            direction: FillDirection::Horizontal,
2351            fraction,
2352        }
2353    }
2354
2355    pub fn height(fraction: f32) -> Self {
2356        Self {
2357            direction: FillDirection::Vertical,
2358            fraction,
2359        }
2360    }
2361
2362    pub fn size(fraction: f32) -> Self {
2363        Self {
2364            direction: FillDirection::Both,
2365            fraction,
2366        }
2367    }
2368}
2369
2370impl Hash for FillElement {
2371    fn hash<H: Hasher>(&self, state: &mut H) {
2372        self.direction.hash(state);
2373        hash_f32_value(state, self.fraction);
2374    }
2375}
2376
2377impl ModifierNodeElement for FillElement {
2378    type Node = FillNode;
2379
2380    fn create(&self) -> Self::Node {
2381        FillNode::new(self.direction, self.fraction)
2382    }
2383
2384    fn update(&self, node: &mut Self::Node) {
2385        if node.direction != self.direction || node.fraction != self.fraction {
2386            node.direction = self.direction;
2387            node.fraction = self.fraction;
2388        }
2389    }
2390
2391    fn capabilities(&self) -> NodeCapabilities {
2392        NodeCapabilities::LAYOUT
2393    }
2394}
2395
2396// ============================================================================
2397// Weight Modifier Node
2398// ============================================================================
2399
2400/// Node that records flex weight data for Row/Column parents.
2401#[derive(Debug)]
2402pub struct WeightNode {
2403    weight: f32,
2404    fill: bool,
2405    state: NodeState,
2406}
2407
2408impl WeightNode {
2409    pub fn new(weight: f32, fill: bool) -> Self {
2410        Self {
2411            weight,
2412            fill,
2413            state: NodeState::new(),
2414        }
2415    }
2416
2417    pub fn layout_weight(&self) -> LayoutWeight {
2418        LayoutWeight {
2419            weight: self.weight,
2420            fill: self.fill,
2421        }
2422    }
2423}
2424
2425impl DelegatableNode for WeightNode {
2426    fn node_state(&self) -> &NodeState {
2427        &self.state
2428    }
2429}
2430
2431impl ModifierNode for WeightNode {
2432    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2433        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2434    }
2435}
2436
2437/// Element that creates and updates weight nodes.
2438#[derive(Debug, Clone, PartialEq)]
2439pub struct WeightElement {
2440    weight: f32,
2441    fill: bool,
2442}
2443
2444impl WeightElement {
2445    pub fn new(weight: f32, fill: bool) -> Self {
2446        Self { weight, fill }
2447    }
2448}
2449
2450impl Hash for WeightElement {
2451    fn hash<H: Hasher>(&self, state: &mut H) {
2452        hash_f32_value(state, self.weight);
2453        self.fill.hash(state);
2454    }
2455}
2456
2457impl ModifierNodeElement for WeightElement {
2458    type Node = WeightNode;
2459
2460    fn create(&self) -> Self::Node {
2461        WeightNode::new(self.weight, self.fill)
2462    }
2463
2464    fn update(&self, node: &mut Self::Node) {
2465        if node.weight != self.weight || node.fill != self.fill {
2466            node.weight = self.weight;
2467            node.fill = self.fill;
2468        }
2469    }
2470
2471    fn capabilities(&self) -> NodeCapabilities {
2472        NodeCapabilities::LAYOUT
2473    }
2474}
2475
2476// ============================================================================
2477// Alignment Modifier Node
2478// ============================================================================
2479
2480/// Node that records alignment preferences for Box/Row/Column scopes.
2481#[derive(Debug)]
2482pub struct AlignmentNode {
2483    box_alignment: Option<Alignment>,
2484    column_alignment: Option<HorizontalAlignment>,
2485    row_alignment: Option<VerticalAlignment>,
2486    state: NodeState,
2487}
2488
2489impl AlignmentNode {
2490    pub fn new(
2491        box_alignment: Option<Alignment>,
2492        column_alignment: Option<HorizontalAlignment>,
2493        row_alignment: Option<VerticalAlignment>,
2494    ) -> Self {
2495        Self {
2496            box_alignment,
2497            column_alignment,
2498            row_alignment,
2499            state: NodeState::new(),
2500        }
2501    }
2502
2503    pub fn box_alignment(&self) -> Option<Alignment> {
2504        self.box_alignment
2505    }
2506
2507    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2508        self.column_alignment
2509    }
2510
2511    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2512        self.row_alignment
2513    }
2514}
2515
2516impl DelegatableNode for AlignmentNode {
2517    fn node_state(&self) -> &NodeState {
2518        &self.state
2519    }
2520}
2521
2522impl ModifierNode for AlignmentNode {
2523    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2524        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2525    }
2526}
2527
2528/// Element that creates and updates alignment nodes.
2529#[derive(Debug, Clone, PartialEq)]
2530pub struct AlignmentElement {
2531    box_alignment: Option<Alignment>,
2532    column_alignment: Option<HorizontalAlignment>,
2533    row_alignment: Option<VerticalAlignment>,
2534}
2535
2536impl AlignmentElement {
2537    pub fn box_alignment(alignment: Alignment) -> Self {
2538        Self {
2539            box_alignment: Some(alignment),
2540            column_alignment: None,
2541            row_alignment: None,
2542        }
2543    }
2544
2545    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2546        Self {
2547            box_alignment: None,
2548            column_alignment: Some(alignment),
2549            row_alignment: None,
2550        }
2551    }
2552
2553    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2554        Self {
2555            box_alignment: None,
2556            column_alignment: None,
2557            row_alignment: Some(alignment),
2558        }
2559    }
2560}
2561
2562impl Hash for AlignmentElement {
2563    fn hash<H: Hasher>(&self, state: &mut H) {
2564        if let Some(alignment) = self.box_alignment {
2565            state.write_u8(1);
2566            hash_alignment(state, alignment);
2567        } else {
2568            state.write_u8(0);
2569        }
2570        if let Some(alignment) = self.column_alignment {
2571            state.write_u8(1);
2572            hash_horizontal_alignment(state, alignment);
2573        } else {
2574            state.write_u8(0);
2575        }
2576        if let Some(alignment) = self.row_alignment {
2577            state.write_u8(1);
2578            hash_vertical_alignment(state, alignment);
2579        } else {
2580            state.write_u8(0);
2581        }
2582    }
2583}
2584
2585impl ModifierNodeElement for AlignmentElement {
2586    type Node = AlignmentNode;
2587
2588    fn create(&self) -> Self::Node {
2589        AlignmentNode::new(
2590            self.box_alignment,
2591            self.column_alignment,
2592            self.row_alignment,
2593        )
2594    }
2595
2596    fn update(&self, node: &mut Self::Node) {
2597        if node.box_alignment != self.box_alignment {
2598            node.box_alignment = self.box_alignment;
2599        }
2600        if node.column_alignment != self.column_alignment {
2601            node.column_alignment = self.column_alignment;
2602        }
2603        if node.row_alignment != self.row_alignment {
2604            node.row_alignment = self.row_alignment;
2605        }
2606    }
2607
2608    fn capabilities(&self) -> NodeCapabilities {
2609        NodeCapabilities::LAYOUT
2610    }
2611}
2612
2613// ============================================================================
2614// Intrinsic Size Modifier Node
2615// ============================================================================
2616
2617#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2618pub enum IntrinsicAxis {
2619    Width,
2620    Height,
2621}
2622
2623/// Node that records intrinsic sizing requests.
2624#[derive(Debug)]
2625pub struct IntrinsicSizeNode {
2626    axis: IntrinsicAxis,
2627    size: IntrinsicSize,
2628    state: NodeState,
2629}
2630
2631impl IntrinsicSizeNode {
2632    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2633        Self {
2634            axis,
2635            size,
2636            state: NodeState::new(),
2637        }
2638    }
2639
2640    pub fn axis(&self) -> IntrinsicAxis {
2641        self.axis
2642    }
2643
2644    pub fn intrinsic_size(&self) -> IntrinsicSize {
2645        self.size
2646    }
2647}
2648
2649impl DelegatableNode for IntrinsicSizeNode {
2650    fn node_state(&self) -> &NodeState {
2651        &self.state
2652    }
2653}
2654
2655impl ModifierNode for IntrinsicSizeNode {
2656    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2657        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2658    }
2659}
2660
2661/// Element that creates and updates intrinsic size nodes.
2662#[derive(Debug, Clone, PartialEq)]
2663pub struct IntrinsicSizeElement {
2664    axis: IntrinsicAxis,
2665    size: IntrinsicSize,
2666}
2667
2668impl IntrinsicSizeElement {
2669    pub fn width(size: IntrinsicSize) -> Self {
2670        Self {
2671            axis: IntrinsicAxis::Width,
2672            size,
2673        }
2674    }
2675
2676    pub fn height(size: IntrinsicSize) -> Self {
2677        Self {
2678            axis: IntrinsicAxis::Height,
2679            size,
2680        }
2681    }
2682}
2683
2684impl Hash for IntrinsicSizeElement {
2685    fn hash<H: Hasher>(&self, state: &mut H) {
2686        state.write_u8(match self.axis {
2687            IntrinsicAxis::Width => 0,
2688            IntrinsicAxis::Height => 1,
2689        });
2690        state.write_u8(match self.size {
2691            IntrinsicSize::Min => 0,
2692            IntrinsicSize::Max => 1,
2693        });
2694    }
2695}
2696
2697impl ModifierNodeElement for IntrinsicSizeElement {
2698    type Node = IntrinsicSizeNode;
2699
2700    fn create(&self) -> Self::Node {
2701        IntrinsicSizeNode::new(self.axis, self.size)
2702    }
2703
2704    fn update(&self, node: &mut Self::Node) {
2705        if node.axis != self.axis {
2706            node.axis = self.axis;
2707        }
2708        if node.size != self.size {
2709            node.size = self.size;
2710        }
2711    }
2712
2713    fn capabilities(&self) -> NodeCapabilities {
2714        NodeCapabilities::LAYOUT
2715    }
2716}
2717
2718#[cfg(test)]
2719#[path = "tests/modifier_nodes_tests.rs"]
2720mod tests;