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