1use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
22use teksilo_core::accessibility::AccessNodeBuilder;
23use teksilo_core::binding::BindingLevel;
24use teksilo_core::build_context::BuildContext;
25use teksilo_core::color_prop::ColorProp;
26use teksilo_core::signal::Prop;
27use teksilo_core::styles::{PanelStyle, PanelStyleConfig, PanelVariant};
28use teksilo_core::widget::{
29 LayoutContext, LayoutResponse, PaintContext, PendingChild, Widget, WidgetPlacement,
30};
31use teksilo_core::widget_id::WidgetId;
32use teksilo_tokens::CornerRadius;
33
34pub const PANEL_PADDING: f32 = 12.0;
36pub const PANEL_CORNER_RADIUS: f32 = 8.0;
37pub const PANEL_BORDER_WIDTH: f32 = 1.0;
38
39#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct PanelRecipe {
42 pub padding: f32,
43 pub corner_radius: f32,
44 pub border_width: f32,
45}
46
47impl Default for PanelRecipe {
48 fn default() -> Self {
49 Self {
50 padding: PANEL_PADDING,
51 corner_radius: PANEL_CORNER_RADIUS,
52 border_width: PANEL_BORDER_WIDTH,
53 }
54 }
55}
56
57#[derive(Debug, Default, Clone, Copy)]
62pub struct RecipePanelStyle {
63 pub recipe: PanelRecipe,
64}
65
66impl RecipePanelStyle {
67 pub fn new(recipe: PanelRecipe) -> Self {
68 Self { recipe }
69 }
70}
71
72impl PanelStyle for RecipePanelStyle {
73 fn make_body(&self, cfg: &PanelStyleConfig, ctx: &mut BuildContext) -> WidgetId {
74 let frame = PanelFrame {
75 child_id: None,
76 pending_child: Some(PendingChild::Id(cfg.content)),
77 variant: cfg.variant,
78 background: cfg.background_override.clone(),
79 border_color: cfg.border_color_override.clone(),
80 border_width: cfg
81 .border_width_override
82 .clone()
83 .unwrap_or(Prop::Static(self.recipe.border_width)),
84 corner_radius: cfg
85 .corner_radius_override
86 .clone()
87 .unwrap_or(Prop::Static(self.recipe.corner_radius)),
88 padding: cfg
89 .padding_override
90 .clone()
91 .unwrap_or(Prop::Static(self.recipe.padding)),
92 };
93 ctx.add(frame)
94 }
95}
96
97struct PanelFrame {
103 child_id: Option<WidgetId>,
104 pending_child: Option<PendingChild>,
105 variant: PanelVariant,
106 background: Option<ColorProp>,
107 border_color: Option<ColorProp>,
108 border_width: Prop<f32>,
109 corner_radius: Prop<f32>,
110 padding: Prop<f32>,
111}
112
113impl std::fmt::Debug for PanelFrame {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("PanelFrame")
116 .field("variant", &self.variant)
117 .finish()
118 }
119}
120
121impl Widget for PanelFrame {
122 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
123 if let Some(pending) = self.pending_child.take() {
124 self.child_id = Some(match pending {
125 PendingChild::Id(id) => id,
126 PendingChild::Deferred(w) => ctx.add_boxed(w),
127 });
128 }
129 let id = ctx.self_id();
130 let registry = ctx.binding_registry();
131 if let Some(p) = &self.background {
132 p.register_if_bound(id, registry, BindingLevel::RepaintOnly);
133 }
134 if let Some(p) = &self.border_color {
135 p.register_if_bound(id, registry, BindingLevel::RepaintOnly);
136 }
137 self.border_width
138 .register_if_bound(id, registry, BindingLevel::RepaintOnly);
139 self.corner_radius
140 .register_if_bound(id, registry, BindingLevel::RepaintOnly);
141 self.padding
142 .register_if_bound(id, registry, BindingLevel::Relayout);
143 self.child_id.into_iter().collect()
144 }
145
146 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
147 let pad = self.padding.get();
148 let inset = pad * 2.0;
149 if let Some(child_id) = self.child_id {
150 let inner_proposal = SizeProposal {
151 width: proposal.width.map(|w| (w - inset).max(0.0)),
152 height: proposal.height.map(|h| (h - inset).max(0.0)),
153 };
154 if let Some(child_size) = ctx.child_size(child_id, inner_proposal) {
155 return (Size::new(child_size.width + inset, child_size.height + inset)).into();
156 }
157 }
158 proposal.resolve(inset, inset).into()
159 }
160
161 fn place_children(
162 &self,
163 bounds: Rect,
164 _proposal: SizeProposal,
165 children: &mut [WidgetPlacement],
166 _ctx: &LayoutContext,
167 ) {
168 let pad = self.padding.get();
169 for child in children.iter_mut() {
170 child.origin = teksilo_canvas::Point::new(bounds.x + pad, bounds.y + pad);
171 child.size = Size::new(
172 (bounds.width - pad * 2.0).max(0.0),
173 (bounds.height - pad * 2.0).max(0.0),
174 );
175 }
176 }
177
178 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
179 let colors = &ctx.theme.colors;
180
181 let bg = if let Some(p) = &self.background {
182 p.resolve(ctx.theme, ctx.effective_enabled)
183 } else {
184 match self.variant {
185 PanelVariant::Plain => colors.surface_main,
186 PanelVariant::Sunken => colors.surface_sunken,
187 PanelVariant::Raised => colors.surface_raised,
188 PanelVariant::Highlighted => colors.accent_subtle_bg,
189 }
190 };
191
192 let radius = self.corner_radius.get();
193 let border_w = self.border_width.get();
194 canvas.fill_rounded_rect(bounds, CornerRadius::uniform(radius), bg);
195
196 if border_w > 0.0 {
197 let border = if let Some(p) = &self.border_color {
198 p.resolve(ctx.theme, ctx.effective_enabled)
199 } else {
200 match self.variant {
201 PanelVariant::Plain => colors.border,
202 PanelVariant::Sunken | PanelVariant::Raised => colors.border,
203 PanelVariant::Highlighted => colors.accent,
204 }
205 };
206 canvas.stroke_rounded_rect(bounds, CornerRadius::uniform(radius), border, border_w);
207 }
208 }
209
210 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
211 builder.set_hidden();
213 }
214
215 fn children(&self) -> Vec<WidgetId> {
216 self.child_id.into_iter().collect()
217 }
218}