Skip to main content

oxframe_2d/
place.rs

1//! Positions relative to another node, the frame, or the safe area.
2//!
3//! A placement is stored on the node and resolved on every draw, so it
4//! follows a neighbor that moves or changes size. Percent sizes
5//! ([`pct`]) track the frame, or the parent when the parent has a fixed size.
6
7use oxframe_core::Signal;
8
9use crate::graph::{NodeData, NodeId, NodeKind, SceneGraph, measure};
10use crate::theme::{self, Step};
11
12/// Which side of a neighbor [`Node::next_to`](crate::Node::next_to) uses.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Side {
15    /// To the left.
16    Left,
17    /// To the right.
18    Right,
19    /// Above.
20    Top,
21    /// Below.
22    Bottom,
23}
24
25/// An edge of a node or of the safe area.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Edge {
28    /// Left edge.
29    Left,
30    /// Right edge.
31    Right,
32    /// Top edge.
33    Top,
34    /// Bottom edge.
35    Bottom,
36}
37
38/// A length in pixels, or a percentage of the frame (or of a sized parent).
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum Span {
41    /// An absolute length in pixels.
42    Px(f64),
43    /// A percentage, `100` meaning the whole reference size.
44    Pct(f64),
45}
46
47impl From<f64> for Span {
48    fn from(px: f64) -> Self {
49        Self::Px(px)
50    }
51}
52
53/// A number that can be a percentage. `pct(80)` and `pct(80.0)` are the same.
54pub trait Percent {
55    /// `100` is the whole reference size.
56    fn percent(self) -> f64;
57}
58
59impl Percent for f64 {
60    fn percent(self) -> f64 {
61        self
62    }
63}
64
65impl Percent for i32 {
66    fn percent(self) -> f64 {
67        f64::from(self)
68    }
69}
70
71/// `80` percent of the frame, or of the parent when it has a fixed size.
72///
73/// ```ignore
74/// Rect::new().width(pct(80)).height(300.0)
75/// ```
76pub fn pct(percent: impl Percent) -> Span {
77    Span::Pct(percent.percent())
78}
79
80/// Which axis a [`Span::Pct`] is a fraction of.
81#[derive(Clone, Copy)]
82pub(crate) enum Axis {
83    X,
84    Y,
85}
86
87/// The rectangle `to_edge` keeps inside, in scene coordinates (origin at
88/// the center, y down).
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct SafeArea {
91    /// Left edge.
92    pub left: f64,
93    /// Top edge.
94    pub top: f64,
95    /// Right edge.
96    pub right: f64,
97    /// Bottom edge.
98    pub bottom: f64,
99}
100
101impl SafeArea {
102    /// Insets for a frame. A portrait frame leaves room for the status bar
103    /// and the buttons of a vertical video; a landscape frame uses a fixed
104    /// margin.
105    pub fn for_frame(width: f64, height: f64) -> Self {
106        let (top, right, bottom, left) = if height > width {
107            (height * 0.12, width * 0.06, height * 0.18, width * 0.06)
108        } else {
109            (64.0, 64.0, 64.0, 64.0)
110        };
111        let hx = width / 2.0;
112        let hy = height / 2.0;
113        Self {
114            left: -hx + left,
115            top: -hy + top,
116            right: hx - right,
117            bottom: hy - bottom,
118        }
119    }
120
121    /// Width of the safe rectangle.
122    pub fn width(self) -> f64 {
123        (self.right - self.left).max(0.0)
124    }
125
126    /// Height of the safe rectangle.
127    pub fn height(self) -> f64 {
128        (self.bottom - self.top).max(0.0)
129    }
130}
131
132/// How a node is placed. Resolved every frame; not set by scene code directly.
133#[derive(Debug, Clone, Copy)]
134pub enum Place {
135    /// Outside `other`, on `side`, separated by `gap` pixels.
136    NextTo {
137        /// The neighbor.
138        other: NodeId,
139        /// Which side of the neighbor.
140        side: Side,
141        /// Pixels between the two boxes.
142        gap: f64,
143    },
144    /// This node's `edge` lines up with the neighbor's same edge.
145    AlignTo {
146        /// The neighbor.
147        other: NodeId,
148        /// The edge to share.
149        edge: Edge,
150    },
151    /// Against the safe area, on `edge`. The other axis stays where it is.
152    ToEdge {
153        /// Which edge of the safe area.
154        edge: Edge,
155    },
156    /// Same center as `other`. The view centers on the frame.
157    CenterIn {
158        /// The node to center on.
159        other: NodeId,
160    },
161}
162
163impl Place {
164    pub(crate) fn next_to(other: NodeId, side: Side) -> Self {
165        Self::NextTo {
166            other,
167            side,
168            gap: theme::current().space(Step::Md),
169        }
170    }
171
172    pub(crate) fn align_to(other: NodeId, edge: Edge) -> Self {
173        Self::AlignTo { other, edge }
174    }
175
176    pub(crate) fn to_edge(edge: Edge) -> Self {
177        Self::ToEdge { edge }
178    }
179
180    pub(crate) fn center_in(other: NodeId) -> Self {
181        Self::CenterIn { other }
182    }
183}
184
185/// A width or height signal. Percentages read the frame size, or the
186/// parent's fixed size when it has one, and follow that size.
187pub(crate) fn span_signal(
188    graph: &SceneGraph,
189    parent: NodeId,
190    axis: Axis,
191    span: Span,
192    name: String,
193) -> Signal<f64> {
194    match span {
195        Span::Px(v) => Signal::new(v).with_name(name),
196        Span::Pct(p) => {
197            let base = reference(graph, parent, axis);
198            let sig = Signal::new(0.0).with_name(name);
199            sig.bind(move || base() * p / 100.0);
200            sig
201        }
202    }
203}
204
205/// Like [`span_signal`] for a layout's optional fixed size.
206pub(crate) fn span_option(
207    graph: &SceneGraph,
208    parent: NodeId,
209    axis: Axis,
210    span: Option<Span>,
211    name: String,
212) -> Signal<Option<f64>> {
213    match span {
214        None => Signal::new(None).with_name(name),
215        Some(Span::Px(v)) => Signal::new(Some(v)).with_name(name),
216        Some(Span::Pct(p)) => {
217            let base = reference(graph, parent, axis);
218            let sig = Signal::new(None).with_name(name);
219            sig.bind(move || Some(base() * p / 100.0));
220            sig
221        }
222    }
223}
224
225fn reference(graph: &SceneGraph, parent: NodeId, axis: Axis) -> Box<dyn Fn() -> f64> {
226    let (fw, fh) = graph.size();
227    let frame = match axis {
228        Axis::X => f64::from(fw),
229        Axis::Y => f64::from(fh),
230    };
231    if parent == SceneGraph::ROOT {
232        return Box::new(move || frame);
233    }
234    let node = graph.node(parent);
235    match &node.kind {
236        NodeKind::Rect { width, height, .. } => {
237            let s = match axis {
238                Axis::X => width.clone(),
239                Axis::Y => height.clone(),
240            };
241            Box::new(move || s.get())
242        }
243        NodeKind::Img { width, height, .. } | NodeKind::Grid { width, height, .. } => {
244            let s = match axis {
245                Axis::X => width.clone(),
246                Axis::Y => height.clone(),
247            };
248            Box::new(move || s.get())
249        }
250        NodeKind::Layout { width, height, .. } => {
251            let s = match axis {
252                Axis::X => width.clone(),
253                Axis::Y => height.clone(),
254            };
255            Box::new(move || s.get().unwrap_or(frame))
256        }
257        _ => Box::new(move || frame),
258    }
259}
260
261/// Writes each node's resolved position into [`NodeData::placed`].
262pub(crate) fn resolve(nodes: &[NodeData], frame: (f64, f64)) {
263    let passes = nodes.len().clamp(1, 8);
264    for _ in 0..passes {
265        let mut changed = false;
266        for (i, n) in nodes.iter().enumerate() {
267            let id = NodeId(u32::try_from(i).unwrap_or(u32::MAX));
268            let (w, h) = measure(nodes, id);
269            let xy = match n.place {
270                None => (n.x.get(), n.y.get()),
271                Some(place) => placed(nodes, id, (w, h), place, frame),
272            };
273            let prev = n.placed.get();
274            if (prev.0 - xy.0).abs() > 1e-4 || (prev.1 - xy.1).abs() > 1e-4 {
275                n.placed.set(xy);
276                changed = true;
277            }
278        }
279        if !changed {
280            break;
281        }
282    }
283}
284
285fn placed(
286    nodes: &[NodeData],
287    id: NodeId,
288    size: (f64, f64),
289    place: Place,
290    frame: (f64, f64),
291) -> (f64, f64) {
292    let n = &nodes[id.0 as usize];
293    let (w, h) = size;
294    match place {
295        Place::ToEdge { edge } => {
296            let safe = SafeArea::for_frame(frame.0, frame.1);
297            let (mut cx, mut cy) = anchor_center(n, w, h, n.x.get(), n.y.get());
298            match edge {
299                Edge::Top => cy = safe.top + h / 2.0,
300                Edge::Bottom => cy = safe.bottom - h / 2.0,
301                Edge::Left => cx = safe.left + w / 2.0,
302                Edge::Right => cx = safe.right - w / 2.0,
303            }
304            from_center(n, w, h, cx, cy)
305        }
306        Place::CenterIn { other } => {
307            let (cx, cy) = scene_center(nodes, other);
308            let (cx, cy) = to_parent(nodes, id, cx, cy);
309            from_center(n, w, h, cx, cy)
310        }
311        Place::AlignTo { other, edge } => {
312            let (ox, oy) = scene_center(nodes, other);
313            let (ow, oh) = measure(nodes, other);
314            let (sx, sy) = scene_center(nodes, id);
315            let (mut cx, mut cy) = (sx, sy);
316            match edge {
317                Edge::Top => cy = oy - oh / 2.0 + h / 2.0,
318                Edge::Bottom => cy = oy + oh / 2.0 - h / 2.0,
319                Edge::Left => cx = ox - ow / 2.0 + w / 2.0,
320                Edge::Right => cx = ox + ow / 2.0 - w / 2.0,
321            }
322            let (cx, cy) = to_parent(nodes, id, cx, cy);
323            from_center(n, w, h, cx, cy)
324        }
325        Place::NextTo { other, side, gap } => {
326            let (ox, oy) = scene_center(nodes, other);
327            let (ow, oh) = measure(nodes, other);
328            let (cx, cy) = match side {
329                Side::Right => (ox + ow / 2.0 + gap + w / 2.0, oy),
330                Side::Left => (ox - ow / 2.0 - gap - w / 2.0, oy),
331                Side::Bottom => (ox, oy + oh / 2.0 + gap + h / 2.0),
332                Side::Top => (ox, oy - oh / 2.0 - gap - h / 2.0),
333            };
334            let (cx, cy) = to_parent(nodes, id, cx, cy);
335            from_center(n, w, h, cx, cy)
336        }
337    }
338}
339
340fn anchor_center(n: &NodeData, w: f64, h: f64, x: f64, y: f64) -> (f64, f64) {
341    let a = n.anchor.get();
342    (x - a.x * w / 2.0, y - a.y * h / 2.0)
343}
344
345fn from_center(n: &NodeData, w: f64, h: f64, cx: f64, cy: f64) -> (f64, f64) {
346    let a = n.anchor.get();
347    (cx + a.x * w / 2.0, cy + a.y * h / 2.0)
348}
349
350/// Center of a node in scene space. The view is the frame center.
351fn scene_center(nodes: &[NodeData], id: NodeId) -> (f64, f64) {
352    if id == SceneGraph::ROOT {
353        return (0.0, 0.0);
354    }
355    let n = &nodes[id.0 as usize];
356    let (w, h) = measure(nodes, id);
357    let (x, y) = n.placed.get();
358    let (mut cx, mut cy) = anchor_center(n, w, h, x, y);
359    let mut cur = n.parent;
360    while let Some(p) = cur {
361        if p == SceneGraph::ROOT {
362            break;
363        }
364        let parent = &nodes[p.0 as usize];
365        let (px, py) = parent.placed.get();
366        cx += px;
367        cy += py;
368        cur = parent.parent;
369    }
370    (cx, cy)
371}
372
373fn to_parent(nodes: &[NodeData], id: NodeId, mut x: f64, mut y: f64) -> (f64, f64) {
374    let mut cur = nodes[id.0 as usize].parent;
375    while let Some(p) = cur {
376        if p == SceneGraph::ROOT {
377            break;
378        }
379        let (px, py) = nodes[p.0 as usize].placed.get();
380        x -= px;
381        y -= py;
382        cur = nodes[p.0 as usize].parent;
383    }
384    (x, y)
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::builder::NodeBuilder;
391    use crate::{Edge, Rect, SceneGraph, Side, View, pct};
392
393    fn scene(w: u32, h: u32, f: impl FnOnce(View)) -> std::rc::Rc<SceneGraph> {
394        let graph = SceneGraph::new(w, h);
395        graph.enter(|| f(View::current()));
396        graph
397    }
398
399    fn bounds(g: &SceneGraph, name: &str) -> [f64; 4] {
400        g.inspect_nodes()
401            .into_iter()
402            .find(|n| n.name == name)
403            .unwrap()
404            .bounds
405    }
406
407    #[test]
408    fn next_to_follows_move_and_resize() {
409        let g = SceneGraph::new(1920, 1080);
410        g.enter(|| {
411            let view = View::current();
412            let card = view.add(Rect::new().width(200.0).height(100.0).name("card"));
413            view.add(
414                Rect::new()
415                    .size(20.0)
416                    .next_to(card, Side::Right)
417                    .name("mark"),
418            );
419            let gap = theme::current().space(Step::Md);
420            let b = bounds(&g, "mark");
421            assert!((b[0] - (100.0 + gap)).abs() < 1.0, "{b:?}");
422
423            card.x().set(80.0);
424            card.width().set(400.0);
425            let b = bounds(&g, "mark");
426            // card center 80, half width 200, then gap, then the mark's left edge.
427            assert!((b[0] - (80.0 + 200.0 + gap)).abs() < 1.0, "{b:?}");
428        });
429    }
430
431    #[test]
432    fn percent_width_and_to_edge_use_the_frame() {
433        let g = scene(1000, 500, |view| {
434            view.add(Rect::new().width(pct(80.0)).height(100.0).name("wide"));
435            view.add(Rect::new().size(100.0).to_edge(Edge::Top).name("title"));
436            assert!(view.safe_area().top < 0.0);
437            assert!(!view.is_portrait());
438        });
439        let wide = bounds(&g, "wide");
440        assert!((wide[2] - wide[0] - 800.0).abs() < 1.0, "{wide:?}");
441        let title = bounds(&g, "title");
442        let safe = SafeArea::for_frame(1000.0, 500.0);
443        assert!((title[1] - safe.top).abs() < 1.0, "{title:?} vs {safe:?}");
444    }
445
446    #[test]
447    fn portrait_safe_area_clears_the_social_chrome() {
448        let g = scene(1080, 1920, |view| {
449            assert!(view.is_portrait());
450            view.add(Rect::new().size(80.0).to_edge(Edge::Bottom).name("end"));
451        });
452        let end = bounds(&g, "end");
453        let safe = SafeArea::for_frame(1080.0, 1920.0);
454        assert!((end[3] - safe.bottom).abs() < 1.0, "{end:?}");
455        assert!(safe.bottom < 960.0 - 64.0);
456    }
457}