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