Skip to main content

cranpose_ui/
modifier_nodes.rs

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