use teksilo_canvas::{Rect, Size, SizeProposal};
use teksilo_core::signal::Prop;
use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
#[derive(Debug)]
pub struct MinSize {
child_id: Option<WidgetId>,
pending_child: Option<PendingChild>,
min_width: Option<Prop<f32>>,
min_height: Option<Prop<f32>>,
}
impl MinSize {
pub fn new(width: f32, height: f32) -> Self {
Self {
child_id: None,
pending_child: None,
min_width: Some(Prop::Static(width)),
min_height: Some(Prop::Static(height)),
}
}
pub fn width(width: f32) -> Self {
Self {
child_id: None,
pending_child: None,
min_width: Some(Prop::Static(width)),
min_height: None,
}
}
pub fn height(height: f32) -> Self {
Self {
child_id: None,
pending_child: None,
min_width: None,
min_height: Some(Prop::Static(height)),
}
}
pub fn min_width(mut self, state: impl Into<Prop<f32>>) -> Self {
self.min_width = Some(state.into());
self
}
pub fn min_height(mut self, state: impl Into<Prop<f32>>) -> Self {
self.min_height = Some(state.into());
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 Widget for MinSize {
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),
});
}
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
if let Some(ref w) = self.min_width {
w.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::Relayout,
);
}
if let Some(ref h) = self.min_height {
h.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::Relayout,
);
}
self.child_id.into_iter().collect()
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let min_w = self.min_width.as_ref().map(|r| r.get());
let min_h = self.min_height.as_ref().map(|r| r.get());
let clamped_proposal = SizeProposal {
width: match (proposal.width, min_w) {
(Some(w), Some(min)) => Some(w.max(min)),
(None, Some(min)) => Some(min),
(w, None) => w,
},
height: match (proposal.height, min_h) {
(Some(h), Some(min)) => Some(h.max(min)),
(None, Some(min)) => Some(min),
(h, None) => h,
},
};
let child_response = self
.child_id
.and_then(|id| ctx.child_layout_response(id, clamped_proposal));
let child_size = child_response
.as_ref()
.map(|r| r.size)
.unwrap_or(Size::ZERO);
let child_flex = child_response.map(|r| r.flex).unwrap_or(0.0);
let child_shrink = child_response.map(|r| r.shrink).unwrap_or(0.0);
let child_min = child_response.map(|r| r.min).unwrap_or(Size::ZERO);
let w = match min_w {
Some(min) => child_size.width.max(min),
None => child_size.width,
};
let h = match min_h {
Some(min) => child_size.height.max(min),
None => child_size.height,
};
let floor_w = child_min.width.max(min_w.unwrap_or(0.0)).min(w);
let floor_h = child_min.height.max(min_h.unwrap_or(0.0)).min(h);
teksilo_core::widget::LayoutResponse::flexible(Size::new(w, h), child_flex)
.with_shrink(child_shrink)
.with_min(Size::new(floor_w, floor_h))
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
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::signal::Signal;
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 clamps_small_child_to_minimum() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(20.0, 10.0));
let min = tree.add(MinSize::new(48.0, 48.0).child_id(child));
tree.layout(SizeProposal::unspecified());
let mb = tree.bounds(min);
assert!((mb.width - 48.0).abs() < 0.01);
assert!((mb.height - 48.0).abs() < 0.01);
}
#[test]
fn large_child_is_not_clamped() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(100.0, 80.0));
let min = tree.add(MinSize::new(48.0, 48.0).child_id(child));
tree.layout(SizeProposal::unspecified());
let mb = tree.bounds(min);
assert!((mb.width - 100.0).abs() < 0.01);
assert!((mb.height - 80.0).abs() < 0.01);
}
#[test]
fn min_width_only() {
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(20.0, 10.0));
let min = tree.add(MinSize::width(48.0).child_id(child));
tree.layout(SizeProposal::unspecified());
let mb = tree.bounds(min);
assert!((mb.width - 48.0).abs() < 0.01);
assert!((mb.height - 10.0).abs() < 0.01);
}
#[test]
fn min_width_dynamic() {
let min_w = Signal::new(48.0_f32);
let mut tree = WidgetTree::new();
let child = tree.add(FixedLeaf(20.0, 10.0));
let min = tree.add(MinSize::width(0.0).min_width(min_w.clone()).child_id(child));
tree.layout(SizeProposal::unspecified());
assert!((tree.bounds(min).width - 48.0).abs() < 0.01);
min_w.set(80.0);
tree.layout(SizeProposal::unspecified());
assert!((tree.bounds(min).width - 80.0).abs() < 0.01);
}
#[derive(Debug)]
struct WrappingLeaf;
impl Widget for WrappingLeaf {
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let content_width = 120.0_f32;
let line_height = 20.0_f32;
let w = proposal.width.unwrap_or(content_width).min(content_width);
let lines = (content_width / w.max(1.0)).ceil();
Size::new(w, lines * line_height).into()
}
}
#[test]
fn child_receives_clamped_proposal() {
use crate::primitives::vstack::VStack;
let mut tree = WidgetTree::new();
let child = tree.add(WrappingLeaf);
let min = tree.add(MinSize::width(100.0).child_id(child));
let _stack = tree.add(VStack::new().add_child(min));
tree.layout(SizeProposal {
width: None,
height: None,
});
let mb = tree.bounds(min);
assert!(
(mb.width - 100.0).abs() < 0.01,
"width should be 100, got {}",
mb.width
);
assert!(
(mb.height - 40.0).abs() < 0.01,
"height should be 40 (2 lines at 100px), got {}",
mb.height
);
}
#[test]
fn unspecified_proposal_gets_clamped_to_minimum() {
let mut tree = WidgetTree::new();
let child = tree.add(WrappingLeaf);
let min = tree.add(MinSize::width(80.0).child_id(child));
tree.layout(SizeProposal::unspecified());
let mb = tree.bounds(min);
assert!(
(mb.width - 80.0).abs() < 0.01,
"width should be 80, got {}",
mb.width
);
assert!(
(mb.height - 40.0).abs() < 0.01,
"height should be 40 (2 lines at 80px), got {}",
mb.height
);
}
}