Skip to main content

frus_widgets/
positioned.rs

1//! [`Positioned`]: places a layer of a [`crate::Stack`] against the stack's own edges,
2//! instead of filling it.
3//!
4//! A badge on a corner, a caption over an image, a button floating above content — all of
5//! them are a stack with one layer pinned somewhere. Without this a layer can only fill
6//! the box, and the only way to put something in a corner was to give it a transparent
7//! wrapper the size of the stack and align inside it.
8//!
9//! Each edge is optional, and what is given decides both the **size** and the **place**,
10//! exactly as in the reference:
11//!
12//! - `left` **and** `right` → the width is what is between them;
13//! - one of them plus [`Positioned::width`] → that width, at that edge;
14//! - neither → the child's own width, placed by the stack's alignment.
15//!
16//! The vertical axis works the same way with `top`, `bottom` and [`Positioned::height`].
17//! Giving all three of a triple is a contradiction, and the extra one is ignored: the two
18//! edges win, since they are what says where the box is.
19//!
20//! ```ignore
21//! Stack::new()
22//!     .layer(photo)
23//!     .layer(Positioned::new(badge).top(8.0).right(8.0))
24//! ```
25
26use frus_layout::Style;
27
28use crate::widget::Widget;
29
30/// What a [`Positioned`] pins, in logical pixels from the stack's edges. `None` on an
31/// edge means "not pinned there".
32#[derive(Clone, Copy, Debug, Default, PartialEq)]
33pub struct Positioning {
34    /// The distance from the stack's left edge, or `None` to leave that edge free.
35    pub left: Option<f32>,
36    /// The distance from the stack's top edge, or `None` to leave that edge free.
37    pub top: Option<f32>,
38    /// The distance from the stack's right edge, or `None` to leave that edge free.
39    pub right: Option<f32>,
40    /// The distance from the stack's bottom edge, or `None` to leave that edge free.
41    pub bottom: Option<f32>,
42    /// A fixed width, or `None` to take the one the edges and the content leave.
43    pub width: Option<f32>,
44    /// A fixed height, or `None` to take the one the edges and the content leave.
45    pub height: Option<f32>,
46    /// The distance from the stack's **start** edge — the left in a left-to-right script,
47    /// the right in a right-to-left one — or `None` (milestone 589).
48    pub start: Option<f32>,
49    /// The distance from the stack's **end** edge, or `None`.
50    pub end: Option<f32>,
51}
52
53impl Positioning {
54    /// The same pins with `start` and `end` made physical for the reading direction: the
55    /// start is the left in a left-to-right script and the right in a right-to-left one. An
56    /// edge given physically as well keeps its own value.
57    pub fn for_direction(self, rtl: bool) -> Self {
58        let (from_left, from_right) = if rtl {
59            (self.end, self.start)
60        } else {
61            (self.start, self.end)
62        };
63        Self {
64            left: self.left.or(from_left),
65            right: self.right.or(from_right),
66            start: None,
67            end: None,
68            ..self
69        }
70    }
71
72    /// The width this asks for in a stack `available` wide, and `None` when it asks for
73    /// the child's own.
74    pub fn resolved_width(&self, available: f32) -> Option<f32> {
75        match (self.left, self.right) {
76            (Some(l), Some(r)) => Some((available - l - r).max(0.0)),
77            _ => self.width,
78        }
79    }
80
81    /// The height this asks for in a stack `available` tall, and `None` when it asks for
82    /// the child's own.
83    pub fn resolved_height(&self, available: f32) -> Option<f32> {
84        match (self.top, self.bottom) {
85            (Some(t), Some(b)) => Some((available - t - b).max(0.0)),
86            _ => self.height,
87        }
88    }
89
90    /// Where the box goes on one axis, given the stack's extent, the child's own and the
91    /// share of the free space the stack's alignment asks for.
92    ///
93    /// The near edge wins if it is pinned; failing that the far edge places the box by
94    /// its own size; failing both, the stack aligns it.
95    pub fn place(
96        near: Option<f32>,
97        far: Option<f32>,
98        available: f32,
99        own: f32,
100        fraction: f32,
101    ) -> f32 {
102        match (near, far) {
103            (Some(n), _) => n,
104            (None, Some(f)) => available - f - own,
105            (None, None) => (available - own).max(0.0) * fraction,
106        }
107    }
108}
109
110/// A layer pinned against a [`crate::Stack`]'s edges. See the module documentation.
111pub struct Positioned<Msg = crate::callback::Callback> {
112    inner: Box<dyn Widget<Msg>>,
113    spec: Positioning,
114}
115
116impl<Msg> Positioned<Msg> {
117    /// Wraps a layer, pinned nowhere yet — which on its own is the stack's alignment at
118    /// the child's natural size.
119    pub fn new(inner: impl Widget<Msg> + 'static) -> Self {
120        Self {
121            inner: Box::new(inner),
122            spec: Positioning::default(),
123        }
124    }
125
126    /// Distance from the stack's left edge — the left in either reading direction; see
127    /// [`start`](Self::start) for one that follows the script.
128    pub fn left(mut self, px: f32) -> Self {
129        self.spec.left = Some(px);
130        self
131    }
132
133    /// Distance from the stack's top edge.
134    pub fn top(mut self, px: f32) -> Self {
135        self.spec.top = Some(px);
136        self
137    }
138
139    /// Distance from the stack's right edge.
140    pub fn right(mut self, px: f32) -> Self {
141        self.spec.right = Some(px);
142        self
143    }
144
145    /// Distance from the stack's bottom edge.
146    pub fn bottom(mut self, px: f32) -> Self {
147        self.spec.bottom = Some(px);
148        self
149    }
150
151    /// Distance from the stack's **start** edge: the left in a left-to-right script, the
152    /// right in a right-to-left one (milestone 589). A `left` or a `right` set as well wins
153    /// on its own side.
154    pub fn start(mut self, px: f32) -> Self {
155        self.spec.start = Some(px);
156        self
157    }
158
159    /// Distance from the stack's **end** edge: the right in a left-to-right script, the left
160    /// in a right-to-left one.
161    pub fn end(mut self, px: f32) -> Self {
162        self.spec.end = Some(px);
163        self
164    }
165
166    /// An explicit width, used when only one horizontal edge is pinned.
167    pub fn width(mut self, px: f32) -> Self {
168        self.spec.width = Some(px);
169        self
170    }
171
172    /// An explicit height, used when only one vertical edge is pinned.
173    pub fn height(mut self, px: f32) -> Self {
174        self.spec.height = Some(px);
175        self
176    }
177
178    /// Pins all four edges: the layer fills the stack inset by `px` on every side.
179    pub fn inset(self, px: f32) -> Self {
180        self.left(px).top(px).right(px).bottom(px)
181    }
182
183    /// **A layer the size of the stack** — the reference's `Positioned.fill`: every edge
184    /// pinned at nought.
185    pub fn fill(inner: impl Widget<Msg> + 'static) -> Self {
186        Self::new(inner).inset(0.0)
187    }
188
189    /// **A layer at `rect`**, in the stack's own coordinates — the reference's
190    /// `Positioned.fromRect`: pinned left and top, with that width and height.
191    pub fn from_rect(inner: impl Widget<Msg> + 'static, rect: frus_core::Rect) -> Self {
192        Self::new(inner)
193            .left(rect.x)
194            .top(rect.y)
195            .width(rect.width)
196            .height(rect.height)
197    }
198
199    /// **A layer inset from each edge of the stack by its own amount** — the reference's
200    /// `Positioned.fromRelativeRect`: `edges` are the distances from the left, top, right
201    /// and bottom edges.
202    pub fn from_relative_rect(inner: impl Widget<Msg> + 'static, edges: frus_core::Insets) -> Self {
203        Self::new(inner)
204            .left(edges.left)
205            .top(edges.top)
206            .right(edges.right)
207            .bottom(edges.bottom)
208    }
209
210    /// It does not touch the box — the stack reads the pins and lays the layer out
211    /// against them, which a style cannot express.
212    fn restyle(&self, base: Style) -> Style {
213        base
214    }
215}
216
217crate::transparent::forward_transparent!(Positioned {
218    /// Claimed: the pins are the whole of what this wrapper adds.
219    fn positioned(&self) -> Option<crate::positioned::Positioning> {
220        Some(self.spec)
221    }
222
223    /// Forwarded: pinning a widget says nothing about which widget it is.
224    fn key(&self) -> Option<u64> {
225        self.inner.key()
226    }
227
228    /// Forwarded too: a place is not a palette.
229    fn theme_override(
230        &self,
231        inherited: &crate::theme::Theme,
232    ) -> Option<Box<crate::theme::Theme>> {
233        self.inner.theme_override(inherited)
234    }
235
236    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
237    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
238        self.inner.media_override(inherited)
239    }
240    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
241        self.inner.scaffold_override()
242    }
243    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
244        self.inner.scroll_behavior_override()
245    }
246    /// Forwarded: a wrapper around a form is still that form (milestone 512).
247    fn autofill_group(&self) -> bool {
248        self.inner.autofill_group()
249    }
250    /// Forwarded: the area's bar is the area's, whatever wraps it.
251    fn area_toolbar(
252        &self,
253        context: crate::ToolbarContext,
254    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
255        self.inner.area_toolbar(context)
256    }
257});
258
259/// Start and end pins follow the reading direction; left and right do not (milestone 589).
260#[cfg(test)]
261mod direction_tests {
262    use super::*;
263    use crate::{build_ui, AnimatedPositioned, Container, Runtime, Stack, Theme};
264    use frus_core::{Color, Curve, Primitive, Size};
265
266    const RED: Color = Color::rgb(1.0, 0.0, 0.0);
267
268    fn square() -> Container<()> {
269        Container::new().width(20.0).height(20.0).color(RED)
270    }
271
272    /// Where the square landed, across a 100-px stack.
273    fn x_of(layer: impl Widget<()> + 'static, theme: &Theme) -> f32 {
274        let stack = Stack::<()>::new().width(100.0).height(100.0).layer(layer);
275        build_ui(&stack, Size::new(100.0, 100.0), &Runtime::default(), theme)
276            .scene()
277            .primitives()
278            .iter()
279            .find_map(|p| match p {
280                Primitive::Rect { rect, color, .. } if *color == RED => Some(rect.x),
281                _ => None,
282            })
283            .expect("the square")
284    }
285
286    /// **A start pin is on the left in left to right and on the right in right to left**;
287    /// an end pin the other way round; a left pin on the left in both.
288    #[test]
289    fn start_and_end_follow_the_script_and_left_does_not() {
290        let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
291        let start = || Positioned::new(square()).start(10.0).top(0.0);
292        assert_eq!(x_of(start(), &ltr), 10.0);
293        assert_eq!(x_of(start(), &rtl), 70.0, "10 from the right");
294        let end = || Positioned::new(square()).end(10.0).top(0.0);
295        assert_eq!(x_of(end(), &ltr), 70.0);
296        assert_eq!(x_of(end(), &rtl), 10.0);
297        let left = || Positioned::new(square()).left(10.0).top(0.0);
298        assert_eq!(x_of(left(), &rtl), 10.0, "a left pin is on the left");
299        // A physical pin given as well wins on its own side.
300        let both = || Positioned::new(square()).start(30.0).left(10.0).top(0.0);
301        assert_eq!(x_of(both(), &ltr), 10.0);
302    }
303
304    /// **An animated layer takes start and end too**, and its moving pins carry them.
305    #[test]
306    fn an_animated_layer_follows_the_script() {
307        let rtl = Theme::default().rtl();
308        let layer = || {
309            AnimatedPositioned::new(0.2, Curve::Linear, square())
310                .start(10.0)
311                .top(0.0)
312        };
313        assert_eq!(x_of(layer(), &rtl), 70.0);
314        let stack = Stack::<()>::new().width(100.0).height(100.0).layer(layer());
315        let mut rt = Runtime::default();
316        rt.advance_pins(&stack, 0.0);
317        let pinned = (0..8)
318            .map(|i| crate::interaction::WidgetId::ROOT.child(i))
319            .find_map(|id| rt.anim_pins(id))
320            .expect("the layer's pins");
321        assert_eq!(pinned.start, Some(10.0));
322        assert_eq!(pinned.for_direction(true).right, Some(10.0));
323    }
324}