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_id(mut self, id: WidgetId) -> Self {
119 self.pending_child = Some(PendingChild::Id(id));
120 self
121 }
122
123 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
125 self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
126 self
127 }
128
129 pub fn background(mut self, color: impl Into<ColorProp>) -> Self {
132 self.background = Some(color.into());
133 self
134 }
135
136 pub fn border_color(mut self, color: impl Into<ColorProp>) -> Self {
139 self.border_color = Some(color.into());
140 self
141 }
142
143 pub fn border_width(mut self, width: impl Into<Prop<f32>>) -> Self {
146 self.border_width = Some(width.into());
147 self
148 }
149
150 pub fn corner_radius(mut self, radius: impl Into<Prop<f32>>) -> Self {
153 self.corner_radius = Some(radius.into());
154 self
155 }
156
157 pub fn padding(mut self, padding: impl Into<Prop<f32>>) -> Self {
160 self.padding = Some(padding.into());
161 self
162 }
163}
164
165impl Default for Panel {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl Widget for Panel {
172 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
173 if let Some(pending) = self.pending_child.take() {
174 self.child_id = Some(match pending {
175 PendingChild::Id(id) => id,
176 PendingChild::Deferred(w) => ctx.add_boxed(w),
177 });
178 }
179 let content = match self.child_id {
180 Some(id) => id,
181 None => ctx.add(crate::primitives::FixedSize::new().width(0.0).height(0.0)),
184 };
185
186 let style: SharedPanelStyle = self
187 .style_override
188 .clone()
189 .or_else(|| ctx.theme().style_slots.panel.clone())
190 .unwrap_or_else(|| Rc::new(crate::styles::RecipePanelStyle::default()));
191 let cfg = PanelStyleConfig {
192 content,
193 variant: self.variant,
194 background_override: self.background.clone(),
195 border_color_override: self.border_color.clone(),
196 border_width_override: self.border_width.clone(),
197 corner_radius_override: self.corner_radius.clone(),
198 padding_override: self.padding.clone(),
199 };
200 let root_id = style.make_body(&cfg, ctx);
201 self.root_child_id = Some(root_id);
202 vec![root_id]
203 }
204
205 fn layout_response(
206 &self,
207 proposal: SizeProposal,
208 ctx: &LayoutContext,
209 ) -> teksilo_core::widget::LayoutResponse {
210 if let Some(root) = self.root_child_id
211 && let Some(size) = ctx.child_size(root, proposal)
212 {
213 return (size).into();
214 }
215 proposal.resolve(0.0, 0.0).into()
216 }
217
218 fn place_children(
219 &self,
220 bounds: Rect,
221 _proposal: SizeProposal,
222 children: &mut [WidgetPlacement],
223 _ctx: &LayoutContext,
224 ) {
225 for child in children.iter_mut() {
226 child.origin = teksilo_canvas::Point::new(bounds.x, bounds.y);
227 child.size = Size::new(bounds.width, bounds.height);
228 }
229 }
230
231 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
232 if self.a11y_presentational {
233 builder.set_hidden();
234 return;
235 }
236 builder.set_role(teksilo_core::accesskit::Role::Group);
237 }
238
239 fn children(&self) -> Vec<WidgetId> {
240 self.root_child_id.into_iter().collect()
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use teksilo_core::widget_tree::WidgetTree;
248
249 #[derive(Debug)]
250 struct FixedLeaf(f32, f32);
251 impl Widget for FixedLeaf {
252 fn layout_response(
253 &self,
254 _proposal: SizeProposal,
255 _ctx: &LayoutContext,
256 ) -> teksilo_core::widget::LayoutResponse {
257 Size::new(self.0, self.1).into()
258 }
259 }
260
261 #[test]
262 fn panel_adds_padding_to_child_size() {
263 let theme = teksilo_core::presets::intui::light();
264 let mut tree = WidgetTree::new().with_theme(theme.clone());
265 let child = tree.add(FixedLeaf(80.0, 40.0));
266 let panel = tree.add(Panel::new().padding(10.0).child_id(child));
267 tree.layout(SizeProposal::unspecified());
268
269 let pb = tree.bounds(panel);
270 assert!((pb.width - 100.0).abs() < 0.01); assert!((pb.height - 60.0).abs() < 0.01); }
273
274 #[test]
275 fn panel_child_positioned_with_padding() {
276 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
277 let child = tree.add(FixedLeaf(80.0, 40.0));
278 let _panel = tree.add(Panel::new().padding(12.0).child_id(child));
279 tree.layout(SizeProposal::exact(200.0, 100.0));
280
281 let cb = tree.bounds(child);
282 assert!((cb.x - 12.0).abs() < 0.01);
283 assert!((cb.y - 12.0).abs() < 0.01);
284 }
285
286 #[test]
287 fn panel_paints_background() {
288 let mut tree = WidgetTree::new().with_theme(teksilo_core::presets::intui::light());
289 let child = tree.add(FixedLeaf(50.0, 30.0));
290 let _panel = tree.add(
291 Panel::new()
292 .background(Color::RED)
293 .corner_radius(8.0)
294 .child_id(child),
295 );
296 tree.layout(SizeProposal::exact(200.0, 100.0));
297 let frame = tree.render();
298 assert!(
299 !frame.shapes.is_empty(),
300 "panel should render a background shape"
301 );
302 }
303}