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/// Node that publishes its measured size (logical px) into a shared cell on
1683/// every measure pass — the Compose `onSizeChanged` seam for consumers that
1684/// need their node's resolved size outside layout (e.g. shader morph
1685/// geometry expressed in node-local pixels). Transparent for layout, draws
1686/// nothing.
1687pub struct SizeReporterNode {
1688    sink: Rc<Cell<Size>>,
1689    state: NodeState,
1690}
1691
1692impl SizeReporterNode {
1693    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1694        Self {
1695            sink,
1696            state: NodeState::new(),
1697        }
1698    }
1699}
1700
1701impl DelegatableNode for SizeReporterNode {
1702    fn node_state(&self) -> &NodeState {
1703        &self.state
1704    }
1705}
1706
1707impl ModifierNode for SizeReporterNode {
1708    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
1709        Some(self)
1710    }
1711
1712    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
1713        Some(self)
1714    }
1715}
1716
1717impl LayoutModifierNode for SizeReporterNode {
1718    fn measure(
1719        &self,
1720        _context: &mut dyn ModifierNodeContext,
1721        measurable: &dyn Measurable,
1722        constraints: Constraints,
1723    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
1724        let placeable = measurable.measure(constraints);
1725        let size = Size {
1726            width: placeable.width(),
1727            height: placeable.height(),
1728        };
1729        self.sink.set(size);
1730        cranpose_ui_layout::LayoutModifierMeasureResult::new(size, 0.0, 0.0)
1731    }
1732}
1733
1734/// Element for [`SizeReporterNode`]; reuses the node, swapping the sink.
1735#[derive(Clone)]
1736pub struct SizeReporterElement {
1737    sink: Rc<Cell<Size>>,
1738}
1739
1740impl SizeReporterElement {
1741    pub fn new(sink: Rc<Cell<Size>>) -> Self {
1742        Self { sink }
1743    }
1744}
1745
1746impl std::fmt::Debug for SizeReporterElement {
1747    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1748        f.debug_struct("SizeReporterElement").finish()
1749    }
1750}
1751
1752impl PartialEq for SizeReporterElement {
1753    fn eq(&self, other: &Self) -> bool {
1754        Rc::ptr_eq(&self.sink, &other.sink)
1755    }
1756}
1757
1758impl Hash for SizeReporterElement {
1759    fn hash<H: Hasher>(&self, state: &mut H) {
1760        (Rc::as_ptr(&self.sink) as usize).hash(state);
1761    }
1762}
1763
1764impl ModifierNodeElement for SizeReporterElement {
1765    type Node = SizeReporterNode;
1766
1767    fn create(&self) -> Self::Node {
1768        SizeReporterNode::new(self.sink.clone())
1769    }
1770
1771    fn update(&self, node: &mut Self::Node) {
1772        node.sink = self.sink.clone();
1773    }
1774
1775    fn capabilities(&self) -> NodeCapabilities {
1776        NodeCapabilities::LAYOUT
1777    }
1778}
1779
1780// ============================================================================
1781// Draw Command Modifier Node
1782// ============================================================================
1783
1784/// Node that stores draw commands emitted by draw modifiers.
1785pub struct DrawCommandNode {
1786    commands: Vec<DrawCommand>,
1787    node_id: Cell<Option<NodeId>>,
1788    state: NodeState,
1789}
1790
1791impl DrawCommandNode {
1792    pub fn new(commands: Vec<DrawCommand>) -> Self {
1793        Self {
1794            commands,
1795            node_id: Cell::new(None),
1796            state: NodeState::new(),
1797        }
1798    }
1799
1800    #[cfg(test)]
1801    pub fn commands(&self) -> &[DrawCommand] {
1802        &self.commands
1803    }
1804
1805    pub(crate) fn observed_commands(&self) -> Vec<DrawCommand> {
1806        let node_id = self.node_id.get();
1807        self.commands
1808            .iter()
1809            .cloned()
1810            .enumerate()
1811            .map(|(index, command)| observe_draw_command(command, node_id, index))
1812            .collect()
1813    }
1814}
1815
1816impl DelegatableNode for DrawCommandNode {
1817    fn node_state(&self) -> &NodeState {
1818        &self.state
1819    }
1820}
1821
1822impl ModifierNode for DrawCommandNode {
1823    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
1824        self.node_id.set(context.node_id());
1825        context.invalidate(cranpose_foundation::InvalidationKind::Draw);
1826    }
1827
1828    fn on_detach(&mut self) {
1829        if let Some(node_id) = self.node_id.replace(None) {
1830            crate::render_state::clear_draw_observations_for_node(node_id);
1831        }
1832    }
1833
1834    fn as_draw_node(&self) -> Option<&dyn DrawModifierNode> {
1835        Some(self)
1836    }
1837
1838    fn as_draw_node_mut(&mut self) -> Option<&mut dyn DrawModifierNode> {
1839        Some(self)
1840    }
1841}
1842
1843impl DrawModifierNode for DrawCommandNode {
1844    fn draw(&self, _draw_scope: &mut dyn DrawScope) {}
1845}
1846
1847fn observe_draw_command(
1848    command: DrawCommand,
1849    node_id: Option<NodeId>,
1850    command_index: usize,
1851) -> DrawCommand {
1852    let Some(node_id) = node_id else {
1853        return command;
1854    };
1855    let observation = crate::render_state::DrawObservationScope::new(node_id, command_index);
1856    match command {
1857        DrawCommand::Behind(draw) => DrawCommand::Behind(Rc::new(move |scope| {
1858            crate::render_state::observe_draw_reads(observation, || draw(scope))
1859        })),
1860        DrawCommand::WithContent(draw) => DrawCommand::WithContent(Rc::new(move |scope| {
1861            crate::render_state::observe_draw_reads(observation, || draw(scope))
1862        })),
1863        DrawCommand::Overlay(draw) => DrawCommand::Overlay(Rc::new(move |scope| {
1864            crate::render_state::observe_draw_reads(observation, || draw(scope))
1865        })),
1866    }
1867}
1868
1869fn draw_command_tag(cmd: &DrawCommand) -> u8 {
1870    match cmd {
1871        DrawCommand::Behind(_) => 0,
1872        DrawCommand::WithContent(_) => 1,
1873        DrawCommand::Overlay(_) => 2,
1874    }
1875}
1876
1877fn draw_command_closure_identity(cmd: &DrawCommand) -> *const () {
1878    match cmd {
1879        DrawCommand::Behind(f) | DrawCommand::WithContent(f) | DrawCommand::Overlay(f) => {
1880            Rc::as_ptr(f) as *const ()
1881        }
1882    }
1883}
1884
1885/// Element that wires draw commands into the modifier node chain.
1886#[derive(Clone)]
1887pub struct DrawCommandElement {
1888    commands: Vec<DrawCommand>,
1889}
1890
1891impl DrawCommandElement {
1892    pub fn new(command: DrawCommand) -> Self {
1893        Self {
1894            commands: vec![command],
1895        }
1896    }
1897
1898    pub fn from_commands(commands: Vec<DrawCommand>) -> Self {
1899        Self { commands }
1900    }
1901}
1902
1903impl std::fmt::Debug for DrawCommandElement {
1904    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1905        f.debug_struct("DrawCommandElement")
1906            .field("commands", &self.commands.len())
1907            .finish()
1908    }
1909}
1910
1911impl PartialEq for DrawCommandElement {
1912    fn eq(&self, other: &Self) -> bool {
1913        if self.commands.len() != other.commands.len() {
1914            return false;
1915        }
1916        self.commands
1917            .iter()
1918            .zip(other.commands.iter())
1919            .all(|(a, b)| {
1920                draw_command_tag(a) == draw_command_tag(b)
1921                    && draw_command_closure_identity(a) == draw_command_closure_identity(b)
1922            })
1923    }
1924}
1925
1926impl Eq for DrawCommandElement {}
1927
1928impl std::hash::Hash for DrawCommandElement {
1929    fn hash<H: Hasher>(&self, state: &mut H) {
1930        "draw_commands".hash(state);
1931        self.commands.len().hash(state);
1932        for command in &self.commands {
1933            draw_command_tag(command).hash(state);
1934            (draw_command_closure_identity(command) as usize).hash(state);
1935        }
1936    }
1937}
1938
1939impl ModifierNodeElement for DrawCommandElement {
1940    type Node = DrawCommandNode;
1941
1942    fn create(&self) -> Self::Node {
1943        DrawCommandNode::new(self.commands.clone())
1944    }
1945
1946    fn update(&self, node: &mut Self::Node) {
1947        node.commands = self.commands.clone();
1948    }
1949
1950    fn capabilities(&self) -> NodeCapabilities {
1951        NodeCapabilities::DRAW
1952    }
1953}
1954
1955// ============================================================================
1956// Offset Modifier Node
1957// ============================================================================
1958
1959/// Node that offsets its content by a fixed (x, y) amount.
1960///
1961/// Matches Kotlin: `OffsetNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
1962#[derive(Debug)]
1963pub struct OffsetNode {
1964    x: f32,
1965    y: f32,
1966    rtl_aware: bool,
1967    state: NodeState,
1968}
1969
1970impl OffsetNode {
1971    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
1972        Self {
1973            x,
1974            y,
1975            rtl_aware,
1976            state: NodeState::new(),
1977        }
1978    }
1979
1980    pub fn offset(&self) -> Point {
1981        Point {
1982            x: self.x,
1983            y: self.y,
1984        }
1985    }
1986
1987    pub fn rtl_aware(&self) -> bool {
1988        self.rtl_aware
1989    }
1990}
1991
1992impl DelegatableNode for OffsetNode {
1993    fn node_state(&self) -> &NodeState {
1994        &self.state
1995    }
1996}
1997
1998impl ModifierNode for OffsetNode {
1999    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2000        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2001    }
2002
2003    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2004        Some(self)
2005    }
2006
2007    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2008        Some(self)
2009    }
2010}
2011
2012impl LayoutModifierNode for OffsetNode {
2013    fn measure(
2014        &self,
2015        _context: &mut dyn ModifierNodeContext,
2016        measurable: &dyn Measurable,
2017        constraints: Constraints,
2018    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2019        // Offset doesn't affect measurement, just placement
2020        let placeable = measurable.measure(constraints);
2021
2022        // Return child size unchanged, but specify the offset for placement
2023        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2024            Size {
2025                width: placeable.width(),
2026                height: placeable.height(),
2027            },
2028            self.x, // Place child offset by x
2029            self.y, // Place child offset by y
2030        )
2031    }
2032
2033    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2034        measurable.min_intrinsic_width(height)
2035    }
2036
2037    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2038        measurable.max_intrinsic_width(height)
2039    }
2040
2041    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2042        measurable.min_intrinsic_height(width)
2043    }
2044
2045    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2046        measurable.max_intrinsic_height(width)
2047    }
2048}
2049
2050/// Element that creates and updates offset nodes.
2051///
2052/// Matches Kotlin: `OffsetElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Offset.kt
2053#[derive(Debug, Clone, PartialEq)]
2054pub struct OffsetElement {
2055    x: f32,
2056    y: f32,
2057    rtl_aware: bool,
2058}
2059
2060impl OffsetElement {
2061    pub fn new(x: f32, y: f32, rtl_aware: bool) -> Self {
2062        Self { x, y, rtl_aware }
2063    }
2064}
2065
2066impl Hash for OffsetElement {
2067    fn hash<H: Hasher>(&self, state: &mut H) {
2068        hash_f32_value(state, self.x);
2069        hash_f32_value(state, self.y);
2070        self.rtl_aware.hash(state);
2071    }
2072}
2073
2074impl ModifierNodeElement for OffsetElement {
2075    type Node = OffsetNode;
2076
2077    fn create(&self) -> Self::Node {
2078        OffsetNode::new(self.x, self.y, self.rtl_aware)
2079    }
2080
2081    fn update(&self, node: &mut Self::Node) {
2082        if node.x != self.x || node.y != self.y || node.rtl_aware != self.rtl_aware {
2083            node.x = self.x;
2084            node.y = self.y;
2085            node.rtl_aware = self.rtl_aware;
2086        }
2087    }
2088
2089    fn capabilities(&self) -> NodeCapabilities {
2090        NodeCapabilities::LAYOUT
2091    }
2092
2093    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2094        Some(InvalidationKind::Layout)
2095    }
2096}
2097
2098// ============================================================================
2099// Fractional Offset Modifier Node
2100// ============================================================================
2101
2102/// Node that offsets its content by a fraction of its own measured size.
2103///
2104/// There is no direct Jetpack Compose modifier equivalent; Compose's slide
2105/// transitions receive the measured size through a lambda instead. This node
2106/// backs `slide_in_vertically` / `slide_out_vertically` in
2107/// `AnimatedVisibility`, where the offset is expressed as a fraction of the
2108/// content height.
2109#[derive(Debug)]
2110pub struct FractionalOffsetNode {
2111    x_fraction: f32,
2112    y_fraction: f32,
2113    state: NodeState,
2114}
2115
2116impl FractionalOffsetNode {
2117    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2118        Self {
2119            x_fraction,
2120            y_fraction,
2121            state: NodeState::new(),
2122        }
2123    }
2124
2125    pub fn fractions(&self) -> Point {
2126        Point {
2127            x: self.x_fraction,
2128            y: self.y_fraction,
2129        }
2130    }
2131}
2132
2133impl DelegatableNode for FractionalOffsetNode {
2134    fn node_state(&self) -> &NodeState {
2135        &self.state
2136    }
2137}
2138
2139impl ModifierNode for FractionalOffsetNode {
2140    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2141        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2142    }
2143
2144    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2145        Some(self)
2146    }
2147
2148    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2149        Some(self)
2150    }
2151}
2152
2153impl LayoutModifierNode for FractionalOffsetNode {
2154    fn measure(
2155        &self,
2156        _context: &mut dyn ModifierNodeContext,
2157        measurable: &dyn Measurable,
2158        constraints: Constraints,
2159    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2160        // Offset doesn't affect measurement, just placement. The placement
2161        // offset is resolved against the measured content size.
2162        let placeable = measurable.measure(constraints);
2163
2164        cranpose_ui_layout::LayoutModifierMeasureResult::new(
2165            Size {
2166                width: placeable.width(),
2167                height: placeable.height(),
2168            },
2169            self.x_fraction * placeable.width(),
2170            self.y_fraction * placeable.height(),
2171        )
2172    }
2173
2174    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2175        measurable.min_intrinsic_width(height)
2176    }
2177
2178    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2179        measurable.max_intrinsic_width(height)
2180    }
2181
2182    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2183        measurable.min_intrinsic_height(width)
2184    }
2185
2186    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2187        measurable.max_intrinsic_height(width)
2188    }
2189}
2190
2191/// Element that creates and updates fractional offset nodes.
2192#[derive(Debug, Clone, PartialEq)]
2193pub struct FractionalOffsetElement {
2194    x_fraction: f32,
2195    y_fraction: f32,
2196}
2197
2198impl FractionalOffsetElement {
2199    pub fn new(x_fraction: f32, y_fraction: f32) -> Self {
2200        Self {
2201            x_fraction,
2202            y_fraction,
2203        }
2204    }
2205}
2206
2207impl Hash for FractionalOffsetElement {
2208    fn hash<H: Hasher>(&self, state: &mut H) {
2209        "fractional_offset".hash(state);
2210        hash_f32_value(state, self.x_fraction);
2211        hash_f32_value(state, self.y_fraction);
2212    }
2213}
2214
2215impl ModifierNodeElement for FractionalOffsetElement {
2216    type Node = FractionalOffsetNode;
2217
2218    fn create(&self) -> Self::Node {
2219        FractionalOffsetNode::new(self.x_fraction, self.y_fraction)
2220    }
2221
2222    fn update(&self, node: &mut Self::Node) {
2223        if node.x_fraction != self.x_fraction || node.y_fraction != self.y_fraction {
2224            node.x_fraction = self.x_fraction;
2225            node.y_fraction = self.y_fraction;
2226        }
2227    }
2228
2229    fn capabilities(&self) -> NodeCapabilities {
2230        NodeCapabilities::LAYOUT
2231    }
2232
2233    fn update_invalidation_kind(&self) -> Option<InvalidationKind> {
2234        Some(InvalidationKind::Layout)
2235    }
2236}
2237
2238// ============================================================================
2239// Fill Modifier Node
2240// ============================================================================
2241
2242/// Direction for fill modifiers (horizontal, vertical, or both).
2243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2244pub enum FillDirection {
2245    Horizontal,
2246    Vertical,
2247    Both,
2248}
2249
2250/// Node that fills the maximum available space in one or both dimensions.
2251///
2252/// Matches Kotlin: `FillNode` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2253#[derive(Debug)]
2254pub struct FillNode {
2255    direction: FillDirection,
2256    fraction: f32,
2257    state: NodeState,
2258}
2259
2260impl FillNode {
2261    pub fn new(direction: FillDirection, fraction: f32) -> Self {
2262        Self {
2263            direction,
2264            fraction,
2265            state: NodeState::new(),
2266        }
2267    }
2268
2269    pub fn direction(&self) -> FillDirection {
2270        self.direction
2271    }
2272
2273    pub fn fraction(&self) -> f32 {
2274        self.fraction
2275    }
2276}
2277
2278impl DelegatableNode for FillNode {
2279    fn node_state(&self) -> &NodeState {
2280        &self.state
2281    }
2282}
2283
2284impl ModifierNode for FillNode {
2285    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2286        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2287    }
2288
2289    fn as_layout_node(&self) -> Option<&dyn LayoutModifierNode> {
2290        Some(self)
2291    }
2292
2293    fn as_layout_node_mut(&mut self) -> Option<&mut dyn LayoutModifierNode> {
2294        Some(self)
2295    }
2296}
2297
2298impl LayoutModifierNode for FillNode {
2299    fn measure(
2300        &self,
2301        _context: &mut dyn ModifierNodeContext,
2302        measurable: &dyn Measurable,
2303        constraints: Constraints,
2304    ) -> cranpose_ui_layout::LayoutModifierMeasureResult {
2305        // Calculate the fill size based on constraints
2306        let (fill_width, child_min_width, child_max_width) = if self.direction
2307            != FillDirection::Vertical
2308            && constraints.max_width != f32::INFINITY
2309        {
2310            let width = (constraints.max_width * self.fraction)
2311                .round()
2312                .clamp(constraints.min_width, constraints.max_width);
2313            // Tight constraint for child on this axis
2314            (width, width, width)
2315        } else {
2316            (
2317                constraints.max_width,
2318                constraints.min_width,
2319                constraints.max_width,
2320            )
2321        };
2322
2323        let (fill_height, child_min_height, child_max_height) = if self.direction
2324            != FillDirection::Horizontal
2325            && constraints.max_height != f32::INFINITY
2326        {
2327            let height = (constraints.max_height * self.fraction)
2328                .round()
2329                .clamp(constraints.min_height, constraints.max_height);
2330            // Tight constraint for child on this axis
2331            (height, height, height)
2332        } else {
2333            (
2334                constraints.max_height,
2335                constraints.min_height,
2336                constraints.max_height,
2337            )
2338        };
2339
2340        let fill_constraints = Constraints {
2341            min_width: child_min_width,
2342            max_width: child_max_width,
2343            min_height: child_min_height,
2344            max_height: child_max_height,
2345        };
2346
2347        let placeable = measurable.measure(fill_constraints);
2348
2349        // Return the FILL size, not the child size.
2350        // The child is measured within tight constraints on the fill axis,
2351        // but we report the fill size to our parent.
2352        let result_width = if self.direction != FillDirection::Vertical
2353            && constraints.max_width != f32::INFINITY
2354        {
2355            fill_width
2356        } else {
2357            placeable.width()
2358        };
2359
2360        let result_height = if self.direction != FillDirection::Horizontal
2361            && constraints.max_height != f32::INFINITY
2362        {
2363            fill_height
2364        } else {
2365            placeable.height()
2366        };
2367
2368        cranpose_ui_layout::LayoutModifierMeasureResult::with_size(Size {
2369            width: result_width,
2370            height: result_height,
2371        })
2372    }
2373
2374    fn min_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2375        measurable.min_intrinsic_width(height)
2376    }
2377
2378    fn max_intrinsic_width(&self, measurable: &dyn Measurable, height: f32) -> f32 {
2379        measurable.max_intrinsic_width(height)
2380    }
2381
2382    fn min_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2383        measurable.min_intrinsic_height(width)
2384    }
2385
2386    fn max_intrinsic_height(&self, measurable: &dyn Measurable, width: f32) -> f32 {
2387        measurable.max_intrinsic_height(width)
2388    }
2389}
2390
2391/// Element that creates and updates fill nodes.
2392///
2393/// Matches Kotlin: `FillElement` in foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
2394#[derive(Debug, Clone, PartialEq)]
2395pub struct FillElement {
2396    direction: FillDirection,
2397    fraction: f32,
2398}
2399
2400impl FillElement {
2401    pub fn width(fraction: f32) -> Self {
2402        Self {
2403            direction: FillDirection::Horizontal,
2404            fraction,
2405        }
2406    }
2407
2408    pub fn height(fraction: f32) -> Self {
2409        Self {
2410            direction: FillDirection::Vertical,
2411            fraction,
2412        }
2413    }
2414
2415    pub fn size(fraction: f32) -> Self {
2416        Self {
2417            direction: FillDirection::Both,
2418            fraction,
2419        }
2420    }
2421}
2422
2423impl Hash for FillElement {
2424    fn hash<H: Hasher>(&self, state: &mut H) {
2425        self.direction.hash(state);
2426        hash_f32_value(state, self.fraction);
2427    }
2428}
2429
2430impl ModifierNodeElement for FillElement {
2431    type Node = FillNode;
2432
2433    fn create(&self) -> Self::Node {
2434        FillNode::new(self.direction, self.fraction)
2435    }
2436
2437    fn update(&self, node: &mut Self::Node) {
2438        if node.direction != self.direction || node.fraction != self.fraction {
2439            node.direction = self.direction;
2440            node.fraction = self.fraction;
2441        }
2442    }
2443
2444    fn capabilities(&self) -> NodeCapabilities {
2445        NodeCapabilities::LAYOUT
2446    }
2447}
2448
2449// ============================================================================
2450// Weight Modifier Node
2451// ============================================================================
2452
2453/// Node that records flex weight data for Row/Column parents.
2454#[derive(Debug)]
2455pub struct WeightNode {
2456    weight: f32,
2457    fill: bool,
2458    state: NodeState,
2459}
2460
2461impl WeightNode {
2462    pub fn new(weight: f32, fill: bool) -> Self {
2463        Self {
2464            weight,
2465            fill,
2466            state: NodeState::new(),
2467        }
2468    }
2469
2470    pub fn layout_weight(&self) -> LayoutWeight {
2471        LayoutWeight {
2472            weight: self.weight,
2473            fill: self.fill,
2474        }
2475    }
2476}
2477
2478impl DelegatableNode for WeightNode {
2479    fn node_state(&self) -> &NodeState {
2480        &self.state
2481    }
2482}
2483
2484impl ModifierNode for WeightNode {
2485    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2486        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2487    }
2488}
2489
2490/// Element that creates and updates weight nodes.
2491#[derive(Debug, Clone, PartialEq)]
2492pub struct WeightElement {
2493    weight: f32,
2494    fill: bool,
2495}
2496
2497impl WeightElement {
2498    pub fn new(weight: f32, fill: bool) -> Self {
2499        Self { weight, fill }
2500    }
2501}
2502
2503impl Hash for WeightElement {
2504    fn hash<H: Hasher>(&self, state: &mut H) {
2505        hash_f32_value(state, self.weight);
2506        self.fill.hash(state);
2507    }
2508}
2509
2510impl ModifierNodeElement for WeightElement {
2511    type Node = WeightNode;
2512
2513    fn create(&self) -> Self::Node {
2514        WeightNode::new(self.weight, self.fill)
2515    }
2516
2517    fn update(&self, node: &mut Self::Node) {
2518        if node.weight != self.weight || node.fill != self.fill {
2519            node.weight = self.weight;
2520            node.fill = self.fill;
2521        }
2522    }
2523
2524    fn capabilities(&self) -> NodeCapabilities {
2525        NodeCapabilities::LAYOUT
2526    }
2527}
2528
2529// ============================================================================
2530// Alignment Modifier Node
2531// ============================================================================
2532
2533/// Node that records alignment preferences for Box/Row/Column scopes.
2534#[derive(Debug)]
2535pub struct AlignmentNode {
2536    box_alignment: Option<Alignment>,
2537    column_alignment: Option<HorizontalAlignment>,
2538    row_alignment: Option<VerticalAlignment>,
2539    state: NodeState,
2540}
2541
2542impl AlignmentNode {
2543    pub fn new(
2544        box_alignment: Option<Alignment>,
2545        column_alignment: Option<HorizontalAlignment>,
2546        row_alignment: Option<VerticalAlignment>,
2547    ) -> Self {
2548        Self {
2549            box_alignment,
2550            column_alignment,
2551            row_alignment,
2552            state: NodeState::new(),
2553        }
2554    }
2555
2556    pub fn box_alignment(&self) -> Option<Alignment> {
2557        self.box_alignment
2558    }
2559
2560    pub fn column_alignment(&self) -> Option<HorizontalAlignment> {
2561        self.column_alignment
2562    }
2563
2564    pub fn row_alignment(&self) -> Option<VerticalAlignment> {
2565        self.row_alignment
2566    }
2567}
2568
2569impl DelegatableNode for AlignmentNode {
2570    fn node_state(&self) -> &NodeState {
2571        &self.state
2572    }
2573}
2574
2575impl ModifierNode for AlignmentNode {
2576    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2577        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2578    }
2579}
2580
2581/// Element that creates and updates alignment nodes.
2582#[derive(Debug, Clone, PartialEq)]
2583pub struct AlignmentElement {
2584    box_alignment: Option<Alignment>,
2585    column_alignment: Option<HorizontalAlignment>,
2586    row_alignment: Option<VerticalAlignment>,
2587}
2588
2589impl AlignmentElement {
2590    pub fn box_alignment(alignment: Alignment) -> Self {
2591        Self {
2592            box_alignment: Some(alignment),
2593            column_alignment: None,
2594            row_alignment: None,
2595        }
2596    }
2597
2598    pub fn column_alignment(alignment: HorizontalAlignment) -> Self {
2599        Self {
2600            box_alignment: None,
2601            column_alignment: Some(alignment),
2602            row_alignment: None,
2603        }
2604    }
2605
2606    pub fn row_alignment(alignment: VerticalAlignment) -> Self {
2607        Self {
2608            box_alignment: None,
2609            column_alignment: None,
2610            row_alignment: Some(alignment),
2611        }
2612    }
2613}
2614
2615impl Hash for AlignmentElement {
2616    fn hash<H: Hasher>(&self, state: &mut H) {
2617        if let Some(alignment) = self.box_alignment {
2618            state.write_u8(1);
2619            hash_alignment(state, alignment);
2620        } else {
2621            state.write_u8(0);
2622        }
2623        if let Some(alignment) = self.column_alignment {
2624            state.write_u8(1);
2625            hash_horizontal_alignment(state, alignment);
2626        } else {
2627            state.write_u8(0);
2628        }
2629        if let Some(alignment) = self.row_alignment {
2630            state.write_u8(1);
2631            hash_vertical_alignment(state, alignment);
2632        } else {
2633            state.write_u8(0);
2634        }
2635    }
2636}
2637
2638impl ModifierNodeElement for AlignmentElement {
2639    type Node = AlignmentNode;
2640
2641    fn create(&self) -> Self::Node {
2642        AlignmentNode::new(
2643            self.box_alignment,
2644            self.column_alignment,
2645            self.row_alignment,
2646        )
2647    }
2648
2649    fn update(&self, node: &mut Self::Node) {
2650        if node.box_alignment != self.box_alignment {
2651            node.box_alignment = self.box_alignment;
2652        }
2653        if node.column_alignment != self.column_alignment {
2654            node.column_alignment = self.column_alignment;
2655        }
2656        if node.row_alignment != self.row_alignment {
2657            node.row_alignment = self.row_alignment;
2658        }
2659    }
2660
2661    fn capabilities(&self) -> NodeCapabilities {
2662        NodeCapabilities::LAYOUT
2663    }
2664}
2665
2666// ============================================================================
2667// Intrinsic Size Modifier Node
2668// ============================================================================
2669
2670#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2671pub enum IntrinsicAxis {
2672    Width,
2673    Height,
2674}
2675
2676/// Node that records intrinsic sizing requests.
2677#[derive(Debug)]
2678pub struct IntrinsicSizeNode {
2679    axis: IntrinsicAxis,
2680    size: IntrinsicSize,
2681    state: NodeState,
2682}
2683
2684impl IntrinsicSizeNode {
2685    pub fn new(axis: IntrinsicAxis, size: IntrinsicSize) -> Self {
2686        Self {
2687            axis,
2688            size,
2689            state: NodeState::new(),
2690        }
2691    }
2692
2693    pub fn axis(&self) -> IntrinsicAxis {
2694        self.axis
2695    }
2696
2697    pub fn intrinsic_size(&self) -> IntrinsicSize {
2698        self.size
2699    }
2700}
2701
2702impl DelegatableNode for IntrinsicSizeNode {
2703    fn node_state(&self) -> &NodeState {
2704        &self.state
2705    }
2706}
2707
2708impl ModifierNode for IntrinsicSizeNode {
2709    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
2710        context.invalidate(cranpose_foundation::InvalidationKind::Layout);
2711    }
2712}
2713
2714/// Element that creates and updates intrinsic size nodes.
2715#[derive(Debug, Clone, PartialEq)]
2716pub struct IntrinsicSizeElement {
2717    axis: IntrinsicAxis,
2718    size: IntrinsicSize,
2719}
2720
2721impl IntrinsicSizeElement {
2722    pub fn width(size: IntrinsicSize) -> Self {
2723        Self {
2724            axis: IntrinsicAxis::Width,
2725            size,
2726        }
2727    }
2728
2729    pub fn height(size: IntrinsicSize) -> Self {
2730        Self {
2731            axis: IntrinsicAxis::Height,
2732            size,
2733        }
2734    }
2735}
2736
2737impl Hash for IntrinsicSizeElement {
2738    fn hash<H: Hasher>(&self, state: &mut H) {
2739        state.write_u8(match self.axis {
2740            IntrinsicAxis::Width => 0,
2741            IntrinsicAxis::Height => 1,
2742        });
2743        state.write_u8(match self.size {
2744            IntrinsicSize::Min => 0,
2745            IntrinsicSize::Max => 1,
2746        });
2747    }
2748}
2749
2750impl ModifierNodeElement for IntrinsicSizeElement {
2751    type Node = IntrinsicSizeNode;
2752
2753    fn create(&self) -> Self::Node {
2754        IntrinsicSizeNode::new(self.axis, self.size)
2755    }
2756
2757    fn update(&self, node: &mut Self::Node) {
2758        if node.axis != self.axis {
2759            node.axis = self.axis;
2760        }
2761        if node.size != self.size {
2762            node.size = self.size;
2763        }
2764    }
2765
2766    fn capabilities(&self) -> NodeCapabilities {
2767        NodeCapabilities::LAYOUT
2768    }
2769}
2770
2771#[cfg(test)]
2772#[path = "tests/modifier_nodes_tests.rs"]
2773mod tests;