teksilo_widgets/primitives/
fixed_size.rs1use teksilo_canvas::{Rect, Size, SizeProposal};
30use teksilo_core::signal::Prop;
31use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
32use teksilo_core::widget_id::WidgetId;
33
34#[derive(Debug)]
40pub struct FixedSize {
41 child_id: Option<WidgetId>,
42 pending_child: Option<PendingChild>,
43 width: Option<Prop<f32>>,
44 height: Option<Prop<f32>>,
45}
46
47impl FixedSize {
48 pub fn new() -> Self {
51 Self {
52 child_id: None,
53 pending_child: None,
54 width: None,
55 height: None,
56 }
57 }
58
59 pub fn child_id(mut self, id: WidgetId) -> Self {
61 self.pending_child = Some(PendingChild::Id(id));
62 self
63 }
64
65 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
67 self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
68 self
69 }
70
71 pub fn width(mut self, state: impl Into<Prop<f32>>) -> Self {
73 self.width = Some(state.into());
74 self
75 }
76
77 pub fn height(mut self, state: impl Into<Prop<f32>>) -> Self {
79 self.height = Some(state.into());
80 self
81 }
82}
83
84impl Default for FixedSize {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl Widget for FixedSize {
91 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
92 if let Some(pending) = self.pending_child.take() {
93 self.child_id = Some(match pending {
94 PendingChild::Id(id) => id,
95 PendingChild::Deferred(w) => ctx.add_boxed(w),
96 });
97 }
98 let self_id = ctx.self_id();
100 let registry = ctx.binding_registry();
101 if let Some(ref w) = self.width {
102 w.register_if_bound(
103 self_id,
104 registry,
105 teksilo_core::binding::BindingLevel::Relayout,
106 );
107 }
108 if let Some(ref h) = self.height {
109 h.register_if_bound(
110 self_id,
111 registry,
112 teksilo_core::binding::BindingLevel::Relayout,
113 );
114 }
115 self.child_id.into_iter().collect()
116 }
117
118 fn layout_response(
119 &self,
120 _proposal: SizeProposal,
121 ctx: &LayoutContext,
122 ) -> teksilo_core::widget::LayoutResponse {
123 let bound_width = self.width.as_ref().map(|r| r.get());
129 let bound_height = self.height.as_ref().map(|r| r.get());
130 let child_proposal = SizeProposal {
131 width: bound_width,
132 height: bound_height,
133 };
134 let child_size = self
135 .child_id
136 .and_then(|id| ctx.child_size(id, child_proposal))
137 .unwrap_or(Size::ZERO);
138
139 let w = bound_width.unwrap_or(child_size.width);
140 let h = bound_height.unwrap_or(child_size.height);
141 Size::new(w, h).into()
142 }
143
144 fn place_children(
145 &self,
146 bounds: Rect,
147 _proposal: SizeProposal,
148 children: &mut [WidgetPlacement],
149 _ctx: &LayoutContext,
150 ) {
151 for child in children.iter_mut() {
152 child.origin = bounds.origin();
153 child.size = bounds.size();
154 }
155 }
156
157 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
158
159 fn children(&self) -> Vec<WidgetId> {
160 self.child_id.into_iter().collect()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use teksilo_core::signal::Signal;
168 use teksilo_core::widget_tree::WidgetTree;
169
170 #[derive(Debug)]
171 struct FixedLeaf(f32, f32);
172 impl Widget for FixedLeaf {
173 fn layout_response(
174 &self,
175 _proposal: SizeProposal,
176 _ctx: &LayoutContext,
177 ) -> teksilo_core::widget::LayoutResponse {
178 Size::new(self.0, self.1).into()
179 }
180 }
181
182 #[test]
183 fn reports_child_natural_size() {
184 let mut tree = WidgetTree::new();
185 let child = tree.add(FixedLeaf(40.0, 20.0));
186 let fixed = tree.add(FixedSize::new().child_id(child));
187 tree.layout(SizeProposal::unspecified());
188
189 let fb = tree.bounds(fixed);
190 assert!((fb.width - 40.0).abs() < 0.01);
191 assert!((fb.height - 20.0).abs() < 0.01);
192 }
193
194 #[test]
195 fn ignores_parent_proposal() {
196 let mut tree = WidgetTree::new();
197 let child = tree.add(FixedLeaf(40.0, 20.0));
198 let fixed = tree.add(FixedSize::new().child_id(child));
199 tree.layout(SizeProposal::unspecified());
200
201 let fb = tree.bounds(fixed);
202 assert!((fb.width - 40.0).abs() < 0.01);
203 assert!((fb.height - 20.0).abs() < 0.01);
204 }
205
206 #[test]
207 fn width_constrains_size() {
208 let width = Signal::new(150.0_f32);
209 let mut tree = WidgetTree::new();
210 let child = tree.add(FixedLeaf(40.0, 20.0));
211 let fixed = tree.add(FixedSize::new().width(width.clone()).child_id(child));
212 tree.layout(SizeProposal::unspecified());
213
214 let fb = tree.bounds(fixed);
215 assert!((fb.width - 150.0).abs() < 0.01); assert!((fb.height - 20.0).abs() < 0.01); }
218
219 #[test]
220 fn width_triggers_relayout_on_change() {
221 let width = Signal::new(200.0_f32);
222 let mut tree = WidgetTree::new();
223 let child = tree.add(FixedLeaf(40.0, 20.0));
224 let fixed = tree.add(FixedSize::new().width(width.clone()).child_id(child));
225 tree.layout(SizeProposal::unspecified());
226 assert!((tree.bounds(fixed).width - 200.0).abs() < 0.01);
227
228 width.set(100.0);
229 tree.layout(SizeProposal::unspecified());
230 assert!((tree.bounds(fixed).width - 100.0).abs() < 0.01);
231 }
232}