#![warn(clippy::all, missing_docs)]
#![deny(clippy::unwrap_used)]
use std::collections::{HashMap, VecDeque};
use thunderdome::Arena;
pub mod clip;
pub mod grid;
pub mod limit;
pub mod overlap;
pub mod padding;
pub mod prelude;
pub mod proportion;
pub mod split3;
pub mod stack;
pub mod visibility;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct NodeIndex(thunderdome::Index);
#[derive(Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct OwnedIndex(thunderdome::Index);
impl OwnedIndex {
pub fn shareable(&self) -> NodeIndex {
NodeIndex(self.0)
}
}
impl From<OwnedIndex> for NodeIndex {
fn from(value: OwnedIndex) -> Self {
NodeIndex(value.0)
}
}
impl From<&OwnedIndex> for NodeIndex {
fn from(value: &OwnedIndex) -> Self {
NodeIndex(value.0)
}
}
impl std::borrow::Borrow<NodeIndex> for OwnedIndex {
fn borrow(&self) -> &NodeIndex {
unsafe { std::mem::transmute(self) }
}
}
impl From<thunderdome::Index> for NodeIndex {
fn from(value: thunderdome::Index) -> Self {
NodeIndex(value)
}
}
impl From<NodeIndex> for thunderdome::Index {
fn from(value: NodeIndex) -> thunderdome::Index {
value.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub enum Alignment {
Begin,
Center,
End,
#[default]
Full,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum Anchor {
Begin,
#[default]
Center,
End,
}
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub 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, h }
}
pub fn new_checked(x: f32, y: f32, w: f32, h: f32) -> Option<Self> {
if w >= 0.0 && h >= 0.0 {
Some(Self::new(x, y, w, h))
} else {
None
}
}
pub fn is_valid(&self) -> bool {
self.w >= 0.0 && self.h >= 0.0
}
pub fn is_empty(&self) -> bool {
self.w == 0.0 || self.h == 0.0
}
pub fn is_zero(&self) -> bool {
self.w == 0.0 && self.h == 0.0
}
pub fn get_size(&self) -> (f32, f32) {
(self.w, self.h)
}
pub fn with_size(mut self, w: f32, h: f32) -> Self {
self.w = w;
self.h = h;
self
}
pub fn shrink_begin_x(mut self, amount: f32) -> Self {
self.w -= amount;
self
}
pub fn shrink_end_x(mut self, amount: f32) -> Self {
self.w -= amount;
self.x += amount;
self
}
pub fn shrink_center_x(mut self, amount: f32) -> Self {
self.w -= amount;
self.x += amount * 0.5;
self
}
pub fn shrink_begin_y(mut self, amount: f32) -> Self {
self.h -= amount;
self
}
pub fn shrink_end_y(mut self, amount: f32) -> Self {
self.h -= amount;
self.y += amount;
self
}
pub fn shrink_center_y(mut self, amount: f32) -> Self {
self.h -= amount;
self.y += amount * 0.5;
self
}
pub fn align(mut self, align: (Alignment, Alignment), min: (f32, f32)) -> Self {
self = match align.0 {
Alignment::Begin => self.shrink_begin_x(self.w - min.0),
Alignment::Center => self.shrink_center_x(self.w - min.0),
Alignment::End => self.shrink_end_x(self.w - min.0),
Alignment::Full => self,
};
match align.1 {
Alignment::Begin => self.shrink_begin_y(self.h - min.1),
Alignment::Center => self.shrink_center_y(self.h - min.1),
Alignment::End => self.shrink_end_y(self.h - min.1),
Alignment::Full => self,
}
}
pub fn anchor(mut self, anchor: (Anchor, Anchor), size: (f32, f32)) -> Self {
self = match anchor.0 {
Anchor::Begin => self.shrink_begin_x(self.w - size.0),
Anchor::Center => self.shrink_center_x(self.w - size.0),
Anchor::End => self.shrink_end_x(self.w - size.0),
};
match anchor.1 {
Anchor::Begin => self.shrink_begin_y(self.h - size.1),
Anchor::Center => self.shrink_center_y(self.h - size.1),
Anchor::End => self.shrink_end_y(self.h - size.1),
}
}
pub fn contains(&self, point: (f32, f32)) -> bool {
point.0 >= self.x
&& point.0 < self.x + self.w
&& point.1 >= self.y
&& point.1 < self.y + self.h
}
pub fn contains_rect(&self, rect: Rect) -> bool {
rect.x >= self.x
&& rect.x + rect.w <= self.x + self.w
&& rect.y >= self.y
&& rect.y + rect.h <= self.y + self.h
}
pub fn intersect(self, other: Self) -> Self {
let x1 = self.x.max(other.x);
let y1 = self.y.max(other.y);
let x2 = (self.x + self.w).min(other.x + other.w);
let y2 = (self.y + self.h).min(other.y + other.h);
Self::new(x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0))
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct NodeCache {
pub min_size: (f32, f32),
pub rect: Rect,
}
pub trait UiNode: std::any::Any {
fn get_align(&self) -> (Alignment, Alignment);
fn get_align_mut(&mut self) -> (&mut Alignment, &mut Alignment);
fn calculate_min_size(&self, tree: &UiTree) -> (f32, f32);
fn calculate_rects(&self, cache: &NodeCache, tree: &UiTree) -> Vec<Rect> {
let _ = (cache, tree);
vec![]
}
fn get_children(&self) -> Vec<NodeIndex> {
vec![]
}
fn get_visible_children(&self) -> Vec<NodeIndex> {
self.get_children()
}
}
impl dyn UiNode {
pub fn downcast_ref<T: UiNode>(&self) -> Option<&T> {
(self as &dyn std::any::Any).downcast_ref()
}
pub fn downcast_mut<T: UiNode>(&mut self) -> Option<&mut T> {
(self as &mut dyn std::any::Any).downcast_mut()
}
}
pub trait UiWalker {
fn enter(&mut self, node: &mut dyn UiNode, rect: Rect, index: NodeIndex);
fn leave(&mut self, node: &mut dyn UiNode, rect: Rect, index: NodeIndex);
}
pub trait PointTester {
fn accept(&self, p: (f32, f32), node: &dyn UiNode, rect: Rect, index: NodeIndex) -> bool;
}
#[derive(Default)]
pub struct LayoutConfig {
pub align_root: bool,
pub force_good: bool,
}
pub struct UiTree {
root: thunderdome::Index,
arena: Arena<Box<dyn UiNode>>,
cache: HashMap<thunderdome::Index, NodeCache>,
}
impl UiTree {
pub fn new(root: impl UiNode) -> Self {
let mut arena = Arena::new();
let index = arena.insert(Box::new(root) as Box<dyn UiNode>);
Self {
root: index,
arena,
cache: HashMap::new(),
}
}
pub fn add_node(&mut self, node: impl UiNode) -> OwnedIndex {
let index = self.arena.insert(Box::new(node) as Box<dyn UiNode>);
self.cache.insert(index, Default::default());
OwnedIndex(index)
}
pub fn remove_node(&mut self, index: OwnedIndex) {
assert_ne!(index.0, self.root, "Root node cannot be removed");
let mut queue: VecDeque<_> = self.arena[index.0].get_children().into();
while let Some(child) = queue.pop_front() {
queue.extend(self.arena[child.0].get_children());
self.arena.remove(child.0);
self.cache.remove(&child.0);
}
self.arena.remove(index.0);
self.cache.remove(&index.0);
}
pub fn get_cache(&self, index: NodeIndex) -> Option<&NodeCache> {
self.cache.get(&index.0)
}
pub fn get_node(&self, index: NodeIndex) -> Option<&dyn UiNode> {
self.arena.get(index.0).map(|node| &**node)
}
pub fn get_node_mut(&mut self, index: NodeIndex) -> Option<&mut dyn UiNode> {
self.arena.get_mut(index.0).map(|node| &mut **node)
}
pub fn get_cast<T: UiNode>(&self, index: NodeIndex) -> Option<&T> {
self.get_node(index).and_then(|node| node.downcast_ref())
}
pub fn get_cast_mut<T: UiNode>(&mut self, index: NodeIndex) -> Option<&mut T> {
self.get_node_mut(index).and_then(|node| node.downcast_mut())
}
pub fn root_index(&self) -> NodeIndex {
NodeIndex(self.root)
}
pub fn calculate_layout(&mut self, root_rect: Rect) -> bool {
self.calculate_layout_ex(root_rect, Default::default())
}
pub fn calculate_layout_ex(&mut self, root_rect: Rect, config: LayoutConfig) -> bool {
for v in self.cache.values_mut() {
*v = Default::default();
}
let mut visit_stack = vec![self.root];
let mut min_stack = Vec::new();
while let Some(v) = visit_stack.pop() {
min_stack.push(v);
visit_stack.extend(
self.arena[v]
.get_visible_children()
.into_iter()
.map(|v| v.0),
);
}
for v in min_stack.iter().rev() {
let min = self.arena[*v].calculate_min_size(self);
self.cache.entry(*v).or_default().min_size = min;
}
let (is_good, root_rect) = if config.force_good {
let root_min = self.cache[&self.root].min_size;
(
true,
Rect::new(
0.0,
0.0,
root_min.0.max(root_rect.w),
root_min.1.max(root_rect.h),
),
)
} else {
let is_good = root_rect.w >= self.cache[&self.root].min_size.0
&& root_rect.h >= self.cache[&self.root].min_size.1;
(is_good, root_rect)
};
if config.align_root {
let align = self.arena[self.root].get_align();
let min = self.cache[&self.root].min_size;
self.cache
.entry(self.root)
.and_modify(|e| e.rect = root_rect.align(align, min));
} else {
self.cache
.entry(self.root)
.and_modify(|e| e.rect = root_rect);
}
for v in min_stack {
let rects = self.arena[v].calculate_rects(&self.cache[&v], self);
for (child, rect) in self.arena[v].get_children().iter().zip(rects) {
self.cache.entry(child.0).or_default().rect = rect;
}
}
is_good
}
pub fn walk_tree(&mut self, walker: &mut impl UiWalker, use_visible: bool) {
self.walk_node(NodeIndex(self.root), walker, use_visible);
}
#[doc = concat!("[`", stringify!(UiNode), "::get_visible_children`]")]
pub fn walk_node(&mut self, index: NodeIndex, walker: &mut impl UiWalker, use_visible: bool) {
let rect = self.cache[&index.0].rect;
walker.enter(self.arena[index.0].as_mut(), rect, index);
let children = if use_visible {
self.arena[index.0].get_visible_children()
} else {
self.arena[index.0].get_children()
};
for child in children {
self.walk_node(child, walker, use_visible);
}
walker.leave(self.arena[index.0].as_mut(), rect, index);
}
pub fn point_test(&self, point: (f32, f32), tester: &mut impl PointTester) {
self.point_test_int(self.root, point, tester);
}
pub fn point_test_node(
&self,
index: NodeIndex,
point: (f32, f32),
tester: &mut impl PointTester,
) {
self.point_test_int(index.0, point, tester);
}
fn point_test_int(
&self,
index: thunderdome::Index,
point: (f32, f32),
tester: &mut impl PointTester,
) -> bool {
let rect = self.cache[&index].rect;
if !rect.contains(point) {
return false;
}
if tester.accept(point, self.arena[index].as_ref(), rect, NodeIndex(index)) {
return true;
}
let children = self.arena[index].get_children();
for child in children.into_iter() {
if self.point_test_int(child.0, point, tester) {
return true;
}
}
false
}
}
pub struct PartialTree {
arena: Arena<Box<dyn UiNode>>,
}
impl PartialTree {
pub fn new() -> Self {
Self {
arena: Arena::new(),
}
}
pub fn add_node(&mut self, node: impl UiNode) -> OwnedIndex {
OwnedIndex(self.arena.insert(Box::new(node) as Box<dyn UiNode>))
}
pub fn complete(self, root: OwnedIndex) -> UiTree {
let cache = self
.arena
.iter()
.map(|(id, _)| (id, Default::default()))
.collect();
UiTree {
arena: self.arena,
root: root.0,
cache,
}
}
}
impl Default for PartialTree {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! ui {
($tree:expr, $($node:tt)*) => {
$crate::ui!(@@_node $tree;; $($node)*)
};
(%%, $($node:tt)*) => {
{
let mut tree = $crate::PartialTree::new();
let root = $crate::ui!(@@_node &mut tree;; $($node)*);
tree.complete(root)
}
};
(%%!, $($node:tt)*) => {
{
let mut tree = $crate::PartialTree::new();
let root = $crate::ui!(@@_node &mut tree;; $($node)*);
(tree, root)
}
};
(@@_node $tree:expr;; #$expr:expr) => {
$expr
};
(@@_node $tree:expr;; $binding:ident = $ha:tt | $va:tt $node:expr) => {
{
let __node_idx = $tree.add_node($crate::ui!(@@_align $ha | $va $node));
$binding = __node_idx.shareable();
__node_idx
}
};
(@@_node $tree:expr;; $binding:ident = $ha:tt | $va:tt $node:expr => [ $($child:tt)* ]) => {
{
let __ui_node = $crate::ui!(
@@_child
$tree;;
$crate::ui!(@@_align $ha | $va $node)
=> [ $($child)* ]
);
let __node_idx = $tree.add_node(__ui_node);
$binding = __node_idx.shareable();
__node_idx
}
};
(@@_node $tree:expr;; $ha:tt | $va:tt $node:expr) => {
{
let __node_idx = $tree.add_node($crate::ui!(@@_align $ha | $va $node));
__node_idx
}
};
(@@_node $tree:expr;; $ha:tt | $va:tt $node:expr => [ $($child:tt)* ]) => {
{
let __ui_node = $crate::ui!(
@@_child
$tree;;
$crate::ui!(@@_align $ha | $va $node)
=> [ $($child)* ]
);
let __node_idx = $tree.add_node(__ui_node);
__node_idx
}
};
(@@_child $tree:expr;; $node:expr => [ ]) => {
$node
};
(
@@_child
$tree:expr;;
$node:expr
=> [
$binding:ident =
$ha:tt |
$va:tt
$child:expr
$(=> [ $($grand:tt)* ])?
$(, $($rest:tt)*)?
]
) => {
{
let __node_idx = $crate::ui!(
@@_node
$tree;;
$binding =
$ha |
$va
$child
$(=> [ $($grand)* ])?
);
$crate::ui!(
@@_child
$tree;;
$node.with_child(__node_idx)
=> [ $($($rest)*)? ]
)
}
};
(
@@_child
$tree:expr;;
$node:expr
=> [
$ha:tt |
$va:tt
$child:expr
$(=> [ $($grand:tt)* ])?
$(, $($rest:tt)*)?
]
) => {
{
let __node_idx = $crate::ui!(
@@_node
$tree;;
$ha |
$va
$child
$(=> [ $($grand)* ])?
);
$crate::ui!(
@@_child
$tree;;
$node.with_child(__node_idx)
=> [ $($($rest)*)? ]
)
}
};
(@@_align $ha:tt | $va: tt $node:expr) => {
$node.with_align(($crate::ui!(@@_align $ha), $crate::ui!(@@_align $va)))
};
(@@_align <) => {
$crate::Alignment::Begin
};
(@@_align -) => {
$crate::Alignment::Center
};
(@@_align >) => {
$crate::Alignment::End
};
(@@_align +) => {
$crate::Alignment::Full
};
(@@_align $a:tt) => {
compile_error!("Invalid alignment. Only `<`, `-`, `>`, and `+` are allowed.")
};
(@@_node $tree:expr;; $node:expr $(=> [ $($child:tt)* ])?) => {
compile_error!(
"Nodes must have an alignment, represented by two of `<`, `-`, `>`, and `+`, \
separated by a `|`"
)
};
(@@_node $tree:expr;; $($tt:tt)*) => {
compile_error!(concat!(
"Invalid node syntax. A node should have an optional assignment, two alignment \
characters, separated by a pipe, and a node expression, optionally followed by \
a fat arrow and children in square brackets.\n\nExample:\n\
`target = -|< Node::new() => [ ]`\n\nFound: `",
$(stringify!($tt),)*
"`."
))
};
($($random:tt)*, $($node:tt)*) => {
compile_error!(concat!(
"Expected either a mutable reference to a tree, `%%`, or `%%!`, found `",
stringify!($($random)*),
"` where the tree should have been."
));
};
($($tt:tt)*) => {
compile_error!(concat!(
"Expected a tree followed by a `,`, followed by a node, found `",
stringify!($($tt)*),
"`."
))
};
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn rect_intersect1() {
let r1 = Rect::new(0.0, 0.0, 10.0, 10.0);
let r2 = Rect::new(5.0, 0.0, 10.0, 10.0);
assert_eq!(
r1.intersect(r2),
Rect::new(5.0, 0.0, 5.0, 10.0)
);
}
#[test]
fn valid_tree() {
let root;
let sep;
let mut tree = ui!(
%%,
root = +|+ stack::HStack::new() => [
-|> split3::HSplit3::new() => [
<|+ padding::Spacer::sized((10.0, 5.0)),
sep = -|+ padding::Spacer::sized((1.0, 10.0)),
>|+ padding::Spacer::sized((10.0, 5.0)),
],
-|< padding::Spacer::sized((10.0, 10.0)),
]
);
tree
.get_cast_mut::<stack::HStack>(root)
.unwrap()
.spacing = 10.0;
assert_eq!(
tree
.get_cast::<padding::Spacer>(sep)
.unwrap()
.get_size(),
(1.0, 10.0)
)
}
}