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