1use crate::text;
2use crate::widget::{Action, SLOT_SIZE, Style, Units, Widget, WidgetKind};
3
4mod flex;
5mod grid;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum FlexDir {
9 Row,
10 Column,
11}
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Align {
14 Start,
15 Center,
16 End,
17}
18
19use yog_gfx::Vec2;
20pub use yog_gfx::graphics::Rect;
21
22pub const SCROLLBAR_W: f32 = 6.0;
25pub const MIN_KNOB_H: f32 = 16.0;
27
28#[derive(Debug, Clone)]
29pub struct LayoutNode {
30 pub rect: Rect,
31 pub id: Option<String>,
32 pub on_click: Option<String>,
33 pub children: Vec<LayoutNode>,
34 pub enabled: bool,
35 pub focused: bool,
36 pub content_h: f32,
39 pub scroll_y: f32,
43 pub clip: bool,
45 pub scrollbar: bool,
47 pub action: Option<Action>,
49 pub visible: bool,
52 pub grid: Option<Box<GridLines>>,
54}
55
56#[derive(Debug, Clone, Default, PartialEq)]
59pub struct GridLines {
60 pub rows: Vec<(f32, f32)>,
62 pub cols: Vec<(f32, f32)>,
64 pub(crate) floor_rows: Vec<f32>,
68 pub(crate) floor_cols: Vec<f32>,
69}
70
71impl LayoutNode {
72 pub fn max_scroll(&self) -> f32 {
74 (self.content_h - self.rect.h).max(0.0)
75 }
76
77 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
80 let max = self.max_scroll();
81 if !self.scrollbar || max <= 0.0 {
82 return None;
83 }
84 let r = self.rect;
85 let track = Rect {
86 x: r.x + r.w - SCROLLBAR_W,
87 y: r.y,
88 w: SCROLLBAR_W,
89 h: r.h,
90 };
91 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
92 let knob = Rect {
93 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
94 h: knob_h,
95 ..track
96 };
97 Some((track, knob))
98 }
99
100 fn translate_y(&mut self, dy: f32) {
101 self.rect.y += dy;
102 if let Some(g) = &mut self.grid {
103 for r in &mut g.rows {
104 r.0 += dy;
105 }
106 }
107 for c in &mut self.children {
108 c.translate_y(dy);
109 }
110 }
111}
112
113impl Default for LayoutNode {
114 fn default() -> Self {
115 Self {
116 rect: Rect::default(),
117 id: None,
118 on_click: None,
119 children: Vec::new(),
120 enabled: true,
121 focused: false,
122 content_h: 0.0,
123 scroll_y: 0.0,
124 clip: false,
125 scrollbar: false,
126 action: None,
127 visible: true,
128 grid: None,
129 }
130 }
131}
132
133pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
150 let mut node = new_node(widget);
151 if widget.visible {
152 let room = Room::new(avail_w, avail_h, avail_w, avail_h);
153 layout_widget(widget, &mut node, Vec2::ZERO, room, Depth::Full);
154 }
155 node
156}
157
158const UNBOUNDED: f32 = f32::MAX;
161
162const NO_WRAP_W: f32 = 4096.0;
165const TEXT_BOX_W: f32 = 100.0;
167const TEXT_BOX_CHROME_H: f32 = 7.0;
169
170fn bounded(v: f32) -> bool {
171 v < UNBOUNDED / 2.0
172}
173
174fn resolve_size(u: Units, basis: f32) -> Option<f32> {
176 match u {
177 Units::Percent(_) if !bounded(basis) => None,
178 _ => u.resolve(basis),
179 }
180}
181
182fn new_node(w: &Widget) -> LayoutNode {
183 LayoutNode {
184 id: w.id.clone(),
185 on_click: w.on_click.clone(),
186 enabled: w.enabled,
187 focused: w.focused,
188 action: w.action,
189 visible: w.visible,
190 ..Default::default()
191 }
192}
193
194#[derive(Clone, Copy, PartialEq)]
196enum Depth {
197 Size,
202 Full,
204}
205
206#[derive(Clone, Copy)]
210struct Room {
211 max_w: f32,
212 max_h: f32,
213 basis_w: f32,
214 basis_h: f32,
215}
216
217impl Room {
218 fn new(max_w: f32, max_h: f32, basis_w: f32, basis_h: f32) -> Room {
219 Room {
220 max_w,
221 max_h,
222 basis_w,
223 basis_h,
224 }
225 }
226}
227
228fn measure(w: &Widget, node: &mut LayoutNode, room: Room) {
230 layout_widget(w, node, Vec2::ZERO, room, Depth::Size);
231}
232
233fn layout_widget(w: &Widget, node: &mut LayoutNode, at: Vec2, room: Room, depth: Depth) {
235 let s = &w.style;
236 let Room { max_w, max_h, .. } = room;
237 let has_children = is_container(w);
238 let explicit_w = resolve_size(s.w, room.basis_w).map(|v| v.min(max_w));
239 let explicit_h = resolve_size(s.h, room.basis_h).map(|v| v.min(max_h));
240 let (ww, hh) = if has_children {
241 (explicit_w.unwrap_or(max_w), explicit_h.unwrap_or(max_h))
242 } else {
243 let (cw, ch) = leaf_size(w, explicit_w.unwrap_or(max_w), room);
245 (explicit_w.unwrap_or(cw), explicit_h.unwrap_or(ch))
246 };
247
248 node.rect = Rect {
249 x: at.x,
250 y: at.y,
251 w: ww,
252 h: hh,
253 };
254 node.grid = None;
255 node.content_h = hh;
256 node.scroll_y = 0.0;
257 node.clip = s.clip;
258 node.scrollbar = s.scrollbar;
259
260 if has_children {
261 let shrink = Shrink {
262 w: explicit_w.is_none(),
263 h: explicit_h.is_none(),
264 max_w,
265 max_h,
266 };
267 layout_children(w, node, Some(shrink), depth);
268 }
269}
270
271fn leaf_size(w: &Widget, ww: f32, room: Room) -> (f32, f32) {
275 let s = &w.style;
276 let Room { max_w, max_h, .. } = room;
277 let (mut ww, mut hh) = (ww, max_h);
278 let fit_w = |content: f32| (content + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
279 let fit_h = |content: f32| (content + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
280 match &w.kind {
281 WidgetKind::Label(t) | WidgetKind::Button(t) => {
282 let avail_w = (ww - s.pad[1] - s.pad[3]).max(0.0);
284 if avail_w < NO_WRAP_W && !s.no_wrap {
287 ww = fit_w(avail_w);
288 hh = fit_h(text::text_height_in(t, avail_w, s.font_scale, &s.font));
289 } else {
290 ww = fit_w(text::str_width_in(t, s.font_scale, &s.font));
291 hh = fit_h(text::LINE_H * s.font_scale);
292 }
293 }
294 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
295 ww = fit_w(SLOT_SIZE);
296 hh = fit_h(SLOT_SIZE);
297 }
298 WidgetKind::Spacer | WidgetKind::Surface(_) => {
299 ww = s.min_w.max(1.0).min(max_w);
300 hh = s.min_h.max(1.0).min(max_h);
301 }
302 WidgetKind::Panel(_) | WidgetKind::Grid(_) => {
305 if !bounded(ww) {
306 ww = s.min_w;
307 }
308 if !bounded(hh) {
309 hh = s.min_h;
310 }
311 }
312 WidgetKind::TextBox(_) => {
315 if !bounded(ww) {
316 ww = TEXT_BOX_W;
317 }
318 ww = ww.max(s.min_w).min(max_w);
319 hh = fit_h(text::LINE_H * s.font_scale + TEXT_BOX_CHROME_H);
320 }
321 WidgetKind::Image(image) => {
322 let Vec2 { x: nw, y: nh } = crate::image::natural_size(image);
323 ww = fit_w(nw);
324 hh = fit_h(nh);
325 }
326 }
327 (ww, hh)
328}
329
330fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
333 node.rect.x = x;
334 node.rect.y = y;
335 node.content_h = node.rect.h;
336 if is_container(w) {
337 layout_children(w, node, None, Depth::Full);
338 }
339}
340
341fn is_container(w: &Widget) -> bool {
343 !w.children.is_empty() || matches!(w.kind, WidgetKind::Grid(_))
344}
345
346struct Shrink {
349 w: bool,
350 h: bool,
351 max_w: f32,
352 max_h: f32,
353}
354
355fn place(child: &Widget, cn: &mut LayoutNode, x: f32, y: f32, depth: Depth) {
358 match depth {
359 Depth::Full => arrange(child, cn, x, y),
360 Depth::Size => {
361 cn.rect.x = x;
362 cn.rect.y = y;
363 }
364 }
365}
366
367fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>, depth: Depth) {
371 match &w.kind {
372 WidgetKind::Grid(spec) => grid::layout(w, spec, node, shrink, depth),
373 _ => flex::layout(w, node, shrink, depth),
374 }
375}
376
377fn measure_aligned(w: &Widget, node: &mut LayoutNode, room: Room) {
381 if bounded(room.max_w) && w.style.w.is_auto() {
382 measure(
383 w,
384 node,
385 Room {
386 max_w: UNBOUNDED,
387 ..room
388 },
389 );
390 if node.rect.w <= room.max_w {
391 return;
392 }
393 }
394 measure(w, node, room);
395}
396
397fn vis_margin(c: &Widget, side: usize) -> f32 {
399 if c.visible { c.style.margin[side] } else { 0.0 }
400}
401
402fn less(v: f32, by: f32) -> f32 {
404 if bounded(v) {
405 (v - by).max(0.0)
406 } else {
407 UNBOUNDED
408 }
409}
410
411fn finish(
414 w: &Widget,
415 node: &mut LayoutNode,
416 mut child_nodes: Vec<LayoutNode>,
417 shrink: Option<Shrink>,
418 bar_w: f32,
419) {
420 let s = &w.style;
421 let Rect { x, y, .. } = node.rect;
422 let extent = |f: &dyn Fn(&Widget, &LayoutNode) -> f32| -> f32 {
423 w.children
424 .iter()
425 .zip(&child_nodes)
426 .map(|(c, n)| f(c, n))
427 .fold(0.0f32, f32::max)
428 };
429 let ch = extent(&|c, n| n.rect.y - y + n.rect.h + vis_margin(c, 2));
430 if let Some(fit) = shrink {
431 if fit.w {
432 let cw = extent(&|c, n| n.rect.x - x + n.rect.w + vis_margin(c, 1));
433 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
434 }
435 if fit.h {
436 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
437 }
438 }
439
440 scroll(node, s, ch + s.pad[2], &mut child_nodes);
442 node.children = child_nodes;
443}
444
445fn scroll(node: &mut LayoutNode, s: &Style, content_h: f32, children: &mut [LayoutNode]) {
448 node.content_h = content_h;
449 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
450 if node.scroll_y > 0.0 {
451 for c in children {
452 c.translate_y(-node.scroll_y);
453 }
454 }
455}
456
457fn content_box(s: &Style, rect: Rect) -> (f32, f32, f32) {
461 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
462 let content_w = if bounded(rect.w) {
463 (rect.w - s.pad[1] - s.pad[3] - bar_w).max(0.0)
464 } else {
465 UNBOUNDED
466 };
467 let content_h = if bounded(rect.h) {
468 (rect.h - s.pad[0] - s.pad[2]).max(0.0)
469 } else {
470 UNBOUNDED
471 };
472 (content_w, content_h, bar_w)
473}
474
475pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
479 if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
480 return None;
481 }
482 for child in node.children.iter().rev() {
483 if let Some(hit) = hit_test(child, mx, my) {
484 return Some(hit);
485 }
486 }
487 if node.is_clickable() {
488 Some(node)
489 } else {
490 None
491 }
492}
493
494pub fn hit_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
497 fn walk(node: &LayoutNode, mx: f32, my: f32, path: &mut Vec<u16>) -> bool {
498 if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
499 return false;
500 }
501 for (i, child) in node.children.iter().enumerate().rev() {
502 path.push(i as u16);
503 if walk(child, mx, my, path) {
504 return true;
505 }
506 path.pop();
507 }
508 node.is_clickable()
509 }
510 let mut path = Vec::new();
511 walk(node, mx, my, &mut path).then_some(path)
512}
513
514pub fn deepest_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
517 if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
518 return None;
519 }
520 let mut path = Vec::new();
521 let mut n = node;
522 'outer: loop {
523 for (i, child) in n.children.iter().enumerate().rev() {
524 if child.visible && child.rect.contains(Vec2::new(mx, my)) {
525 path.push(i as u16);
526 n = child;
527 continue 'outer;
528 }
529 }
530 return Some(path);
531 }
532}
533
534pub fn node_at<'a>(node: &'a LayoutNode, path: &[u16]) -> Option<&'a LayoutNode> {
536 let mut n = node;
537 for &i in path {
538 n = n.children.get(i as usize)?;
539 }
540 Some(n)
541}
542
543pub fn node_at_mut<'a>(node: &'a mut LayoutNode, path: &[u16]) -> Option<&'a mut LayoutNode> {
545 let mut n = node;
546 for &i in path {
547 n = n.children.get_mut(i as usize)?;
548 }
549 Some(n)
550}
551
552impl LayoutNode {
553 pub fn is_clickable(&self) -> bool {
556 self.visible && self.enabled && (self.action.is_some() || self.on_click.is_some())
557 }
558}
559
560pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
565 if node.id.as_deref() == Some(id) {
566 return Some(node.rect);
567 }
568 node.children.iter().find_map(|c| find_by_id(c, id))
569}
570
571pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
574 if node.id.as_deref() == Some(id) {
575 return Some(node);
576 }
577 node.children.iter().find_map(|c| find_node(c, id))
578}
579
580pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
582 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
583 for child in &mut node.children {
584 set_focus(child, focused_id);
585 }
586}
587
588#[cfg(test)]
589mod tests;