1use oxframe_core::Signal;
8
9use crate::graph::{NodeData, NodeId, NodeKind, SceneGraph, measure};
10use crate::theme::{self, Step};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Side {
15 Left,
17 Right,
19 Top,
21 Bottom,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Edge {
28 Left,
30 Right,
32 Top,
34 Bottom,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum Span {
41 Px(f64),
43 Pct(f64),
45}
46
47impl From<f64> for Span {
48 fn from(px: f64) -> Self {
49 Self::Px(px)
50 }
51}
52
53pub trait Percent {
55 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
71pub fn pct(percent: impl Percent) -> Span {
77 Span::Pct(percent.percent())
78}
79
80#[derive(Clone, Copy)]
82pub(crate) enum Axis {
83 X,
84 Y,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct SafeArea {
91 pub left: f64,
93 pub top: f64,
95 pub right: f64,
97 pub bottom: f64,
99}
100
101impl SafeArea {
102 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 pub fn width(self) -> f64 {
123 (self.right - self.left).max(0.0)
124 }
125
126 pub fn height(self) -> f64 {
128 (self.bottom - self.top).max(0.0)
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
134pub enum Place {
135 NextTo {
137 other: NodeId,
139 side: Side,
141 gap: f64,
143 },
144 AlignTo {
146 other: NodeId,
148 edge: Edge,
150 },
151 ToEdge {
153 edge: Edge,
155 },
156 CenterIn {
158 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
185pub(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
205pub(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
261pub(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
350fn 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 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}