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