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(), <r), 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(), <r), 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(), <r), 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}