Skip to main content

cranpose_ui/
modifier_nodes.rs

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