Skip to main content

cranpose_ui/
modifier_nodes.rs

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