use accesskit::Node as AccessKitNode;
use glam::Vec2;
use martensite_core::widget::{LayoutConstraints, LayoutContext, Widget};
use martensite_core::Rect;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum StackAlignment {
#[default]
TopStart,
TopEnd,
BottomStart,
BottomEnd,
Center,
Stretch,
}
pub struct Stack {
pub alignment: StackAlignment,
pub children: Vec<Box<dyn Widget>>,
child_sizes: Vec<Vec2>,
cached_bounds: Rect,
}
impl Stack {
pub fn new() -> Self {
Self {
alignment: StackAlignment::default(),
children: Vec::new(),
child_sizes: Vec::new(),
cached_bounds: Rect::default(),
}
}
#[inline]
pub fn alignment(mut self, alignment: StackAlignment) -> Self {
self.alignment = alignment;
self
}
#[inline]
pub fn child(mut self, child: impl Widget + 'static) -> Self {
self.children.push(Box::new(child));
self
}
#[inline]
pub fn child_count(&self) -> usize {
self.children.len()
}
}
impl Default for Stack {
fn default() -> Self {
Self::new()
}
}
impl Widget for Stack {
fn measure(&mut self, cx: &mut LayoutContext, constraints: LayoutConstraints) -> Vec2 {
self.child_sizes.clear();
let n = self.children.len();
if n == 0 {
return Vec2::ZERO;
}
self.child_sizes.reserve(n);
let mut max_width = 0.0f32;
let mut max_height = 0.0f32;
for child in &mut self.children {
let child_constraints = LayoutConstraints {
min_size: Vec2::ZERO,
max_size: constraints.max_size,
};
let size = child.measure(cx, child_constraints);
self.child_sizes.push(size);
max_width = max_width.max(size.x);
max_height = max_height.max(size.y);
}
Vec2::new(max_width, max_height)
}
fn layout(&mut self, cx: &mut LayoutContext, bounds: Rect) {
self.cached_bounds = bounds;
let alignment = self.alignment;
for (i, child) in self.children.iter_mut().enumerate() {
let child_size = self.child_sizes.get(i).copied().unwrap_or(Vec2::ZERO);
let (w, h) = if matches!(alignment, StackAlignment::Stretch) {
(bounds.size.x, bounds.size.y)
} else {
(child_size.x, child_size.y)
};
let pos = match alignment {
StackAlignment::TopStart | StackAlignment::Stretch => bounds.origin,
StackAlignment::TopEnd => {
Vec2::new(bounds.origin.x + bounds.size.x - w, bounds.origin.y)
}
StackAlignment::BottomStart => {
Vec2::new(bounds.origin.x, bounds.origin.y + bounds.size.y - h)
}
StackAlignment::BottomEnd => Vec2::new(
bounds.origin.x + bounds.size.x - w,
bounds.origin.y + bounds.size.y - h,
),
StackAlignment::Center => Vec2::new(
bounds.origin.x + (bounds.size.x - w) / 2.0,
bounds.origin.y + (bounds.size.y - h) / 2.0,
),
};
child.layout(cx, Rect::new(pos.x, pos.y, w, h));
}
}
fn accessibility(&self, node: &mut AccessKitNode) {
node.set_role(accesskit::Role::GenericContainer);
}
}
impl std::fmt::Debug for Stack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Stack")
.field("alignment", &self.alignment)
.field("child_count", &self.children.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use martensite_core::widget::DummyWidget;
use martensite_core::HotNode;
fn make_cx(hot: &mut HotNode) -> LayoutContext<'_> {
LayoutContext { hot }
}
#[test]
fn stack_new_is_empty() {
let s = Stack::new();
assert!(s.children.is_empty());
}
#[test]
fn stack_measure_empty() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut s = Stack::new();
let size = s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
assert_eq!(size, Vec2::ZERO);
}
#[test]
fn stack_measure_with_children() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut s = Stack::new().child(DummyWidget).child(DummyWidget);
let size = s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
assert_eq!(size, Vec2::ZERO);
}
#[test]
fn stack_layout_positions_children() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut s = Stack::new().child(DummyWidget).child(DummyWidget);
s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
s.layout(&mut cx, Rect::new(10.0, 20.0, 100.0, 50.0));
assert_eq!(s.cached_bounds, Rect::new(10.0, 20.0, 100.0, 50.0));
}
#[test]
fn stack_alignment_center() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut s = Stack::new()
.alignment(StackAlignment::Center)
.child(DummyWidget);
s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
s.layout(&mut cx, Rect::new(0.0, 0.0, 100.0, 100.0));
}
#[test]
fn stack_alignment_stretch() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut s = Stack::new()
.alignment(StackAlignment::Stretch)
.child(DummyWidget);
s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
s.layout(&mut cx, Rect::new(0.0, 0.0, 100.0, 100.0));
}
#[test]
fn stack_alignment_corners() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
for align in [
StackAlignment::TopStart,
StackAlignment::TopEnd,
StackAlignment::BottomStart,
StackAlignment::BottomEnd,
] {
let mut s = Stack::new().alignment(align).child(DummyWidget);
s.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
s.layout(&mut cx, Rect::new(0.0, 0.0, 100.0, 100.0));
}
}
#[test]
fn stack_debug_format() {
let s = Stack::new()
.alignment(StackAlignment::Center)
.child(DummyWidget);
let debug = format!("{:?}", s);
assert!(debug.contains("Stack"));
assert!(debug.contains("Center"));
}
}