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