use crate::text;
use crate::widget::{Action, SLOT_SIZE, Style, Units, Widget, WidgetKind};
mod flex;
mod grid;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FlexDir {
Row,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Align {
Start,
Center,
End,
}
use yog_gfx::Vec2;
pub use yog_gfx::graphics::Rect;
pub const SCROLLBAR_W: f32 = 6.0;
pub const MIN_KNOB_H: f32 = 16.0;
#[derive(Debug, Clone)]
pub struct LayoutNode {
pub rect: Rect,
pub id: Option<String>,
pub on_click: Option<String>,
pub children: Vec<LayoutNode>,
pub enabled: bool,
pub focused: bool,
pub content_h: f32,
pub scroll_y: f32,
pub clip: bool,
pub scrollbar: bool,
pub action: Option<Action>,
pub visible: bool,
pub grid: Option<Box<GridLines>>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GridLines {
pub rows: Vec<(f32, f32)>,
pub cols: Vec<(f32, f32)>,
pub(crate) floor_rows: Vec<f32>,
pub(crate) floor_cols: Vec<f32>,
}
impl LayoutNode {
pub fn max_scroll(&self) -> f32 {
(self.content_h - self.rect.h).max(0.0)
}
pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
let max = self.max_scroll();
if !self.scrollbar || max <= 0.0 {
return None;
}
let r = self.rect;
let track = Rect {
x: r.x + r.w - SCROLLBAR_W,
y: r.y,
w: SCROLLBAR_W,
h: r.h,
};
let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
let knob = Rect {
y: r.y + (r.h - knob_h) * (self.scroll_y / max),
h: knob_h,
..track
};
Some((track, knob))
}
fn translate_y(&mut self, dy: f32) {
self.rect.y += dy;
if let Some(g) = &mut self.grid {
for r in &mut g.rows {
r.0 += dy;
}
}
for c in &mut self.children {
c.translate_y(dy);
}
}
}
impl Default for LayoutNode {
fn default() -> Self {
Self {
rect: Rect::default(),
id: None,
on_click: None,
children: Vec::new(),
enabled: true,
focused: false,
content_h: 0.0,
scroll_y: 0.0,
clip: false,
scrollbar: false,
action: None,
visible: true,
grid: None,
}
}
}
pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
let mut node = new_node(widget);
if widget.visible {
let room = Room::new(avail_w, avail_h, avail_w, avail_h);
layout_widget(widget, &mut node, Vec2::ZERO, room, Depth::Full);
}
node
}
const UNBOUNDED: f32 = f32::MAX;
const NO_WRAP_W: f32 = 4096.0;
const TEXT_BOX_W: f32 = 100.0;
const TEXT_BOX_CHROME_H: f32 = 7.0;
fn bounded(v: f32) -> bool {
v < UNBOUNDED / 2.0
}
fn resolve_size(u: Units, basis: f32) -> Option<f32> {
match u {
Units::Percent(_) if !bounded(basis) => None,
_ => u.resolve(basis),
}
}
fn new_node(w: &Widget) -> LayoutNode {
LayoutNode {
id: w.id.clone(),
on_click: w.on_click.clone(),
enabled: w.enabled,
focused: w.focused,
action: w.action,
visible: w.visible,
..Default::default()
}
}
#[derive(Clone, Copy, PartialEq)]
enum Depth {
Size,
Full,
}
#[derive(Clone, Copy)]
struct Room {
max_w: f32,
max_h: f32,
basis_w: f32,
basis_h: f32,
}
impl Room {
fn new(max_w: f32, max_h: f32, basis_w: f32, basis_h: f32) -> Room {
Room {
max_w,
max_h,
basis_w,
basis_h,
}
}
}
fn measure(w: &Widget, node: &mut LayoutNode, room: Room) {
layout_widget(w, node, Vec2::ZERO, room, Depth::Size);
}
fn layout_widget(w: &Widget, node: &mut LayoutNode, at: Vec2, room: Room, depth: Depth) {
let s = &w.style;
let Room { max_w, max_h, .. } = room;
let has_children = is_container(w);
let explicit_w = resolve_size(s.w, room.basis_w).map(|v| v.min(max_w));
let explicit_h = resolve_size(s.h, room.basis_h).map(|v| v.min(max_h));
let (ww, hh) = if has_children {
(explicit_w.unwrap_or(max_w), explicit_h.unwrap_or(max_h))
} else {
let (cw, ch) = leaf_size(w, explicit_w.unwrap_or(max_w), room);
(explicit_w.unwrap_or(cw), explicit_h.unwrap_or(ch))
};
node.rect = Rect {
x: at.x,
y: at.y,
w: ww,
h: hh,
};
node.grid = None;
node.content_h = hh;
node.scroll_y = 0.0;
node.clip = s.clip;
node.scrollbar = s.scrollbar;
if has_children {
let shrink = Shrink {
w: explicit_w.is_none(),
h: explicit_h.is_none(),
max_w,
max_h,
};
layout_children(w, node, Some(shrink), depth);
}
}
fn leaf_size(w: &Widget, ww: f32, room: Room) -> (f32, f32) {
let s = &w.style;
let Room { max_w, max_h, .. } = room;
let (mut ww, mut hh) = (ww, max_h);
let fit_w = |content: f32| (content + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
let fit_h = |content: f32| (content + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
match &w.kind {
WidgetKind::Label(t) | WidgetKind::Button(t) => {
let avail_w = (ww - s.pad[1] - s.pad[3]).max(0.0);
if avail_w < NO_WRAP_W && !s.no_wrap {
ww = fit_w(avail_w);
hh = fit_h(text::text_height_in(t, avail_w, s.font_scale, &s.font));
} else {
ww = fit_w(text::str_width_in(t, s.font_scale, &s.font));
hh = fit_h(text::LINE_H * s.font_scale);
}
}
WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
ww = fit_w(SLOT_SIZE);
hh = fit_h(SLOT_SIZE);
}
WidgetKind::Spacer | WidgetKind::Surface(_) => {
ww = s.min_w.max(1.0).min(max_w);
hh = s.min_h.max(1.0).min(max_h);
}
WidgetKind::Panel(_) | WidgetKind::Grid(_) => {
if !bounded(ww) {
ww = s.min_w;
}
if !bounded(hh) {
hh = s.min_h;
}
}
WidgetKind::TextBox(_) => {
if !bounded(ww) {
ww = TEXT_BOX_W;
}
ww = ww.max(s.min_w).min(max_w);
hh = fit_h(text::LINE_H * s.font_scale + TEXT_BOX_CHROME_H);
}
WidgetKind::Image(image) => {
let Vec2 { x: nw, y: nh } = crate::image::natural_size(image);
ww = fit_w(nw);
hh = fit_h(nh);
}
}
(ww, hh)
}
fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
node.rect.x = x;
node.rect.y = y;
node.content_h = node.rect.h;
if is_container(w) {
layout_children(w, node, None, Depth::Full);
}
}
fn is_container(w: &Widget) -> bool {
!w.children.is_empty() || matches!(w.kind, WidgetKind::Grid(_))
}
struct Shrink {
w: bool,
h: bool,
max_w: f32,
max_h: f32,
}
fn place(child: &Widget, cn: &mut LayoutNode, x: f32, y: f32, depth: Depth) {
match depth {
Depth::Full => arrange(child, cn, x, y),
Depth::Size => {
cn.rect.x = x;
cn.rect.y = y;
}
}
}
fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>, depth: Depth) {
match &w.kind {
WidgetKind::Grid(spec) => grid::layout(w, spec, node, shrink, depth),
_ => flex::layout(w, node, shrink, depth),
}
}
fn measure_aligned(w: &Widget, node: &mut LayoutNode, room: Room) {
if bounded(room.max_w) && w.style.w.is_auto() {
measure(
w,
node,
Room {
max_w: UNBOUNDED,
..room
},
);
if node.rect.w <= room.max_w {
return;
}
}
measure(w, node, room);
}
fn vis_margin(c: &Widget, side: usize) -> f32 {
if c.visible { c.style.margin[side] } else { 0.0 }
}
fn less(v: f32, by: f32) -> f32 {
if bounded(v) {
(v - by).max(0.0)
} else {
UNBOUNDED
}
}
fn finish(
w: &Widget,
node: &mut LayoutNode,
mut child_nodes: Vec<LayoutNode>,
shrink: Option<Shrink>,
bar_w: f32,
) {
let s = &w.style;
let Rect { x, y, .. } = node.rect;
let extent = |f: &dyn Fn(&Widget, &LayoutNode) -> f32| -> f32 {
w.children
.iter()
.zip(&child_nodes)
.map(|(c, n)| f(c, n))
.fold(0.0f32, f32::max)
};
let ch = extent(&|c, n| n.rect.y - y + n.rect.h + vis_margin(c, 2));
if let Some(fit) = shrink {
if fit.w {
let cw = extent(&|c, n| n.rect.x - x + n.rect.w + vis_margin(c, 1));
node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
}
if fit.h {
node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
}
}
scroll(node, s, ch + s.pad[2], &mut child_nodes);
node.children = child_nodes;
}
fn scroll(node: &mut LayoutNode, s: &Style, content_h: f32, children: &mut [LayoutNode]) {
node.content_h = content_h;
node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
if node.scroll_y > 0.0 {
for c in children {
c.translate_y(-node.scroll_y);
}
}
}
fn content_box(s: &Style, rect: Rect) -> (f32, f32, f32) {
let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
let content_w = if bounded(rect.w) {
(rect.w - s.pad[1] - s.pad[3] - bar_w).max(0.0)
} else {
UNBOUNDED
};
let content_h = if bounded(rect.h) {
(rect.h - s.pad[0] - s.pad[2]).max(0.0)
} else {
UNBOUNDED
};
(content_w, content_h, bar_w)
}
pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
return None;
}
for child in node.children.iter().rev() {
if let Some(hit) = hit_test(child, mx, my) {
return Some(hit);
}
}
if node.is_clickable() {
Some(node)
} else {
None
}
}
pub fn hit_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
fn walk(node: &LayoutNode, mx: f32, my: f32, path: &mut Vec<u16>) -> bool {
if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
return false;
}
for (i, child) in node.children.iter().enumerate().rev() {
path.push(i as u16);
if walk(child, mx, my, path) {
return true;
}
path.pop();
}
node.is_clickable()
}
let mut path = Vec::new();
walk(node, mx, my, &mut path).then_some(path)
}
pub fn deepest_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
if !node.visible || !node.rect.contains(Vec2::new(mx, my)) {
return None;
}
let mut path = Vec::new();
let mut n = node;
'outer: loop {
for (i, child) in n.children.iter().enumerate().rev() {
if child.visible && child.rect.contains(Vec2::new(mx, my)) {
path.push(i as u16);
n = child;
continue 'outer;
}
}
return Some(path);
}
}
pub fn node_at<'a>(node: &'a LayoutNode, path: &[u16]) -> Option<&'a LayoutNode> {
let mut n = node;
for &i in path {
n = n.children.get(i as usize)?;
}
Some(n)
}
pub fn node_at_mut<'a>(node: &'a mut LayoutNode, path: &[u16]) -> Option<&'a mut LayoutNode> {
let mut n = node;
for &i in path {
n = n.children.get_mut(i as usize)?;
}
Some(n)
}
impl LayoutNode {
pub fn is_clickable(&self) -> bool {
self.visible && self.enabled && (self.action.is_some() || self.on_click.is_some())
}
}
pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
if node.id.as_deref() == Some(id) {
return Some(node.rect);
}
node.children.iter().find_map(|c| find_by_id(c, id))
}
pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
if node.id.as_deref() == Some(id) {
return Some(node);
}
node.children.iter().find_map(|c| find_node(c, id))
}
pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
for child in &mut node.children {
set_focus(child, focused_id);
}
}
#[cfg(test)]
mod tests;