Skip to main content

cranpose_ui/modifier/
offset.rs

1use std::{
2    hash::{Hash, Hasher},
3    rc::Rc,
4};
5
6use cranpose_foundation::{
7    DelegatableNode, ModifierNode, ModifierNodeContext, ModifierNodeElement, NodeCapabilities,
8    NodeState,
9};
10
11use super::{GraphicsLayer, Modifier, Point, inspector_metadata};
12use crate::{
13    LayoutDirection,
14    modifier_nodes::{FractionalOffsetElement, GraphicsLayerNode, OffsetElement},
15};
16
17impl Modifier {
18    /// Offset the content by (x, y). The offsets can be positive or negative.
19    ///
20    /// This modifier is RTL-aware: positive x offsets move content right in LTR
21    /// and left in RTL layouts.
22    ///
23    /// Matches Kotlin: `Modifier.offset(x: Dp, y: Dp)`
24    ///
25    /// Example: `Modifier::empty().offset(10.0, 20.0)`
26    pub fn offset(self, x: f32, y: f32) -> Self {
27        let modifier = Self::with_element(OffsetElement::new(x, y, true)).with_inspector_metadata(
28            inspector_metadata("offset", move |info| {
29                info.add_offset_components("offsetX", "offsetY", Point { x, y });
30            }),
31        );
32        self.then(modifier)
33    }
34
35    /// Offset the content by (x, y) without considering layout direction.
36    ///
37    /// Positive x always moves content to the right regardless of RTL.
38    ///
39    /// Matches Kotlin: `Modifier.absoluteOffset(x: Dp, y: Dp)`
40    ///
41    /// Example: `Modifier::empty().absolute_offset(10.0, 20.0)`
42    pub fn absolute_offset(self, x: f32, y: f32) -> Self {
43        let modifier = Self::with_element(OffsetElement::new(x, y, false)).with_inspector_metadata(
44            inspector_metadata("absoluteOffset", move |info| {
45                info.add_offset_components("absoluteOffsetX", "absoluteOffsetY", Point { x, y });
46            }),
47        );
48        self.then(modifier)
49    }
50
51    /// Offset the content by a fraction of its own measured size.
52    ///
53    /// `x_fraction` / `y_fraction` are multiplied by the measured width /
54    /// height of the content when it is placed, so `offset_fraction(0.0,
55    /// -0.5)` moves the content up by half its own height. Like
56    /// [`Modifier::offset`], this only affects placement, not measurement.
57    ///
58    /// There is no direct Jetpack Compose equivalent; Compose's
59    /// `slideInVertically`/`slideOutVertically` receive the measured size via
60    /// a lambda instead. This modifier backs `slide_in_vertically` /
61    /// `slide_out_vertically` in `AnimatedVisibility`.
62    ///
63    /// Example: `Modifier::empty().offset_fraction(0.0, -0.5)`
64    pub fn offset_fraction(self, x_fraction: f32, y_fraction: f32) -> Self {
65        let modifier = Self::with_element(FractionalOffsetElement::new(x_fraction, y_fraction))
66            .with_inspector_metadata(inspector_metadata("offsetFraction", move |info| {
67                info.add_property("offsetFractionX", x_fraction.to_string());
68                info.add_property("offsetFractionY", y_fraction.to_string());
69            }));
70        self.then(modifier)
71    }
72
73    /// Offsets the content by what `offset` returns, in logical pixels:
74    /// Compose's `Modifier.offset { }`. The scene reads `offset` as it
75    /// draws, so a state it reads moves the content and where it takes
76    /// clicks without composing or measuring it again. A right-to-left
77    /// layout direction mirrors `x`.
78    ///
79    /// Example: `Modifier::empty().offset_with(move || Point::new(drag.get(), 0.0))`
80    pub fn offset_with(self, offset: impl Fn() -> Point + 'static) -> Self {
81        self.then_offset_px(OffsetPxElement::new(Rc::new(offset), true), "offset")
82    }
83
84    /// [`Modifier::offset_with`] without considering layout direction:
85    /// Compose's `Modifier.absoluteOffset { }`. Positive `x` always moves the
86    /// content to the right.
87    pub fn absolute_offset_with(self, offset: impl Fn() -> Point + 'static) -> Self {
88        self.then_offset_px(
89            OffsetPxElement::new(Rc::new(offset), false),
90            "absoluteOffset",
91        )
92    }
93
94    fn then_offset_px(self, element: OffsetPxElement, name: &'static str) -> Self {
95        let modifier =
96            Self::with_element(element).with_inspector_metadata(inspector_metadata(name, |info| {
97                info.add_property("lazy", "true");
98            }));
99        self.then(modifier)
100    }
101}
102
103// Moves its content by a closure's offset, which the scene reads as a layer
104// translation: Compose's `OffsetPxElement`, which places with a layer.
105#[derive(Clone)]
106pub(crate) struct OffsetPxElement {
107    offset: Rc<dyn Fn() -> Point>,
108    rtl_aware: bool,
109}
110
111impl OffsetPxElement {
112    fn new(offset: Rc<dyn Fn() -> Point>, rtl_aware: bool) -> Self {
113        Self { offset, rtl_aware }
114    }
115}
116
117impl std::fmt::Debug for OffsetPxElement {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("OffsetPxElement")
120            .field("rtl_aware", &self.rtl_aware)
121            .finish_non_exhaustive()
122    }
123}
124
125impl PartialEq for OffsetPxElement {
126    fn eq(&self, other: &Self) -> bool {
127        Rc::ptr_eq(&self.offset, &other.offset) && self.rtl_aware == other.rtl_aware
128    }
129}
130
131impl Eq for OffsetPxElement {}
132
133impl Hash for OffsetPxElement {
134    fn hash<H: Hasher>(&self, state: &mut H) {
135        Rc::as_ptr(&self.offset).cast::<()>().hash(state);
136        self.rtl_aware.hash(state);
137    }
138}
139
140impl ModifierNodeElement for OffsetPxElement {
141    type Node = OffsetPxNode;
142
143    fn create(&self) -> Self::Node {
144        OffsetPxNode {
145            layer: GraphicsLayerNode::new_lazy(translation(Rc::clone(&self.offset), false)),
146            offset: Rc::clone(&self.offset),
147            rtl_aware: self.rtl_aware,
148            layout_direction: LayoutDirection::Ltr,
149        }
150    }
151
152    fn update(&self, node: &mut Self::Node) {
153        if !Rc::ptr_eq(&node.offset, &self.offset) || node.rtl_aware != self.rtl_aware {
154            node.offset = Rc::clone(&self.offset);
155            node.rtl_aware = self.rtl_aware;
156            node.resolve_translation();
157        }
158    }
159
160    fn capabilities(&self) -> NodeCapabilities {
161        NodeCapabilities::DRAW
162    }
163
164    fn always_update(&self) -> bool {
165        true
166    }
167
168    fn auto_invalidate_on_update(&self) -> bool {
169        false
170    }
171}
172
173// The node of an [`OffsetPxElement`]: a lazy graphics layer whose
174// translation is the offset, with `x` mirrored right to left when it is
175// RTL aware.
176pub(crate) struct OffsetPxNode {
177    layer: GraphicsLayerNode,
178    offset: Rc<dyn Fn() -> Point>,
179    rtl_aware: bool,
180    layout_direction: LayoutDirection,
181}
182
183impl OffsetPxNode {
184    pub(crate) fn layer(&self) -> &GraphicsLayerNode {
185        &self.layer
186    }
187
188    pub(crate) fn set_layout_direction(&mut self, direction: LayoutDirection) {
189        if self.layout_direction != direction {
190            self.layout_direction = direction;
191            self.resolve_translation();
192        }
193    }
194
195    // Gives the layer a resolver for the current offset and direction, which
196    // draws the node again.
197    fn resolve_translation(&mut self) {
198        let mirror = self.rtl_aware && self.layout_direction.is_rtl();
199        self.layer
200            .set_lazy(translation(Rc::clone(&self.offset), mirror));
201    }
202}
203
204fn translation(offset: Rc<dyn Fn() -> Point>, mirror: bool) -> Rc<dyn Fn() -> GraphicsLayer> {
205    Rc::new(move || {
206        let offset = offset();
207        GraphicsLayer {
208            translation_x: if mirror { -offset.x } else { offset.x },
209            translation_y: offset.y,
210            ..GraphicsLayer::default()
211        }
212    })
213}
214
215impl DelegatableNode for OffsetPxNode {
216    fn node_state(&self) -> &NodeState {
217        self.layer.node_state()
218    }
219}
220
221impl ModifierNode for OffsetPxNode {
222    fn on_attach(&mut self, context: &mut dyn ModifierNodeContext) {
223        self.layer.on_attach(context);
224    }
225
226    fn on_detach(&mut self) {
227        self.layer.on_detach();
228    }
229}