1use std::rc::Rc;
33
34use teksilo_canvas::{Rect, Size, SizeProposal};
35use teksilo_core::accessibility::AccessNodeBuilder;
36use teksilo_core::color_prop::ColorProp;
37use teksilo_core::signal::Prop;
38use teksilo_core::styles::{PanelStyleConfig, PanelVariant, SharedPanelStyle};
39use teksilo_core::widget::{LayoutContext, PendingChild, Widget, WidgetPlacement};
40use teksilo_core::widget_id::WidgetId;
41#[cfg(test)]
42use teksilo_tokens::Color;
43
44pub struct Panel {
46 child_id: Option<WidgetId>,
47 pending_child: Option<PendingChild>,
48 background: Option<ColorProp>,
49 border_color: Option<ColorProp>,
50 border_width: Option<Prop<f32>>,
51 corner_radius: Option<Prop<f32>>,
52 padding: Option<Prop<f32>>,
53 variant: PanelVariant,
54 style_override: Option<SharedPanelStyle>,
55 root_child_id: Option<WidgetId>,
56 a11y_presentational: bool,
57}
58
59impl std::fmt::Debug for Panel {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("Panel")
62 .field("variant", &self.variant)
63 .field("a11y_presentational", &self.a11y_presentational)
64 .finish()
65 }
66}
67
68impl Panel {
69 pub fn new() -> Self {
71 Self {
72 child_id: None,
73 pending_child: None,
74 background: None,
75 border_color: None,
76 border_width: None,
77 corner_radius: None,
78 padding: None,
79 variant: PanelVariant::default(),
80 style_override: None,
81 root_child_id: None,
82 a11y_presentational: false,
83 }
84 }
85
86 pub fn variant(mut self, variant: PanelVariant) -> Self {
92 self.variant = variant;
93 self
94 }
95
96 pub fn style(mut self, style: impl teksilo_core::styles::PanelStyle) -> Self {
103 self.style_override = Some(Rc::new(style));
104 self
105 }
106
107 pub fn a11y_presentational(mut self) -> Self {
113 self.a11y_presentational = true;
114 self
115 }
116
117 pub fn child(mut self, widget: impl teksilo_core::IntoTeksiChild) -> Self {
119 self.pending_child = Some(teksilo_core::IntoTeksiChild::into_pending(widget));
120 self
121 }
122 pub fn child_opt(self, widget: Option<impl teksilo_core::IntoTeksiChild>) -> Self {
129 match widget {
130 Some(w) => self.child(w),
131 None => self,
132 }
133 }
134
135 pub fn background(mut self, color: impl Into<ColorProp>) -> Self {
138 self.background = Some(color.into());
139 self
140 }
141
142 pub fn border_color(mut self, color: impl Into<ColorProp>) -> Self {
145 self.border_color = Some(color.into());
146 self
147 }
148
149 pub fn border_width(mut self, width: impl Into<Prop<f32>>) -> Self {
153 self.border_width = Some(width.into());
154 self
155 }
156
157 pub fn corner_radius(mut self, radius: impl Into<Prop<f32>>) -> Self {
160 self.corner_radius = Some(radius.into());
161 self
162 }
163
164 pub fn padding(mut self, padding: impl Into<Prop<f32>>) -> Self {
168 self.padding = Some(padding.into());
169 self
170 }
171}
172
173impl Default for Panel {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179impl Widget for Panel {
180 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
181 if let Some(pending) = self.pending_child.take() {
182 self.child_id = Some(match pending {
183 PendingChild::Id(id) => id,
184 PendingChild::Deferred(w) => ctx.add_boxed(w),
185 });
186 }
187 let content = match self.child_id {
188 Some(id) => id,
189 None => ctx.add(crate::primitives::FixedSize::new().width(0.0).height(0.0)),
192 };
193
194 let style: SharedPanelStyle = self
195 .style_override
196 .clone()
197 .or_else(|| ctx.theme().style_slots.panel.clone())
198 .unwrap_or_else(|| {
199 Rc::new(crate::styles::RecipePanelStyle::for_tokens(
200 &ctx.theme().input,
201 ))
202 });
203 let cfg = PanelStyleConfig {
204 content,
205 variant: self.variant,
206 background_override: self.background.clone(),
207 border_color_override: self.border_color.clone(),
208 border_width_override: self.border_width.clone(),
209 corner_radius_override: self.corner_radius.clone(),
210 padding_override: self.padding.clone(),
211 };
212 let root_id = style.make_body(&cfg, ctx);
213 self.root_child_id = Some(root_id);
214 vec![root_id]
215 }
216
217 fn layout_response(
218 &self,
219 proposal: SizeProposal,
220 ctx: &LayoutContext,
221 ) -> teksilo_core::widget::LayoutResponse {
222 if let Some(root) = self.root_child_id
223 && let Some(size) = ctx.child_size(root, proposal)
224 {
225 return (size).into();
226 }
227 proposal.resolve(0.0, 0.0).into()
228 }
229
230 fn place_children(
231 &self,
232 bounds: Rect,
233 _proposal: SizeProposal,
234 children: &mut [WidgetPlacement],
235 _ctx: &LayoutContext,
236 ) {
237 for child in children.iter_mut() {
238 child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y);
239 child.size = Size::new(bounds.width, bounds.height);
240 }
241 }
242
243 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
244 if self.a11y_presentational {
245 builder.set_hidden();
246 return;
247 }
248 builder.set_role(teksilo_core::accesskit::Role::Group);
249 }
250
251 fn children(&self) -> Vec<WidgetId> {
252 self.root_child_id.into_iter().collect()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use teksilo_core::widget_tree::WidgetTree;
260
261 #[derive(Debug)]
262 struct FixedLeaf(f32, f32);
263 impl Widget for FixedLeaf {
264 fn layout_response(
265 &self,
266 _proposal: SizeProposal,
267 _ctx: &LayoutContext,
268 ) -> teksilo_core::widget::LayoutResponse {
269 Size::new(self.0, self.1).into()
270 }
271 }
272
273 #[test]
274 fn panel_adds_padding_to_child_size() {
275 let theme = teksilo_core::presets::intui::light();
276 let mut tree = WidgetTree::new().with_theme(theme.clone());
277 let child = tree.add(FixedLeaf(80.0, 40.0));
278 let panel = tree.add(Panel::new().padding(10.0).child(child));
279 tree.layout(SizeProposal::unspecified());
280
281 let pb = tree.bounds(panel);
282 assert!((pb.width - 100.0).abs() < 0.01); assert!((pb.height - 60.0).abs() < 0.01); }
285
286 #[test]
287 fn panel_child_positioned_with_padding() {
288 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
289 let child = tree.add(FixedLeaf(80.0, 40.0));
290 let _panel = tree.add(Panel::new().padding(12.0).child(child));
291 tree.layout(SizeProposal::exact(200.0, 100.0));
292
293 let cb = tree.bounds(child);
294 assert!((cb.x - 12.0).abs() < 0.01);
295 assert!((cb.y - 12.0).abs() < 0.01);
296 }
297
298 #[test]
299 fn panel_paints_background() {
300 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
301 let child = tree.add(FixedLeaf(50.0, 30.0));
302 let _panel = tree.add(
303 Panel::new()
304 .background(Color::RED)
305 .corner_radius(8.0)
306 .child(child),
307 );
308 tree.layout(SizeProposal::exact(200.0, 100.0));
309 let frame = tree.render();
310 assert!(
311 !frame.shapes.is_empty(),
312 "panel should render a background shape"
313 );
314 }
315}