use teksilo_canvas::{Point, Rect, Size, SizeProposal};
use teksilo_core::widget::{
LayoutContext, LayoutResponse, PaintContext, PendingChild, Widget, WidgetPlacement,
};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::Alignment;
#[derive(Debug)]
pub struct Expand {
child_id: Option<WidgetId>,
pending_child: Option<PendingChild>,
horizontal: bool,
vertical: bool,
flex: f32,
child_alignment: Option<Alignment>,
respect_intrinsic: bool,
}
impl Expand {
pub fn new() -> Self {
Self {
child_id: None,
pending_child: None,
horizontal: true,
vertical: true,
flex: 1.0,
child_alignment: None,
respect_intrinsic: false,
}
}
pub fn horizontal() -> Self {
Self {
child_id: None,
pending_child: None,
horizontal: true,
vertical: false,
flex: 1.0,
child_alignment: None,
respect_intrinsic: false,
}
}
pub fn vertical() -> Self {
Self {
child_id: None,
pending_child: None,
horizontal: false,
vertical: true,
flex: 1.0,
child_alignment: None,
respect_intrinsic: false,
}
}
pub fn flex(mut self, flex: f32) -> Self {
self.flex = flex.max(0.0);
self
}
pub fn align_child(mut self, alignment: Alignment) -> Self {
self.child_alignment = Some(alignment);
self
}
pub fn respect_intrinsic(mut self) -> Self {
self.respect_intrinsic = true;
self
}
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 Expand {
fn default() -> Self {
Self::new()
}
}
impl Widget for Expand {
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) -> LayoutResponse {
let child_size = self
.child_id
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or(Size::ZERO);
let basis_w = if self.respect_intrinsic {
child_size.width
} else {
0.0
};
let basis_h = if self.respect_intrinsic {
child_size.height
} else {
0.0
};
let w = match proposal.width {
Some(pw) => pw,
None if self.horizontal => basis_w,
None => child_size.width,
};
let h = match proposal.height {
Some(ph) => ph,
None if self.vertical => basis_h,
None => child_size.height,
};
let flex = match (proposal.width, proposal.height) {
(None, Some(_)) if !self.horizontal => 0.0,
(Some(_), None) if !self.vertical => 0.0,
_ => self.flex,
};
LayoutResponse::flexible(Size::new(w, h), flex)
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
ctx: &LayoutContext,
) {
for child in children.iter_mut() {
if let Some(alignment) = self.child_alignment {
let child_size = ctx
.child_size(child.id, SizeProposal::exact(bounds.width, bounds.height))
.unwrap_or(bounds.size());
let rtl = ctx.is_rtl();
let (dx, dy) = alignment.resolve(
(child_size.width, child_size.height),
(bounds.width, bounds.height),
rtl,
);
child.origin = Point::new(bounds.x + dx, bounds.y + dy);
child.size = child_size;
} else {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
}
fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
fn children(&self) -> Vec<WidgetId> {
self.child_id.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_core::widget_tree::WidgetTree;
#[derive(Debug)]
struct FixedLeaf(f32, f32);
impl Widget for FixedLeaf {
fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
Size::new(self.0, self.1).into()
}
}
#[derive(Debug)]
struct WrappingLeaf {
natural_width: f32,
line_height: f32,
}
impl Widget for WrappingLeaf {
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
match proposal.width {
Some(w) if w > 0.0 => {
let lines = (self.natural_width / w).ceil().max(1.0);
Size::new(w, lines * self.line_height).into()
}
_ => Size::new(self.natural_width, self.line_height).into(),
}
}
}
#[test]
fn expand_measures_a_width_dependent_child_at_the_width_it_will_get() {
let mut tree = WidgetTree::new();
let child = tree.add(WrappingLeaf {
natural_width: 400.0,
line_height: 16.0,
});
let expand = tree.add(Expand::horizontal().child_id(child));
tree.layout(SizeProposal {
width: Some(100.0),
height: None,
});
let eb = tree.bounds(expand);
assert!(
(eb.height - 64.0).abs() < 0.01,
"400px of content at 100px wide is four 16px lines; \
got {} (a one-line answer means the bound width was discarded)",
eb.height
);
}
#[test]
fn expand_still_shrink_wraps_on_a_fully_open_proposal() {
let mut tree = WidgetTree::new();
let child = tree.add(WrappingLeaf {
natural_width: 400.0,
line_height: 16.0,
});
let expand = tree.add(Expand::horizontal().child_id(child));
tree.layout(SizeProposal::unspecified());
let eb = tree.bounds(expand);
assert!(
(eb.height - 16.0).abs() < 0.01,
"with no width to wrap at, the child's single-line height stands (got {})",
eb.height
);
}
#[test]
fn expand_at_root_fills_proposal() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(40.0, 20.0));
let expand = tree.add(Expand::new().child_id(child));
tree.layout(SizeProposal::exact(200.0, 100.0));
let eb = tree.bounds(expand);
assert!((eb.width - 200.0).abs() < 0.01);
assert!((eb.height - 100.0).abs() < 0.01);
let cb = tree.bounds(child);
assert!((cb.width - 200.0).abs() < 0.01);
assert!((cb.height - 100.0).abs() < 0.01);
}
#[test]
fn align_child_top_trailing() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(40.0, 20.0));
let _expand = tree.add(
Expand::new()
.align_child(Alignment::TOP_TRAILING)
.child_id(child),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let cb = tree.bounds(child);
assert!((cb.width - 40.0).abs() < 0.01);
assert!((cb.height - 20.0).abs() < 0.01);
assert!((cb.x - 160.0).abs() < 0.01); assert!((cb.y - 0.0).abs() < 0.01); }
#[test]
fn flex_default_is_one() {
let theme = teksilo_core::presets::intui::light();
let ctx = LayoutContext::for_testing(&theme);
let r = Expand::new().layout_response(SizeProposal::unspecified(), &ctx);
assert_eq!(r.flex, 1.0);
}
#[test]
fn flex_zero_opts_out() {
let theme = teksilo_core::presets::intui::light();
let ctx = LayoutContext::for_testing(&theme);
let r = Expand::new()
.flex(0.0)
.layout_response(SizeProposal::unspecified(), &ctx);
assert_eq!(r.flex, 0.0);
}
}