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