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