use teksilo_canvas::{Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
#[derive(Debug)]
pub struct Center {
child_id: Option<WidgetId>,
pending_child: Option<PendingChild>,
}
impl Center {
pub fn new() -> Self {
Self {
child_id: None,
pending_child: None,
}
}
pub fn child_id(mut self, id: WidgetId) -> Self {
self.pending_child = Some(PendingChild::Id(id));
self
}
pub fn child(mut self, widget: impl Widget + 'static) -> Self {
self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
self
}
}
impl Default for Center {
fn default() -> Self {
Self::new()
}
}
impl Widget for Center {
fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
if let Some(pending) = self.pending_child.take() {
self.child_id = Some(match pending {
PendingChild::Id(id) => id,
PendingChild::Deferred(w) => ctx.add_boxed(w),
});
}
self.child_id.into_iter().collect()
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let child = self
.child_id
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or(Size::ZERO);
Size::new(
proposal.width.unwrap_or(child.width),
proposal.height.unwrap_or(child.height),
)
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
let child_proposal = SizeProposal::exact(bounds.width, bounds.height);
for child in children.iter_mut() {
let child_size = ctx
.child_size(child.id, child_proposal)
.unwrap_or(bounds.size());
let dx = (bounds.width - child_size.width) / 2.0;
let dy = (bounds.height - child_size.height) / 2.0;
child.origin = Point::new(bounds.x + dx, bounds.y + dy);
child.size = child_size;
}
}
fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
}
fn children(&self) -> Vec<WidgetId> {
self.child_id.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_canvas::Size;
use teksilo_core::widget_tree::WidgetTree;
#[derive(Debug)]
struct FixedLeaf(f32, f32);
impl Widget for FixedLeaf {
fn layout_response(
&self,
_proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
Size::new(self.0, self.1).into()
}
}
#[test]
fn centers_child() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(40.0, 20.0));
let _center = tree.add(Center::new().child_id(child));
tree.layout(SizeProposal::exact(200.0, 100.0));
let cb = tree.bounds(child);
assert!((cb.x - 80.0).abs() < 0.01); assert!((cb.y - 40.0).abs() < 0.01); }
#[test]
fn claims_full_space() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(40.0, 20.0));
let center = tree.add(Center::new().child_id(child));
tree.layout(SizeProposal::exact(200.0, 100.0));
let cb = tree.bounds(center);
assert!((cb.width - 200.0).abs() < 0.01);
assert!((cb.height - 100.0).abs() < 0.01);
}
#[derive(Debug)]
struct AdaptiveLeaf {
natural_width: f32,
height: f32,
}
impl Widget for AdaptiveLeaf {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let w = match proposal.width {
Some(max) => self.natural_width.min(max),
None => self.natural_width,
};
Size::new(w, self.height).into()
}
}
#[test]
fn caps_adaptive_child_at_bounds() {
let mut tree = WidgetTree::new();
let child = tree.add(AdaptiveLeaf {
natural_width: 300.0,
height: 16.0,
});
let _center = tree.add(Center::new().child_id(child));
tree.layout(SizeProposal::exact(100.0, 40.0));
let cb = tree.bounds(child);
assert!(
(cb.width - 100.0).abs() < 0.01,
"adaptive child must be capped at Center's width (100), got {}",
cb.width
);
assert!((cb.x - 0.0).abs() < 0.01, "capped child fills, x = 0");
assert!((cb.y - 12.0).abs() < 0.01, "still vertically centered");
let mut t2 = WidgetTree::new();
let small = t2.add(AdaptiveLeaf {
natural_width: 40.0,
height: 16.0,
});
let _c2 = t2.add(Center::new().child_id(small));
t2.layout(SizeProposal::exact(100.0, 40.0));
let sb = t2.bounds(small);
assert!((sb.width - 40.0).abs() < 0.01);
assert!((sb.x - 30.0).abs() < 0.01, "(100-40)/2");
}
#[test]
fn center_in_hstack_shrink_wraps_child_without_overflow() {
use crate::primitives::{Expand, HStack};
let mut tree = WidgetTree::new();
let logo = tree.add(FixedLeaf(50.0, 20.0));
let title = tree.add(FixedLeaf(40.0, 20.0));
let center = tree.add(Center::new().child_id(title));
let _row = tree.add(HStack::new().add_child(logo).add_child(center));
tree.layout(SizeProposal::exact(300.0, 20.0));
assert!(
(tree.bounds(center).width - 40.0).abs() < 0.01,
"Center should shrink-wrap its child (40), got {}",
tree.bounds(center).width
);
let logo_right = tree.bounds(logo).x + tree.bounds(logo).width;
assert!(
tree.bounds(title).x >= logo_right - 0.01,
"title must not overflow left over the logo: title.x={}, logo right={}",
tree.bounds(title).x,
logo_right
);
let mut t2 = WidgetTree::new();
let logo2 = t2.add(FixedLeaf(50.0, 20.0));
let title2 = t2.add(FixedLeaf(40.0, 20.0));
let center2 = t2.add(Center::new().child_id(title2));
let exp = t2.add(Expand::horizontal().child_id(center2));
let _row2 = t2.add(HStack::new().add_child(logo2).add_child(exp));
t2.layout(SizeProposal::exact(300.0, 20.0));
assert!(
(t2.bounds(exp).width - 250.0).abs() < 0.01,
"Expand should claim the slack (250), got {}",
t2.bounds(exp).width
);
assert!(
(t2.bounds(title2).x - 155.0).abs() < 0.5,
"title should be centered in the remaining space (~155), got {}",
t2.bounds(title2).x
);
}
}