teksilo_widgets/styles/
recipe_calendar_style.rs1use teksilo_core::build_context::BuildContext;
25use teksilo_core::color_prop::ColorProp;
26use teksilo_core::signal::Signal;
27use teksilo_core::styles::{
28 CalendarDayConfig, CalendarDayFill, CalendarHeaderConfig, CalendarStyle, CalendarZoomCellConfig,
29};
30use teksilo_core::widget_id::WidgetId;
31use teksilo_i18n::lit;
32use teksilo_tokens::{
33 BorderRole, CornerRadius, InputTokens, SurfaceRole, TargetRole, TextRole, TextStyleRole,
34};
35
36use crate::primitives::{Center, Expand, FixedSize, HStack, RectWidget, TextWidget, ZStack};
37use teksilo_core::styles::density::{dp, spacing};
38
39pub const CALENDAR_OUTER_PADDING: f32 = 8.0;
46pub const CALENDAR_SECTION_GAP: f32 = 4.0;
48pub const CALENDAR_HEADER_HEIGHT: f32 = 28.0;
50pub const CALENDAR_WEEKDAY_ROW_HEIGHT: f32 = 20.0;
52pub const CALENDAR_CELL_SIZE: f32 = 32.0;
54pub const CALENDAR_CELL_RADIUS: f32 = 4.0;
56pub const CALENDAR_CELL_GAP: f32 = 0.0;
58pub const CALENDAR_TODAY_RING_WIDTH: f32 = 1.0;
60pub const CALENDAR_NAV_ICON_SIZE: f32 = 12.0;
62pub const CALENDAR_WEEK_NUMBER_COLUMN_WIDTH: f32 = 28.0;
64pub const CALENDAR_NAV_ARROW_SIZE: f32 = 24.0;
66pub const CALENDAR_NAV_ARROW_RADIUS: f32 = 4.0;
68pub const CALENDAR_HEADER_GAP: f32 = 4.0;
70pub const CALENDAR_ZOOM_CELL_RADIUS: f32 = 6.0;
72
73#[derive(Debug, Clone, Copy, PartialEq)]
75pub struct CalendarRecipe {
76 pub outer_padding: f32,
77 pub section_gap: f32,
78 pub header_height: f32,
79 pub weekday_row_height: f32,
80 pub cell_size: f32,
81 pub cell_radius: f32,
82 pub cell_gap: f32,
83 pub today_ring_width: f32,
84 pub nav_icon_size: f32,
85 pub week_number_column_width: f32,
86 pub nav_arrow_size: f32,
87 pub nav_arrow_radius: f32,
88 pub header_gap: f32,
89 pub zoom_cell_radius: f32,
90}
91
92impl CalendarRecipe {
93 pub fn for_tokens(tokens: &InputTokens) -> Self {
99 Self {
100 outer_padding: spacing(CALENDAR_OUTER_PADDING, tokens),
101 section_gap: spacing(CALENDAR_SECTION_GAP, tokens),
102 header_height: dp(CALENDAR_HEADER_HEIGHT, TargetRole::Target, tokens),
103 weekday_row_height: CALENDAR_WEEKDAY_ROW_HEIGHT,
104 cell_size: dp(CALENDAR_CELL_SIZE, TargetRole::Target, tokens),
105 cell_radius: CALENDAR_CELL_RADIUS,
106 cell_gap: spacing(CALENDAR_CELL_GAP, tokens),
107 today_ring_width: CALENDAR_TODAY_RING_WIDTH,
108 nav_icon_size: CALENDAR_NAV_ICON_SIZE,
109 week_number_column_width: CALENDAR_WEEK_NUMBER_COLUMN_WIDTH,
110 nav_arrow_size: dp(CALENDAR_NAV_ARROW_SIZE, TargetRole::Target, tokens),
111 nav_arrow_radius: CALENDAR_NAV_ARROW_RADIUS,
112 header_gap: spacing(CALENDAR_HEADER_GAP, tokens),
113 zoom_cell_radius: CALENDAR_ZOOM_CELL_RADIUS,
114 }
115 }
116}
117
118impl Default for CalendarRecipe {
119 fn default() -> Self {
120 Self::for_tokens(&InputTokens::default())
121 }
122}
123
124#[derive(Debug, Default, Clone, Copy)]
126pub struct RecipeCalendarStyle {
127 pub recipe: CalendarRecipe,
128}
129
130impl RecipeCalendarStyle {
131 pub fn new(recipe: CalendarRecipe) -> Self {
132 Self { recipe }
133 }
134
135 pub fn for_tokens(tokens: &InputTokens) -> Self {
142 Self {
143 recipe: CalendarRecipe::for_tokens(tokens),
144 }
145 }
146}
147
148impl CalendarStyle for RecipeCalendarStyle {
149 fn make_day_cell(&self, cfg: &CalendarDayConfig, ctx: &mut BuildContext) -> WidgetId {
150 let radius = self.recipe.cell_radius;
151 let today_ring_width = self.recipe.today_ring_width;
152
153 let bg_role: Signal<SurfaceRole> = cfg.fill.map(|f| match f {
156 CalendarDayFill::Selected => SurfaceRole::Selected,
157 CalendarDayFill::InRange => SurfaceRole::SelectedInactive,
158 CalendarDayFill::None => SurfaceRole::Transparent,
159 });
160 let bg_widget = RectWidget::new()
161 .background(bg_role)
162 .corner_radius(CornerRadius::uniform(radius));
163 let bg_id = ctx.add(bg_widget);
164
165 let ring_id = if cfg.is_today && !cfg.is_out_of_month {
167 let ring = RectWidget::new()
168 .background(SurfaceRole::Transparent)
169 .border_color(BorderRole::Focused)
170 .border_width(today_ring_width)
171 .corner_radius(CornerRadius::uniform(radius));
172 Some(ctx.add(ring))
173 } else {
174 None
175 };
176
177 let focus_ring_width = ctx.theme_signal().get().shape.focus_ring_width;
181 let focus_ring = RectWidget::new()
182 .background(SurfaceRole::Transparent)
183 .border_color(BorderRole::Focused)
184 .border_width(focus_ring_width)
185 .corner_radius(CornerRadius::uniform(radius));
186 let focus_ring_id = ctx.add(focus_ring);
187 ctx.visible_when(focus_ring_id, cfg.is_focused_cell.clone());
188
189 let text_color: ColorProp = if cfg.is_disabled || cfg.is_out_of_month {
193 TextRole::Disabled.into()
194 } else {
195 let role_signal: Signal<TextRole> = cfg.fill.map(|f| match f {
196 CalendarDayFill::Selected => TextRole::OnAccent,
197 _ => TextRole::Primary,
198 });
199 ColorProp::DynamicTextRole(role_signal)
200 };
201 let label = TextWidget::new(lit!(cfg.label.clone()))
202 .style(TextStyleRole::Body)
203 .color(text_color)
204 .single_line()
205 .a11y_hidden();
206 let label_id = ctx.add(label);
207 let centered = ctx.add(Center::new().child(label_id));
208
209 let mut z = ZStack::new().child(bg_id);
211 if let Some(ring) = ring_id {
212 z = z.child(ring);
213 }
214 z = z.child(focus_ring_id).child(centered);
215 let z_id = ctx.add(z);
216
217 ctx.add(
218 FixedSize::new()
219 .width(cfg.cell_size)
220 .height(cfg.cell_size)
221 .child(z_id),
222 )
223 }
224
225 fn make_zoom_cell(&self, cfg: &CalendarZoomCellConfig, ctx: &mut BuildContext) -> WidgetId {
226 let bg_role = cfg
229 .is_selected
230 .clone()
231 .zip3(&cfg.is_hovered, &cfg.is_pressed)
232 .map(|(sel, hov, prs)| {
233 if *sel {
234 SurfaceRole::Accent
235 } else if *prs {
236 SurfaceRole::Pressed
237 } else if *hov {
238 SurfaceRole::Hover
239 } else {
240 SurfaceRole::Transparent
241 }
242 });
243 let text_role = cfg.is_selected.map(|sel| {
244 if *sel {
245 TextRole::OnAccent
246 } else {
247 TextRole::Primary
248 }
249 });
250
251 let bg = RectWidget::new()
252 .background(ColorProp::DynamicSurfaceRole(bg_role))
253 .corner_radius(CornerRadius::uniform(self.recipe.zoom_cell_radius));
254 let bg_id = ctx.add(bg);
255
256 let text = TextWidget::new(lit!(cfg.label.clone()))
257 .style(TextStyleRole::Body)
258 .color(ColorProp::DynamicTextRole(text_role))
259 .single_line()
260 .a11y_hidden();
261 let text_id = ctx.add(Center::new().child(text));
262
263 let z_id = ctx.add(ZStack::new().child(bg_id).child(text_id));
264 ctx.add(
265 FixedSize::new()
266 .width(cfg.cell_width)
267 .height(cfg.cell_height)
268 .child(z_id),
269 )
270 }
271
272 fn make_header(&self, cfg: &CalendarHeaderConfig, ctx: &mut BuildContext) -> WidgetId {
273 let title_filled = ctx.add(Expand::horizontal().child(cfg.title));
277
278 let mut row = HStack::new().spacing(self.recipe.header_gap);
279 if let Some(id) = cfg.prev_double {
280 row = row.child(id);
281 }
282 if let Some(id) = cfg.prev {
283 row = row.child(id);
284 }
285 row = row.child(title_filled);
286 if let Some(id) = cfg.next {
287 row = row.child(id);
288 }
289 if let Some(id) = cfg.next_double {
290 row = row.child(id);
291 }
292 ctx.add(row)
293 }
294
295 fn nav_arrow_size(&self, _tokens: &InputTokens) -> f32 {
300 self.recipe.nav_arrow_size
301 }
302}