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