teksilo_widgets/styles/
recipe_dialog_style.rs1use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
19use teksilo_core::accessibility::AccessNodeBuilder;
20use teksilo_core::build_context::BuildContext;
21use teksilo_core::styles::{DialogStyle, DialogStyleConfig};
22use teksilo_core::widget::{
23 LayoutContext, LayoutResponse, PaintContext, PendingChild, Widget, WidgetPlacement,
24};
25use teksilo_core::widget_id::WidgetId;
26use teksilo_tokens::{CornerRadius, InputTokens, SurfaceRole, TargetRole};
27
28use crate::primitives::RectWidget;
29use teksilo_core::styles::density::{dp, spacing};
30
31pub const DIALOG_CONTENT_PADDING: f32 = 24.0;
33pub const DIALOG_MIN_WIDTH: f32 = 280.0;
34pub const DIALOG_CORNER_RADIUS: f32 = 8.0;
35
36#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct DialogRecipe {
40 pub content_padding: f32,
41 pub min_width: f32,
42 pub corner_radius: f32,
43}
44
45impl DialogRecipe {
46 pub fn for_tokens(tokens: &InputTokens) -> Self {
52 Self {
53 content_padding: spacing(DIALOG_CONTENT_PADDING, tokens),
54 min_width: dp(DIALOG_MIN_WIDTH, TargetRole::Target, tokens),
55 corner_radius: DIALOG_CORNER_RADIUS,
56 }
57 }
58}
59
60impl Default for DialogRecipe {
61 fn default() -> Self {
62 Self::for_tokens(&InputTokens::default())
63 }
64}
65
66#[derive(Debug, Default, Clone, Copy)]
70pub struct RecipeDialogStyle {
71 pub recipe: DialogRecipe,
72}
73
74impl RecipeDialogStyle {
75 pub fn new(recipe: DialogRecipe) -> Self {
76 Self { recipe }
77 }
78
79 pub fn for_tokens(tokens: &InputTokens) -> Self {
86 Self {
87 recipe: DialogRecipe::for_tokens(tokens),
88 }
89 }
90}
91
92impl DialogStyle for RecipeDialogStyle {
93 fn make_panel(&self, cfg: &DialogStyleConfig, ctx: &mut BuildContext) -> WidgetId {
94 ctx.add(DialogPanel {
95 child_id: None,
96 pending_child: Some(PendingChild::Id(cfg.content)),
97 padding: cfg.padding_override.unwrap_or(self.recipe.content_padding),
98 min_width: cfg.min_width_override.unwrap_or(self.recipe.min_width),
99 recipe: self.recipe,
100 })
101 }
102
103 fn make_scrim(&self, ctx: &mut BuildContext) -> WidgetId {
104 ctx.add(RectWidget::new().background(SurfaceRole::Scrim))
105 }
106}
107
108struct DialogPanel {
114 child_id: Option<WidgetId>,
115 pending_child: Option<PendingChild>,
116 padding: f32,
117 min_width: f32,
118 recipe: DialogRecipe,
119}
120
121impl std::fmt::Debug for DialogPanel {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("DialogPanel")
124 .field("padding", &self.padding)
125 .field("min_width", &self.min_width)
126 .finish()
127 }
128}
129
130impl Widget for DialogPanel {
131 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
132 if let Some(pending) = self.pending_child.take() {
133 self.child_id = Some(match pending {
134 PendingChild::Id(id) => id,
135 PendingChild::Deferred(w) => ctx.add_boxed(w),
136 });
137 }
138 self.child_id.into_iter().collect()
139 }
140
141 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
142 let inset = self.padding * 2.0;
143 let content = self
144 .child_id
145 .and_then(|id| {
146 ctx.child_size(
147 id,
148 SizeProposal {
149 width: proposal.width.map(|width| (width - inset).max(0.0)),
150 height: proposal.height.map(|height| (height - inset).max(0.0)),
151 },
152 )
153 })
154 .unwrap_or_else(|| proposal.resolve(240.0, 120.0));
155
156 Size::new(
157 (content.width + inset).max(self.min_width),
158 content.height + inset,
159 )
160 .into()
161 }
162
163 fn place_children(
164 &self,
165 bounds: Rect,
166 _proposal: SizeProposal,
167 children: &mut [WidgetPlacement],
168 _ctx: &LayoutContext,
169 ) {
170 let pad = self.padding;
171 for child in children.iter_mut() {
172 child.origin = teksilo_canvas::Point::new(bounds.x + pad, bounds.y + pad);
173 child.size = Size::new(
174 (bounds.width - pad * 2.0).max(0.0),
175 (bounds.height - pad * 2.0).max(0.0),
176 );
177 }
178 }
179
180 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
181 let radius = CornerRadius::uniform(self.recipe.corner_radius);
182 canvas.fill_rounded_rect(bounds, radius, ctx.theme.colors.surface_main);
183 canvas.stroke_rounded_rect(
184 bounds,
185 radius,
186 ctx.theme.colors.border_strong,
187 ctx.theme.shape.border_width,
188 );
189 }
190
191 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
192 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
201 }
202
203 fn children(&self) -> Vec<WidgetId> {
204 self.child_id.into_iter().collect()
205 }
206}