use accesskit::Node as AccessKitNode;
use glam::Vec2;
use martensite_core::widget::{LayoutConstraints, LayoutContext, Widget};
use martensite_core::Rect;
use martensite_layout::geometry::{EdgeInsets, Size};
use martensite_theme::Oklab;
pub struct Container {
pub padding: EdgeInsets,
pub background: Option<Oklab>,
pub child: Option<Box<dyn Widget>>,
cached_content_size: Size,
cached_bounds: Rect,
}
impl Container {
pub fn new() -> Self {
Self {
padding: EdgeInsets::default(),
background: None,
child: None,
cached_content_size: Size::zero(),
cached_bounds: Rect::default(),
}
}
#[inline]
pub fn padding(mut self, padding: EdgeInsets) -> Self {
self.padding = padding;
self
}
#[inline]
pub fn padding_uniform(mut self, value: f32) -> Self {
self.padding = EdgeInsets::uniform(value);
self
}
#[inline]
pub fn background(mut self, color: Oklab) -> Self {
self.background = Some(color);
self
}
#[inline]
pub fn child(mut self, child: impl Widget + 'static) -> Self {
self.child = Some(Box::new(child));
self
}
#[inline]
pub fn content_area(&self) -> Rect {
Rect::new(
self.cached_bounds.origin.x + self.padding.left,
self.cached_bounds.origin.y + self.padding.top,
self.cached_content_size.width,
self.cached_content_size.height,
)
}
}
impl Default for Container {
fn default() -> Self {
Self::new()
}
}
impl Widget for Container {
fn measure(&mut self, cx: &mut LayoutContext, constraints: LayoutConstraints) -> Vec2 {
let h_inset = self.padding.horizontal();
let v_inset = self.padding.vertical();
let child_min = Vec2::new(
(constraints.min_size.x - h_inset).max(0.0),
(constraints.min_size.y - v_inset).max(0.0),
);
let child_max = Vec2::new(
(constraints.max_size.x - h_inset).max(0.0),
(constraints.max_size.y - v_inset).max(0.0),
);
let child_size = if let Some(child) = &mut self.child {
let child_constraints = LayoutConstraints {
min_size: child_min,
max_size: child_max,
};
child.measure(cx, child_constraints)
} else {
Vec2::ZERO
};
let total = Vec2::new(child_size.x + h_inset, child_size.y + v_inset);
self.cached_content_size = Size::new(child_size.x, child_size.y);
total
}
fn layout(&mut self, cx: &mut LayoutContext, bounds: Rect) {
self.cached_bounds = bounds;
let content = Rect::new(
bounds.origin.x + self.padding.left,
bounds.origin.y + self.padding.top,
(bounds.size.x - self.padding.horizontal()).max(0.0),
(bounds.size.y - self.padding.vertical()).max(0.0),
);
self.cached_content_size = Size::new(content.size.x, content.size.y);
if let Some(child) = &mut self.child {
child.layout(cx, content);
}
}
fn accessibility(&self, node: &mut AccessKitNode) {
node.set_role(accesskit::Role::GenericContainer);
}
}
impl std::fmt::Debug for Container {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Container")
.field("padding", &self.padding)
.field("background", &self.background)
.field("has_child", &self.child.is_some())
.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 container_new_is_empty() {
let c = Container::new();
assert!(c.child.is_none());
assert!(c.background.is_none());
assert!(c.padding.is_zero());
}
#[test]
fn container_builder_methods() {
let c = Container::new().padding_uniform(10.0).background(Oklab {
l: 1.0,
a: 0.0,
b: 0.0,
alpha: 1.0,
});
assert_eq!(c.padding.horizontal(), 20.0);
assert!(c.background.is_some());
}
#[test]
fn container_measure_no_child() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut c = Container::new().padding_uniform(10.0);
let size = c.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
assert_eq!(size, Vec2::new(20.0, 20.0));
}
#[test]
fn container_measure_with_child() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut c = Container::new().padding_uniform(10.0).child(DummyWidget);
let size = c.measure(
&mut cx,
LayoutConstraints {
min_size: Vec2::ZERO,
max_size: Vec2::new(100.0, 100.0),
},
);
assert_eq!(size, Vec2::new(20.0, 20.0));
}
#[test]
fn container_layout_sets_bounds() {
let mut hot = HotNode::new(taffy::NodeId::new(1));
let mut cx = make_cx(&mut hot);
let mut c = Container::new().padding_uniform(10.0);
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
c.layout(&mut cx, bounds);
assert_eq!(c.cached_bounds, bounds);
let content = c.content_area();
assert_eq!(content.origin.x, 10.0);
assert_eq!(content.origin.y, 10.0);
}
#[test]
fn container_debug_format() {
let c = Container::new().padding_uniform(5.0);
let debug = format!("{:?}", c);
assert!(debug.contains("Container"));
}
}