use super::{Align, Insets, Length, SideMode, TextStyle, Tint};
use crate::edit::Editor;
const BUTTON_PADDING: (f32, f32) = (12.0, 6.0);
const BUTTON_MIN: (f32, f32) = (64.0, 32.0);
pub(crate) const NAV_ITEM_PADDING: (f32, f32) = (16.0, 12.0);
const NAV_ITEM_HEIGHT: f32 = 36.0;
pub(crate) const NAV_ICON: (f32, f32) = (16.0, 16.0);
pub(crate) const SIDE_COMPACT_WIDTH: f32 = 48.0;
pub(crate) const TEXT_BOX_PADDING: (f32, f32) = (11.0, 6.0);
const TEXT_BOX_WIDTH: f32 = 200.0;
const TEXT_BOX_MIN_HEIGHT: f32 = 32.0;
const CARD_TEXT_GAP: f32 = 4.0;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub(crate) struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Rect {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
x,
y,
w: w.max(0.0),
h: h.max(0.0),
}
}
fn inset(self, i: Insets) -> Self {
Rect::new(
self.x + i.left,
self.y + i.top,
self.w - i.left - i.right,
self.h - i.top - i.bottom,
)
}
pub fn contains(&self, x: f32, y: f32) -> bool {
x >= self.x && x < self.x + self.w && y >= self.y && y < self.y + self.h
}
pub fn intersect(self, other: Rect) -> Rect {
let x = self.x.max(other.x);
let y = self.y.max(other.y);
let right = (self.x + self.w).min(other.x + other.w);
let bottom = (self.y + self.h).min(other.y + other.h);
Rect::new(x, y, right - x, bottom - y)
}
pub fn is_empty(&self) -> bool {
self.w <= 0.0 || self.h <= 0.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Content {
Blank,
Text {
text: String,
style: TextStyle,
},
Button {
label: String,
accent: bool,
},
NavItem {
label: String,
icon: Option<String>,
selected: bool,
},
SideBar {
mode: SideMode,
modes: Vec<SideMode>,
width: f32,
title: String,
},
SideToggle,
Card {
text: Option<(String, String)>,
pinned: bool,
tint: Option<Tint>,
},
Scroll {
offset: f32,
extent: f32,
},
TextBox {
edit: Editor,
placeholder: String,
scroll: f32,
},
}
impl Content {
pub fn card() -> Self {
Content::Card {
text: None,
pinned: false,
tint: None,
}
}
pub fn interactive(&self) -> bool {
matches!(
self,
Content::Button { .. }
| Content::NavItem { .. }
| Content::SideToggle
| Content::TextBox { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Axis {
Row,
Column,
Stack,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Container {
pub axis: Axis,
pub gap: f32,
pub padding: Insets,
pub align: Align,
pub children: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Node {
pub content: Content,
pub container: Option<Container>,
pub width: Length,
pub height: Length,
pub enabled: bool,
pub visible: bool,
pub parent: Option<usize>,
pub rect: Rect,
pub clickable: bool,
pub draggable: bool,
pub drop_target: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct DropPlace {
pub target: usize,
pub index: usize,
pub changes: bool,
pub line: Option<Rect>,
}
impl Node {
pub fn leaf(content: Content) -> Self {
Self {
content,
container: None,
width: Length::Auto,
height: Length::Auto,
enabled: true,
visible: true,
parent: None,
rect: Rect::default(),
clickable: false,
draggable: false,
drop_target: false,
}
}
pub fn container(content: Content, axis: Axis, align: Align) -> Self {
Self {
container: Some(Container {
axis,
gap: 0.0,
padding: Insets::default(),
align,
children: Vec::new(),
}),
..Self::leaf(content)
}
}
pub fn children(&self) -> &[usize] {
self.container.as_ref().map_or(&[], |c| &c.children)
}
}
pub(crate) trait Measure {
fn text(&self, text: &str, style: TextStyle, max_width: f32) -> (f32, f32);
fn label(&self, label: &str) -> (f32, f32);
}
#[derive(Debug, Default)]
pub(crate) struct Tree {
pub nodes: Vec<Node>,
pub root: Option<usize>,
}
impl Tree {
pub fn add(&mut self, mut node: Node) -> usize {
let limit = self.nodes.len();
let mut children: Vec<usize> = match &node.content {
Content::SideBar { title, .. } => vec![self.add_side_head(title.clone())],
Content::Card {
text: Some((title, body)),
..
} => self.add_card_text(title.clone(), body.clone()),
_ => Vec::new(),
};
if let (Content::Card { text: Some(_), .. }, Some(c)) = (&node.content, &mut node.container)
{
c.axis = Axis::Column;
if c.gap == 0.0 {
c.gap = CARD_TEXT_GAP;
}
}
let index = self.nodes.len();
for &child in node.children() {
if child < limit && !children.contains(&child) {
children.push(child);
}
}
self.nodes.push(node);
if let Some(c) = &mut self.nodes[index].container {
c.children.clone_from(&children);
}
for child in children {
if let Some(old) = self.nodes[child].parent.replace(index)
&& let Some(c) = &mut self.nodes[old].container
{
c.children.retain(|&x| x != child);
}
if self.root == Some(child) {
self.root = None;
}
}
self.apply_side(index);
index
}
fn add_card_text(&mut self, title: String, body: String) -> Vec<usize> {
let title = self.add(Node::leaf(Content::Text {
text: title,
style: TextStyle::BodyStrong,
}));
let mut text = Node::leaf(Content::Text {
text: body,
style: TextStyle::Body,
});
text.visible = !matches!(&text.content, Content::Text { text, .. } if text.is_empty());
let body = self.add(text);
vec![title, body]
}
pub fn card_text(&self, card: usize) -> Option<(String, String)> {
match &self.nodes.get(card)?.content {
Content::Card { text, .. } => text.clone(),
_ => None,
}
}
pub fn set_card_text(&mut self, card: usize, title: &str, body: &str) {
let Some(Content::Card {
text: Some(text), ..
}) = self.nodes.get_mut(card).map(|n| &mut n.content)
else {
return;
};
*text = (title.to_owned(), body.to_owned());
let &[title_node, body_node, ..] = self.nodes[card].children() else {
return;
};
for (node, value) in [(title_node, title), (body_node, body)] {
if let Content::Text { text, .. } = &mut self.nodes[node].content {
*text = value.to_owned();
}
}
self.nodes[body_node].visible = !body.is_empty();
}
pub fn set_pinned(&mut self, card: usize, to: bool) {
if let Some(Content::Card { pinned, .. }) = self.nodes.get_mut(card).map(|n| &mut n.content)
{
*pinned = to;
}
}
pub fn set_tint(&mut self, card: usize, to: Option<Tint>) {
if let Some(Content::Card { tint, .. }) = self.nodes.get_mut(card).map(|n| &mut n.content) {
*tint = to;
}
}
fn own_start(&self, index: usize) -> usize {
match &self.nodes[index].content {
Content::SideBar { .. } => 1,
Content::Card { text: Some(_), .. } => 2,
_ => 0,
}
}
pub fn set_children(&mut self, parent: usize, children: &[usize]) {
if self.nodes.get(parent).is_none_or(|n| n.container.is_none()) {
return;
}
let mut list: Vec<usize> = Vec::new();
for &child in children {
if child < self.nodes.len() && !list.contains(&child) && !self.within(parent, child) {
list.push(child);
}
}
let start = self
.own_start(parent)
.min(self.nodes[parent].children().len());
let old: Vec<usize> = self.nodes[parent].children()[start..].to_vec();
for child in old {
self.nodes[child].parent = None;
}
for &child in &list {
if let Some(from) = self.nodes[child].parent.replace(parent)
&& let Some(c) = &mut self.nodes[from].container
{
c.children.retain(|&x| x != child);
}
if self.root == Some(child) {
self.root = None;
}
}
if let Some(c) = &mut self.nodes[parent].container {
c.children.truncate(start);
c.children.extend(list);
}
}
pub fn within(&self, mut index: usize, ancestor: usize) -> bool {
loop {
if index == ancestor {
return true;
}
match self.nodes[index].parent {
Some(parent) => index = parent,
None => return false,
}
}
}
pub fn drag_source_at(&self, x: f32, y: f32) -> Option<usize> {
let top = self
.draw_order()
.into_iter()
.rev()
.find(|&i| self.shows_at(i, x, y))?;
let mut at = Some(top);
while let Some(index) = at {
let n = &self.nodes[index];
if n.draggable && n.enabled {
return Some(index);
}
at = n.parent;
}
None
}
pub fn drop_at(&self, x: f32, y: f32, item: usize) -> Option<DropPlace> {
let target = self.draw_order().into_iter().rev().find(|&i| {
self.nodes[i].drop_target && self.shows_at(i, x, y) && !self.within(i, item)
})?;
let node = &self.nodes[target];
let c = node.container.as_ref()?;
let own = &c.children[self.own_start(target).min(c.children.len())..];
let others: Vec<usize> = own.iter().copied().filter(|&ch| ch != item).collect();
let shown: Vec<usize> = others
.iter()
.copied()
.filter(|&ch| self.present(ch))
.collect();
let along = |r: Rect| match c.axis {
Axis::Row => (r.x, r.w),
Axis::Column | Axis::Stack => (r.y, r.h),
};
let pointer = if c.axis == Axis::Row { x } else { y };
let at = match c.axis {
Axis::Stack => shown.len(),
Axis::Row | Axis::Column => shown
.iter()
.position(|&ch| {
let (start, length) = along(self.nodes[ch].rect);
pointer < start + length / 2.0
})
.unwrap_or(shown.len()),
};
let index = shown.get(at).map_or(others.len(), |ch| {
others.iter().position(|o| o == ch).unwrap_or(others.len())
});
let changes = self.nodes[item].parent != Some(target)
|| own.iter().position(|&ch| ch == item) != Some(index);
let inner = node.rect.inset(c.padding);
let before = at.checked_sub(1).and_then(|i| shown.get(i));
let edge = match (before, shown.get(at)) {
(Some(&before), _) => {
let (start, length) = along(self.nodes[before].rect);
start + length + c.gap / 2.0
}
(None, Some(&after)) => {
(along(self.nodes[after].rect).0 - c.gap / 2.0).max(along(inner).0)
}
(None, None) => along(inner).0,
};
let line = match c.axis {
Axis::Row => Some(Rect::new(edge - 1.0, inner.y, 2.0, inner.h)),
Axis::Column => Some(Rect::new(inner.x, edge - 1.0, inner.w, 2.0)),
Axis::Stack => None,
};
Some(DropPlace {
target,
index,
changes,
line: line.filter(|_| changes),
})
}
fn add_side_head(&mut self, title: String) -> usize {
let toggle = self.add(Node::leaf(Content::SideToggle));
let title = self.add(Node::leaf(Content::Text {
text: title,
style: TextStyle::Subtitle,
}));
let mut head = Node::container(Content::Blank, Axis::Row, Align::Center);
if let Some(c) = &mut head.container {
c.children = vec![toggle, title];
}
self.add(head)
}
fn apply_side(&mut self, bar: usize) {
let Content::SideBar {
mode,
modes,
width,
title,
} = &self.nodes[bar].content
else {
return;
};
let expanded = *mode == SideMode::Expanded;
let button = modes.len() > 1;
let named = expanded && !title.is_empty();
let padding = self.nodes[bar]
.container
.as_ref()
.map_or(0.0, |c| c.padding.left + c.padding.right);
let width = if expanded {
*width
} else {
SIDE_COMPACT_WIDTH + padding
};
self.nodes[bar].width = Length::Fixed(width);
let Some(&head) = self.nodes[bar].children().first() else {
return;
};
self.nodes[head].visible = button || named;
if let &[toggle, title] = self.nodes[head].children() {
self.nodes[toggle].visible = button;
self.nodes[title].visible = named;
}
}
pub fn side_mode(&self, bar: usize) -> Option<SideMode> {
match self.nodes.get(bar)?.content {
Content::SideBar { mode, .. } => Some(mode),
_ => None,
}
}
pub fn set_side_mode(&mut self, bar: usize, to: SideMode) {
if let Some(Content::SideBar { mode, .. }) = self.nodes.get_mut(bar).map(|n| &mut n.content)
{
*mode = to;
self.apply_side(bar);
}
}
pub fn step_side(&mut self, toggle: usize) -> Option<usize> {
let head = self.nodes.get(toggle)?.parent?;
let bar = self.nodes[head].parent?;
let Content::SideBar { mode, modes, .. } = &self.nodes[bar].content else {
return None;
};
let at = modes.iter().position(|m| m == mode);
let next = *modes.get(at.map_or(0, |i| (i + 1) % modes.len()))?;
self.set_side_mode(bar, next);
Some(bar)
}
pub fn compact(&self, index: usize) -> bool {
let mut at = self.nodes[index].parent;
while let Some(parent) = at {
if let Content::SideBar { mode, .. } = self.nodes[parent].content {
return mode != SideMode::Expanded;
}
at = self.nodes[parent].parent;
}
false
}
fn present(&self, index: usize) -> bool {
let n = &self.nodes[index];
n.visible
&& n.parent.is_none_or(|p| match self.nodes[p].content {
Content::SideBar {
mode: SideMode::Collapsed,
..
} => self.nodes[p].children().first() == Some(&index),
_ => true,
})
}
pub fn get(&self, index: usize) -> Option<&Node> {
self.nodes.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut Node> {
self.nodes.get_mut(index)
}
pub fn layout(&mut self, width: f32, height: f32, m: &dyn Measure) {
if let Some(root) = self.root {
self.arrange(root, Rect::new(0.0, 0.0, width, height), m);
}
}
pub fn measure(&self, index: usize, max_w: f32, max_h: f32, m: &dyn Measure) -> (f32, f32) {
let n = &self.nodes[index];
let max_w = match n.width {
Length::Fixed(w) => w,
_ => max_w,
};
let (w, h) = match &n.container {
Some(c) => self.measure_container(c, max_w, max_h, m),
None => intrinsic(&n.content, self.compact(index), max_w, m),
};
let pick = |length: Length, content: f32| match length {
Length::Fixed(v) => v,
Length::Auto | Length::Fill(_) => content,
};
(pick(n.width, w), pick(n.height, h))
}
fn measure_container(
&self,
c: &Container,
max_w: f32,
max_h: f32,
m: &dyn Measure,
) -> (f32, f32) {
let p = c.padding;
let inner_w = (max_w - p.left - p.right).max(0.0);
let inner_h = (max_h - p.top - p.bottom).max(0.0);
let shown = c.children.iter().filter(|&&child| self.present(child));
let gaps = c.gap * shown.clone().count().saturating_sub(1) as f32;
let sizes = shown.map(|&child| self.measure(child, inner_w, inner_h, m));
let (w, h) = match c.axis {
Axis::Row => sizes.fold((gaps, 0.0f32), |(w, h), (cw, ch)| (w + cw, h.max(ch))),
Axis::Column => sizes.fold((0.0f32, gaps), |(w, h), (cw, ch)| (w.max(cw), h + ch)),
Axis::Stack => sizes.fold((0.0f32, 0.0f32), |(w, h), (cw, ch)| (w.max(cw), h.max(ch))),
};
(w + p.left + p.right, h + p.top + p.bottom)
}
fn arrange(&mut self, index: usize, rect: Rect, m: &dyn Measure) {
self.nodes[index].rect = rect;
let Some(mut c) = self.nodes[index].container.clone() else {
return;
};
c.children.retain(|&child| self.present(child));
if let Content::Scroll { offset, .. } = self.nodes[index].content {
self.arrange_scroll(index, &c, rect, offset, m);
return;
}
let inner = rect.inset(c.padding);
match c.axis {
Axis::Row => self.arrange_row(&c, inner, m),
Axis::Column => self.arrange_column(&c, inner, m),
Axis::Stack => {
for &child in &c.children {
let (w, h) = self.cross_size(child, inner.w, inner.h, c.align, m);
let x = offset(inner.x, inner.w, w, c.align);
let y = offset(inner.y, inner.h, h, c.align);
self.arrange(child, Rect::new(x, y, w, h), m);
}
}
}
}
fn arrange_scroll(
&mut self,
index: usize,
c: &Container,
rect: Rect,
offset: f32,
m: &dyn Measure,
) {
let (_, extent) = self.measure_container(c, rect.w, f32::MAX / 4.0, m);
let offset = offset.clamp(0.0, (extent - rect.h).max(0.0));
self.nodes[index].content = Content::Scroll { offset, extent };
let full = Rect::new(rect.x, rect.y - offset, rect.w, extent);
self.arrange_column(c, full.inset(c.padding), m);
}
pub fn clip(&self, index: usize) -> Option<Rect> {
let mut clip: Option<Rect> = None;
let mut at = self.nodes[index].parent;
while let Some(parent) = at {
if let Content::Scroll { .. } = self.nodes[parent].content {
let r = self.nodes[parent].rect;
clip = Some(clip.map_or(r, |c| c.intersect(r)));
}
at = self.nodes[parent].parent;
}
clip
}
fn shows_at(&self, index: usize, x: f32, y: f32) -> bool {
self.nodes[index].rect.contains(x, y) && self.clip(index).is_none_or(|c| c.contains(x, y))
}
pub fn scroll_at(&self, x: f32, y: f32, delta: f32) -> Option<usize> {
self.draw_order().into_iter().rev().find(|&i| {
self.shows_at(i, x, y)
&& match self.nodes[i].content {
Content::Scroll { offset, extent } => {
let max = (extent - self.nodes[i].rect.h).max(0.0);
(delta > 0.0 && offset < max) || (delta < 0.0 && offset > 0.0)
}
_ => false,
}
})
}
pub fn scroll_by(&mut self, index: usize, delta: f32) {
if let Some(Content::Scroll { offset, extent }) =
self.nodes.get_mut(index).map(|n| &mut n.content)
{
*offset = (*offset + delta).clamp(0.0, *extent);
}
}
pub fn reveal(&mut self, index: usize) -> bool {
let mut at = self.nodes.get(index).and_then(|n| n.parent);
while let Some(parent) = at {
if let Content::Scroll { offset, extent } = self.nodes[parent].content {
let view = self.nodes[parent].rect;
let r = self.nodes[index].rect;
let mut moved = offset;
if r.y + r.h > view.y + view.h {
moved += r.y + r.h - (view.y + view.h);
}
if r.y - (moved - offset) < view.y {
moved = offset - (view.y - r.y);
}
let moved = moved.clamp(0.0, (extent - view.h).max(0.0));
self.nodes[parent].content = Content::Scroll {
offset: moved,
extent,
};
return moved != offset;
}
at = self.nodes[parent].parent;
}
false
}
fn arrange_row(&mut self, c: &Container, inner: Rect, m: &dyn Measure) {
let widths = self.main_sizes(c, inner.w, |tree, child| {
let n = &tree.nodes[child];
match n.width {
Length::Fill(weight) => Err(weight),
_ => Ok(tree.measure(child, inner.w, inner.h, m).0),
}
});
let mut x = inner.x;
for (&child, w) in c.children.iter().zip(widths) {
let h = match self.nodes[child].height {
Length::Fixed(h) => h,
Length::Fill(_) => inner.h,
Length::Auto if c.align == Align::Stretch => inner.h,
Length::Auto => self.measure(child, w, inner.h, m).1.min(inner.h),
};
let y = offset(inner.y, inner.h, h, c.align);
self.arrange(child, Rect::new(x, y, w, h), m);
x += w + c.gap;
}
}
fn arrange_column(&mut self, c: &Container, inner: Rect, m: &dyn Measure) {
let widths: Vec<f32> = c
.children
.iter()
.map(|&child| match self.nodes[child].width {
Length::Fixed(w) => w,
Length::Fill(_) => inner.w,
Length::Auto if c.align == Align::Stretch => inner.w,
Length::Auto => self.measure(child, inner.w, inner.h, m).0.min(inner.w),
})
.collect();
let heights = self.main_sizes(c, inner.h, |tree, child| {
let i = c.children.iter().position(|&x| x == child).unwrap_or(0);
match tree.nodes[child].height {
Length::Fill(weight) => Err(weight),
_ => Ok(tree.measure(child, widths[i], inner.h, m).1),
}
});
let mut y = inner.y;
for ((&child, w), h) in c.children.iter().zip(widths).zip(heights) {
let x = offset(inner.x, inner.w, w, c.align);
self.arrange(child, Rect::new(x, y, w, h), m);
y += h + c.gap;
}
}
fn main_sizes(
&self,
c: &Container,
space: f32,
size: impl Fn(&Tree, usize) -> Result<f32, f32>,
) -> Vec<f32> {
let sizes: Vec<Result<f32, f32>> =
c.children.iter().map(|&child| size(self, child)).collect();
let gaps = c.gap * c.children.len().saturating_sub(1) as f32;
let fixed: f32 = sizes.iter().filter_map(|s| s.ok()).sum();
let weights: f32 = sizes
.iter()
.filter_map(|s| s.err())
.map(|w| w.max(0.0))
.sum();
let free = (space - fixed - gaps).max(0.0);
sizes
.into_iter()
.map(|s| match s {
Ok(v) => v,
Err(_) if weights <= 0.0 => 0.0,
Err(w) => free * w.max(0.0) / weights,
})
.collect()
}
fn cross_size(
&self,
child: usize,
w: f32,
h: f32,
align: Align,
m: &dyn Measure,
) -> (f32, f32) {
let n = &self.nodes[child];
let (mw, mh) = self.measure(child, w, h, m);
let pick = |length: Length, space: f32, measured: f32| match length {
Length::Fixed(v) => v,
Length::Fill(_) => space,
Length::Auto if align == Align::Stretch => space,
Length::Auto => measured.min(space),
};
(pick(n.width, w, mw), pick(n.height, h, mh))
}
pub fn draw_order(&self) -> Vec<usize> {
let mut out = Vec::with_capacity(self.nodes.len());
if let Some(root) = self.root
&& self.nodes[root].visible
{
self.walk(root, &mut out);
}
out
}
fn walk(&self, index: usize, out: &mut Vec<usize>) {
out.push(index);
for &child in self.nodes[index].children() {
if self.present(child) {
self.walk(child, out);
}
}
}
pub fn focus_order(&self) -> Vec<usize> {
self.draw_order()
.into_iter()
.filter(|&i| self.focusable(i))
.collect()
}
pub fn focusable(&self, index: usize) -> bool {
self.nodes.get(index).is_some_and(|n| {
n.enabled && (n.content.interactive() || n.clickable) && self.shown(index)
})
}
pub fn shown(&self, mut index: usize) -> bool {
loop {
if !self.present(index) {
return false;
}
if Some(index) == self.root {
return true;
}
match self.nodes[index].parent {
Some(parent) => index = parent,
None => return false,
}
}
}
pub fn hit(&self, x: f32, y: f32) -> Option<usize> {
self.draw_order()
.into_iter()
.rev()
.find(|&i| self.focusable(i) && self.shows_at(i, x, y))
}
pub fn next_focus(&self, from: Option<usize>, back: bool) -> Option<usize> {
let order = self.focus_order();
if order.is_empty() {
return None;
}
let at = from.and_then(|f| order.iter().position(|&i| i == f));
let next = match (at, back) {
(None, false) => 0,
(None, true) => order.len() - 1,
(Some(i), false) => (i + 1) % order.len(),
(Some(i), true) => (i + order.len() - 1) % order.len(),
};
Some(order[next])
}
}
fn intrinsic(content: &Content, compact: bool, max_w: f32, m: &dyn Measure) -> (f32, f32) {
match content {
Content::Blank
| Content::Card { .. }
| Content::Scroll { .. }
| Content::SideBar { .. } => (0.0, 0.0),
Content::SideToggle => (SIDE_COMPACT_WIDTH, NAV_ITEM_HEIGHT),
Content::NavItem { .. } if compact => (SIDE_COMPACT_WIDTH, NAV_ITEM_HEIGHT),
Content::Text { text, style } => m.text(text, *style, max_w),
Content::Button { label, .. } => {
let (w, h) = m.label(label);
(
(w + 2.0 * BUTTON_PADDING.0).max(BUTTON_MIN.0),
(h + 2.0 * BUTTON_PADDING.1).max(BUTTON_MIN.1),
)
}
Content::NavItem { label, icon, .. } => {
let (w, h) = m.label(label);
let icon = icon.as_ref().map_or(0.0, |_| NAV_ICON.0 + NAV_ICON.1);
(
w + icon + NAV_ITEM_PADDING.0 + NAV_ITEM_PADDING.1,
h.max(NAV_ITEM_HEIGHT),
)
}
Content::TextBox { edit, .. } => {
let (px, py) = TEXT_BOX_PADDING;
let (sample, wrap) = match edit.text() {
text if edit.multiline() && !text.is_empty() => (text, max_w - 2.0 * px),
_ => (" ", f32::MAX / 4.0),
};
let (_, h) = m.text(sample, TextStyle::Body, wrap.max(1.0));
(
TEXT_BOX_WIDTH.min(max_w),
(h + 2.0 * py).max(TEXT_BOX_MIN_HEIGHT),
)
}
}
}
fn offset(start: f32, space: f32, size: f32, align: Align) -> f32 {
match align {
Align::Start | Align::Stretch => start,
Align::Center => start + ((space - size) / 2.0).max(0.0),
Align::End => start + (space - size).max(0.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Fixed;
impl Measure for Fixed {
fn text(&self, text: &str, _: TextStyle, max_width: f32) -> (f32, f32) {
let chars = text.chars().count() as f32;
let per_line = (max_width / 8.0).floor().max(1.0);
let lines = (chars / per_line).ceil().max(1.0);
((chars.min(per_line)) * 8.0, lines * 20.0)
}
fn label(&self, label: &str) -> (f32, f32) {
(label.chars().count() as f32 * 8.0, 20.0)
}
}
fn text(s: &str) -> Node {
Node::leaf(Content::Text {
text: s.into(),
style: TextStyle::Body,
})
}
fn button(s: &str) -> Node {
Node::leaf(Content::Button {
label: s.into(),
accent: false,
})
}
fn with(mut node: Node, children: &[usize]) -> Node {
node.container.as_mut().unwrap().children = children.to_vec();
node
}
fn scroll(children: &[usize]) -> Node {
let mut node = with(
Node::container(
Content::Scroll {
offset: 0.0,
extent: 0.0,
},
Axis::Column,
Align::Stretch,
),
children,
);
node.height = Length::Fill(1.0);
node
}
fn five_in_a_scroll() -> (Tree, usize, Vec<usize>) {
let mut t = Tree::default();
let buttons: Vec<usize> = (0..5).map(|i| t.add(button(&format!("B{i}")))).collect();
let s = t.add(scroll(&buttons));
t.root = Some(s);
t.layout(200.0, 100.0, &Fixed);
(t, s, buttons)
}
#[test]
fn a_scroll_moves_its_children_and_stays_in_range() {
let (mut t, s, b) = five_in_a_scroll();
assert_eq!(t.nodes[b[4]].rect, Rect::new(0.0, 128.0, 200.0, 32.0));
assert_eq!(
t.nodes[s].content,
Content::Scroll {
offset: 0.0,
extent: 160.0
}
);
t.scroll_by(s, 40.0);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[b[0]].rect.y, -40.0);
assert_eq!(t.nodes[b[4]].rect.y, 88.0);
t.scroll_by(s, 500.0);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[b[4]].rect.y, 68.0);
t.scroll_by(s, -500.0);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[b[0]].rect.y, 0.0);
}
#[test]
fn a_scroll_cuts_what_is_outside_it() {
let (t, s, b) = five_in_a_scroll();
assert_eq!(t.clip(b[3]), Some(Rect::new(0.0, 0.0, 200.0, 100.0)));
assert_eq!(t.clip(s), None);
assert_eq!(t.hit(5.0, 98.0), Some(b[3]));
assert_eq!(t.hit(5.0, 110.0), None);
}
#[test]
fn the_wheel_finds_a_scroll_that_can_move() {
let (mut t, s, _) = five_in_a_scroll();
assert_eq!(t.scroll_at(5.0, 50.0, 10.0), Some(s));
assert_eq!(t.scroll_at(5.0, 50.0, -10.0), None);
assert_eq!(t.scroll_at(5.0, 150.0, 10.0), None);
t.scroll_by(s, 60.0);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.scroll_at(5.0, 50.0, 10.0), None);
assert_eq!(t.scroll_at(5.0, 50.0, -10.0), Some(s));
}
#[test]
fn reveal_scrolls_a_widget_into_view() {
let (mut t, s, b) = five_in_a_scroll();
assert!(t.reveal(b[4]));
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[b[4]].rect.y + 32.0, 100.0);
assert!(!t.reveal(b[4]));
assert!(t.reveal(b[0]));
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[b[0]].rect.y, 0.0);
assert!(!t.reveal(s));
}
#[test]
fn a_short_scroll_does_not_move() {
let mut t = Tree::default();
let ok = t.add(button("OK"));
let s = t.add(scroll(&[ok]));
t.root = Some(s);
t.layout(200.0, 100.0, &Fixed);
t.scroll_by(s, 50.0);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[ok].rect.y, 0.0);
assert_eq!(t.scroll_at(5.0, 5.0, 10.0), None);
}
#[test]
fn a_hidden_widget_takes_no_space_and_no_gap() {
let mut t = Tree::default();
let a = t.add(text("one"));
let b = t.add(button("OK"));
let c = t.add(text("two"));
let mut col = with(
Node::container(Content::Blank, Axis::Column, Align::Stretch),
&[a, b, c],
);
col.container.as_mut().unwrap().gap = 10.0;
let col = t.add(col);
t.root = Some(col);
t.nodes[b].visible = false;
assert_eq!(t.measure(col, 200.0, 100.0, &Fixed), (24.0, 50.0));
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[c].rect, Rect::new(0.0, 30.0, 200.0, 20.0));
assert_eq!(t.draw_order(), [col, a, c]);
assert!(!t.focusable(b));
assert_eq!(t.hit(5.0, 25.0), None);
t.nodes[b].visible = true;
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[c].rect, Rect::new(0.0, 72.0, 200.0, 20.0));
assert_eq!(t.hit(5.0, 35.0), Some(b));
}
#[test]
fn a_hidden_container_hides_its_widgets() {
let mut t = Tree::default();
let ok = t.add(button("OK"));
let page = t.add(with(
Node::container(Content::Blank, Axis::Column, Align::Stretch),
&[ok],
));
let root = t.add(with(
Node::container(Content::Blank, Axis::Stack, Align::Stretch),
&[page],
));
t.root = Some(root);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.hit(5.0, 5.0), Some(ok));
t.nodes[page].visible = false;
assert_eq!(t.hit(5.0, 5.0), None);
assert!(!t.focusable(ok));
assert_eq!(t.next_focus(None, false), None);
}
#[test]
fn a_navigation_item_has_space_for_its_mark() {
let mut t = Tree::default();
let item = t.add(nav("Inbox", None));
assert_eq!(t.measure(item, 200.0, 100.0, &Fixed), (68.0, 36.0));
assert!(t.nodes[item].content.interactive());
let with_icon = t.add(nav("Inbox", Some("i")));
assert_eq!(t.measure(with_icon, 200.0, 100.0, &Fixed), (100.0, 36.0));
}
fn nav(label: &str, icon: Option<&str>) -> Node {
Node::leaf(Content::NavItem {
label: label.into(),
icon: icon.map(Into::into),
selected: false,
})
}
fn side_bar(modes: &[SideMode], title: &str) -> (Tree, usize, [usize; 2], usize) {
let mut t = Tree::default();
let items = [t.add(nav("Home", Some("h"))), t.add(nav("Mail", None))];
let bar = t.add(with(
Node::container(
Content::SideBar {
mode: SideMode::Expanded,
modes: modes.to_vec(),
width: 200.0,
title: title.into(),
},
Axis::Column,
Align::Stretch,
),
&items,
));
let mut page = text("page");
page.width = Length::Fill(1.0);
let page = t.add(page);
let row = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Stretch),
&[bar, page],
));
t.root = Some(row);
t.layout(400.0, 300.0, &Fixed);
(t, bar, items, page)
}
fn toggle_of(t: &Tree, bar: usize) -> usize {
let head = t.nodes[bar].children()[0];
t.nodes[head].children()[0]
}
fn card(title: &str, body: &str) -> Node {
let mut node = Node::container(
Content::Card {
text: Some((title.into(), body.into())),
pinned: false,
tint: None,
},
Axis::Stack,
Align::Stretch,
);
node.clickable = true;
node.draggable = true;
node
}
fn lanes() -> (Tree, [usize; 2], [usize; 3]) {
let mut t = Tree::default();
let cards = [
t.add(card("A", "")),
t.add(card("B", "")),
t.add(card("C", "")),
];
let mut lanes = [0; 2];
for (lane, own) in [&cards[..2], &cards[2..]].into_iter().enumerate() {
let mut node = with(
Node::container(Content::Blank, Axis::Column, Align::Stretch),
own,
);
node.container.as_mut().unwrap().gap = 10.0;
node.width = Length::Fixed(100.0);
node.drop_target = true;
lanes[lane] = t.add(node);
}
let row = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Stretch),
&lanes,
));
t.root = Some(row);
t.layout(200.0, 300.0, &Fixed);
(t, lanes, cards)
}
#[test]
fn a_card_with_a_text_stacks_its_title_and_its_body() {
let mut t = Tree::default();
let extra = t.add(button("Open"));
let c = t.add(with(card("Plan", "Write it"), &[extra]));
t.root = Some(c);
t.layout(200.0, 200.0, &Fixed);
let [title, body, last] = t.nodes[c].children() else {
panic!("a card with a text has its texts before its children");
};
assert_eq!(*last, extra);
assert_eq!(t.nodes[*title].rect, Rect::new(0.0, 0.0, 200.0, 20.0));
assert_eq!(t.nodes[*body].rect, Rect::new(0.0, 24.0, 200.0, 20.0));
assert_eq!(t.card_text(c), Some(("Plan".into(), "Write it".into())));
t.set_card_text(c, "Plan B", "");
t.layout(200.0, 200.0, &Fixed);
assert_eq!(t.card_text(c), Some(("Plan B".into(), String::new())));
assert_eq!(t.nodes[extra].rect.y, 24.0);
assert_eq!(t.focus_order(), [c, extra]);
assert_eq!(t.hit(5.0, 5.0), Some(c));
assert_eq!(t.hit(5.0, 30.0), Some(extra));
}
#[test]
fn a_drag_starts_on_a_card_and_drops_between_cards() {
let (t, lanes, cards) = lanes();
assert_eq!(t.drag_source_at(50.0, 35.0), Some(cards[1]));
assert_eq!(t.drag_source_at(50.0, 100.0), None);
let place = t.drop_at(150.0, 5.0, cards[0]).unwrap();
assert_eq!(
(place.target, place.index, place.changes),
(lanes[1], 0, true)
);
assert_eq!(place.line, Some(Rect::new(100.0, -1.0, 100.0, 2.0)));
let place = t.drop_at(150.0, 200.0, cards[0]).unwrap();
assert_eq!((place.index, place.changes), (1, true));
assert_eq!(place.line, Some(Rect::new(100.0, 24.0, 100.0, 2.0)));
let place = t.drop_at(50.0, 200.0, cards[0]).unwrap();
assert_eq!(
(place.target, place.index, place.changes),
(lanes[0], 1, true)
);
let place = t.drop_at(50.0, 5.0, cards[0]).unwrap();
assert_eq!((place.index, place.changes, place.line), (0, false, None));
assert_eq!(t.drop_at(250.0, 5.0, cards[0]), None);
}
#[test]
fn new_children_replace_the_old_ones() {
let (mut t, lanes, cards) = lanes();
t.set_children(lanes[1], &[cards[0], cards[2], cards[0], lanes[1]]);
assert_eq!(t.nodes[lanes[0]].children(), [cards[1]]);
assert_eq!(t.nodes[lanes[1]].children(), [cards[0], cards[2]]);
t.set_children(lanes[1], &[]);
assert_eq!(t.nodes[cards[2]].parent, None);
assert!(!t.shown(cards[2]));
let extra = t.add(button("Open"));
t.set_children(cards[1], &[extra]);
assert_eq!(t.nodes[cards[1]].children().len(), 3);
assert_eq!(t.nodes[cards[1]].children()[2], extra);
}
#[test]
fn a_side_bar_has_a_width_for_each_mode() {
let (mut t, bar, items, page) = side_bar(&SideMode::ALL, "App");
let toggle = toggle_of(&t, bar);
assert_eq!(t.nodes[bar].rect.w, 200.0);
assert_eq!(t.nodes[toggle].rect, Rect::new(0.0, 0.0, 48.0, 36.0));
assert_eq!(t.nodes[items[0]].rect, Rect::new(0.0, 36.0, 200.0, 36.0));
assert_eq!(t.nodes[page].rect.x, 200.0);
t.set_side_mode(bar, SideMode::Icons);
t.layout(400.0, 300.0, &Fixed);
assert_eq!(t.nodes[items[1]].rect, Rect::new(0.0, 72.0, 48.0, 36.0));
assert_eq!(t.nodes[page].rect.x, 48.0);
assert_eq!(t.focus_order(), [toggle, items[0], items[1]]);
t.set_side_mode(bar, SideMode::Collapsed);
t.layout(400.0, 300.0, &Fixed);
assert_eq!(t.nodes[bar].rect.w, 48.0);
assert_eq!(t.focus_order(), [toggle]);
assert_eq!(t.hit(5.0, 40.0), None);
assert!(!t.shown(items[0]));
}
#[test]
fn the_button_of_a_side_bar_steps_through_its_modes() {
let (mut t, bar, _, _) = side_bar(&[SideMode::Expanded, SideMode::Collapsed], "");
let toggle = toggle_of(&t, bar);
assert_eq!(t.step_side(toggle), Some(bar));
assert_eq!(t.side_mode(bar), Some(SideMode::Collapsed));
t.step_side(toggle);
assert_eq!(t.side_mode(bar), Some(SideMode::Expanded));
t.set_side_mode(bar, SideMode::Icons);
t.step_side(toggle);
assert_eq!(t.side_mode(bar), Some(SideMode::Expanded));
}
#[test]
fn a_side_bar_with_one_mode_has_no_button() {
let (t, _, items, _) = side_bar(&[SideMode::Expanded], "");
assert_eq!(t.focus_order(), items);
assert_eq!(t.nodes[items[0]].rect.y, 0.0);
let (t, bar, items, _) = side_bar(&[], "App");
let head = t.nodes[bar].children()[0];
assert!(t.shown(head));
assert_eq!(t.focus_order(), items);
}
#[test]
fn a_column_stacks_and_stretches() {
let mut t = Tree::default();
let a = t.add(text("hello"));
let b = t.add(button("OK"));
let col = t.add(with(
Node::container(Content::Blank, Axis::Column, Align::Stretch),
&[a, b],
));
t.root = Some(col);
t.layout(200.0, 100.0, &Fixed);
assert_eq!(t.nodes[a].rect, Rect::new(0.0, 0.0, 200.0, 20.0));
assert_eq!(t.nodes[b].rect, Rect::new(0.0, 20.0, 200.0, 32.0));
}
#[test]
fn fills_share_what_is_left_by_weight() {
let mut t = Tree::default();
let ok = t.add(button("OK"));
let mut one = Node::leaf(Content::Blank);
one.width = Length::Fill(1.0);
let one = t.add(one);
let mut two = Node::leaf(Content::Blank);
two.width = Length::Fill(2.0);
let two = t.add(two);
let mut row = with(
Node::container(Content::Blank, Axis::Row, Align::Center),
&[one, ok, two],
);
row.container.as_mut().unwrap().gap = 8.0;
let row = t.add(row);
t.root = Some(row);
t.layout(400.0, 100.0, &Fixed);
assert!((t.nodes[one].rect.w - 106.666_67).abs() < 0.01);
assert_eq!(t.nodes[ok].rect.x, t.nodes[one].rect.w + 8.0);
assert!((t.nodes[two].rect.w - 213.333_33).abs() < 0.01);
assert_eq!(t.nodes[ok].rect.y, 34.0);
}
#[test]
fn text_wraps_to_the_column_width() {
let mut t = Tree::default();
let long = t.add(text("abcdefghij"));
let col = t.add(with(
Node::container(Content::Blank, Axis::Column, Align::Stretch),
&[long],
));
t.root = Some(col);
t.layout(40.0, 200.0, &Fixed);
assert_eq!(t.nodes[long].rect.h, 40.0);
}
#[test]
fn padding_and_gap_are_inside_and_between() {
let mut t = Tree::default();
let a = t.add(text("a"));
let b = t.add(text("b"));
let mut col = with(
Node::container(Content::card(), Axis::Column, Align::Start),
&[a, b],
);
let c = col.container.as_mut().unwrap();
c.padding = Insets::all(10.0);
c.gap = 5.0;
let col = t.add(col);
t.root = Some(col);
t.layout(100.0, 100.0, &Fixed);
assert_eq!(t.nodes[a].rect, Rect::new(10.0, 10.0, 8.0, 20.0));
assert_eq!(t.nodes[b].rect, Rect::new(10.0, 35.0, 8.0, 20.0));
assert_eq!(t.measure(col, 100.0, 100.0, &Fixed), (28.0, 65.0));
}
#[test]
fn tab_goes_through_enabled_buttons_and_wraps() {
let mut t = Tree::default();
let a = t.add(button("A"));
let mut off = button("off");
off.enabled = false;
let off = t.add(off);
let b = t.add(button("B"));
let row = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Center),
&[a, off, b],
));
t.root = Some(row);
assert_eq!(t.focus_order(), [a, b]);
assert_eq!(t.next_focus(None, false), Some(a));
assert_eq!(t.next_focus(Some(a), false), Some(b));
assert_eq!(t.next_focus(Some(b), false), Some(a));
assert_eq!(t.next_focus(Some(a), true), Some(b));
}
#[test]
fn a_click_goes_to_the_button_under_it() {
let mut t = Tree::default();
let a = t.add(button("A"));
let row = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Start),
&[a],
));
t.root = Some(row);
t.layout(300.0, 100.0, &Fixed);
assert_eq!(t.hit(10.0, 10.0), Some(a));
assert_eq!(t.hit(200.0, 10.0), None);
}
#[test]
fn a_child_moves_to_its_new_container() {
let mut t = Tree::default();
let a = t.add(text("a"));
let first = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Start),
&[a],
));
let second = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Start),
&[a],
));
assert!(t.nodes[first].children().is_empty());
assert_eq!(t.nodes[second].children(), [a]);
assert_eq!(t.nodes[a].parent, Some(second));
}
#[test]
fn a_child_listed_twice_is_in_its_container_once() {
let mut t = Tree::default();
let a = t.add(text("a"));
let b = t.add(text("b"));
let col = t.add(with(
Node::container(Content::Blank, Axis::Column, Align::Start),
&[a, b, a],
));
assert_eq!(t.nodes[col].children(), [a, b]);
}
fn text_box(text: &str, multiline: bool) -> Node {
let mut edit = Editor::new(multiline, usize::MAX);
edit.set_text(text);
Node::leaf(Content::TextBox {
edit,
placeholder: String::new(),
scroll: 0.0,
})
}
#[test]
fn a_text_box_takes_focus_and_grows_with_many_lines() {
let mut t = Tree::default();
let one = t.add(text_box("abcdefghij", false));
let many = t.add(text_box("abcdefghij", true));
let col = t.add(with(
Node::container(Content::Blank, Axis::Column, Align::Start),
&[one, many],
));
t.root = Some(col);
t.layout(60.0, 300.0, &Fixed);
assert_eq!(t.focus_order(), [one, many]);
assert_eq!(t.nodes[one].rect, Rect::new(0.0, 0.0, 60.0, 32.0));
assert_eq!(t.nodes[many].rect.h, 72.0);
}
#[test]
fn a_widget_outside_the_root_takes_no_focus() {
let mut t = Tree::default();
let a = t.add(button("A"));
let b = t.add(button("B"));
let row = t.add(with(
Node::container(Content::Blank, Axis::Row, Align::Start),
&[a],
));
t.root = Some(row);
assert_eq!(t.focus_order(), [a]);
assert!(!t.focusable(b));
}
}