teksilo_widgets/primitives/
spacer.rs1use teksilo_canvas::{Rect, Size, SizeProposal};
25use teksilo_core::widget::{LayoutContext, PaintContext, Widget};
26
27#[derive(Debug)]
29pub struct Spacer {
30 min_length: f32,
31}
32
33impl Spacer {
34 pub fn new() -> Self {
37 Self { min_length: 0.0 }
38 }
39
40 pub fn min_length(mut self, min: f32) -> Self {
46 self.min_length = min;
47 self
48 }
49}
50
51impl Default for Spacer {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl Widget for Spacer {
58 fn layout_response(
59 &self,
60 _proposal: SizeProposal,
61 ctx: &LayoutContext,
62 ) -> teksilo_core::widget::LayoutResponse {
63 use teksilo_core::widget::StackAxis;
71 let size = match ctx.stack_main_axis() {
72 Some(StackAxis::Horizontal) => Size::new(self.min_length, 0.0),
73 Some(StackAxis::Vertical) => Size::new(0.0, self.min_length),
74 None => Size::new(self.min_length, self.min_length),
75 };
76 teksilo_core::widget::LayoutResponse::flexible(size, 1.0)
77 }
78
79 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {
80 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use teksilo_core::widget_tree::WidgetTree;
88
89 use crate::primitives::hstack::HStack;
90 use crate::primitives::vstack::VStack;
91
92 #[derive(Debug)]
93 struct FixedLeaf(f32, f32);
94 impl Widget for FixedLeaf {
95 fn layout_response(
96 &self,
97 _proposal: SizeProposal,
98 _ctx: &LayoutContext,
99 ) -> teksilo_core::widget::LayoutResponse {
100 Size::new(self.0, self.1).into()
101 }
102 }
103
104 #[test]
105 fn spacer_pushes_to_trailing_in_hstack() {
106 let mut tree = WidgetTree::new();
107 let spacer = tree.add(Spacer::new());
108 let btn = tree.add(FixedLeaf(60.0, 30.0));
109 let _stack = tree.add(HStack::new().add_child(spacer).add_child(btn));
110 tree.layout(SizeProposal::exact(300.0, 50.0));
111
112 assert!((tree.bounds(btn).x - 240.0).abs() < 0.01);
114 }
115
116 #[test]
117 fn two_spacers_center_child_in_hstack() {
118 let mut tree = WidgetTree::new();
119 let s1 = tree.add(Spacer::new());
120 let label = tree.add(FixedLeaf(60.0, 30.0));
121 let s2 = tree.add(Spacer::new());
122 let _stack = tree.add(HStack::new().add_child(s1).add_child(label).add_child(s2));
123 tree.layout(SizeProposal::exact(300.0, 50.0));
124
125 assert!((tree.bounds(label).x - 120.0).abs() < 0.01);
128 }
129
130 #[test]
131 fn spacer_pushes_to_bottom_in_vstack() {
132 let mut tree = WidgetTree::new();
133 let spacer = tree.add(Spacer::new());
134 let btn = tree.add(FixedLeaf(60.0, 30.0));
135 let _stack = tree.add(VStack::new().add_child(spacer).add_child(btn));
136 tree.layout(SizeProposal::exact(200.0, 300.0));
137
138 assert!((tree.bounds(btn).y - 270.0).abs() < 0.01);
140 }
141
142 #[test]
143 fn spacer_with_min_length() {
144 let mut tree = WidgetTree::new();
145 let btn1 = tree.add(FixedLeaf(60.0, 30.0));
146 let spacer = tree.add(Spacer::new().min_length(20.0));
147 let btn2 = tree.add(FixedLeaf(60.0, 30.0));
148 let _stack = tree.add(
149 HStack::new()
150 .add_child(btn1)
151 .add_child(spacer)
152 .add_child(btn2),
153 );
154 tree.layout(SizeProposal::exact(300.0, 50.0));
155
156 assert!((tree.bounds(btn2).x - 240.0).abs() < 0.01);
158 }
159
160 #[test]
161 fn min_length_does_not_inflate_cross_axis() {
162 let mut tree = WidgetTree::new();
166 let btn = tree.add(FixedLeaf(60.0, 30.0));
167 let spacer = tree.add(Spacer::new().min_length(80.0));
168 let stack = tree.add(HStack::new().add_child(btn).add_child(spacer));
169 tree.layout(SizeProposal {
171 width: Some(400.0),
172 height: None,
173 });
174 assert!(
175 (tree.bounds(stack).height - 30.0).abs() < 0.01,
176 "HStack height should follow content (30), not the spacer min_length (80); got {}",
177 tree.bounds(stack).height
178 );
179 }
180
181 #[test]
182 fn min_length_is_honoured_on_the_main_axis() {
183 let mut tree = WidgetTree::new();
186 let btn1 = tree.add(FixedLeaf(60.0, 30.0));
187 let spacer = tree.add(Spacer::new().min_length(40.0));
188 let btn2 = tree.add(FixedLeaf(60.0, 30.0));
189 let stack = tree.add(
190 HStack::new()
191 .add_child(btn1)
192 .add_child(spacer)
193 .add_child(btn2),
194 );
195 tree.layout(SizeProposal::exact(160.0, 50.0));
197 assert!((tree.bounds(btn2).x - 100.0).abs() < 0.01);
198 let _ = stack;
199 }
200
201 #[test]
202 fn flex_factor_is_one() {
203 let theme = teksilo_core::presets::intui::light();
204 let ctx = LayoutContext::for_testing(&theme);
205 let r = Spacer::new().layout_response(SizeProposal::unspecified(), &ctx);
206 assert_eq!(r.flex, 1.0);
207 }
208}