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::{BorderRole, CornerRadius, SurfaceRole, TextRole, TextStyleRole};
33
34use crate::primitives::{Center, Expand, FixedSize, HStack, RectWidget, TextWidget, ZStack};
35
36pub const CALENDAR_OUTER_PADDING: f32 = 8.0;
43pub const CALENDAR_SECTION_GAP: f32 = 4.0;
45pub const CALENDAR_HEADER_HEIGHT: f32 = 28.0;
47pub const CALENDAR_WEEKDAY_ROW_HEIGHT: f32 = 20.0;
49pub const CALENDAR_CELL_SIZE: f32 = 32.0;
51pub const CALENDAR_CELL_RADIUS: f32 = 4.0;
53pub const CALENDAR_CELL_GAP: f32 = 0.0;
55pub const CALENDAR_TODAY_RING_WIDTH: f32 = 1.0;
57pub const CALENDAR_NAV_ICON_SIZE: f32 = 12.0;
59pub const CALENDAR_WEEK_NUMBER_COLUMN_WIDTH: f32 = 28.0;
61pub const CALENDAR_NAV_ARROW_SIZE: f32 = 24.0;
63pub const CALENDAR_NAV_ARROW_RADIUS: f32 = 4.0;
65pub const CALENDAR_HEADER_GAP: f32 = 4.0;
67pub const CALENDAR_ZOOM_CELL_RADIUS: f32 = 6.0;
69
70#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct CalendarRecipe {
73 pub outer_padding: f32,
74 pub section_gap: f32,
75 pub header_height: f32,
76 pub weekday_row_height: f32,
77 pub cell_size: f32,
78 pub cell_radius: f32,
79 pub cell_gap: f32,
80 pub today_ring_width: f32,
81 pub nav_icon_size: f32,
82 pub week_number_column_width: f32,
83 pub nav_arrow_size: f32,
84 pub nav_arrow_radius: f32,
85 pub header_gap: f32,
86 pub zoom_cell_radius: f32,
87}
88
89impl Default for CalendarRecipe {
90 fn default() -> Self {
91 Self {
92 outer_padding: CALENDAR_OUTER_PADDING,
93 section_gap: CALENDAR_SECTION_GAP,
94 header_height: CALENDAR_HEADER_HEIGHT,
95 weekday_row_height: CALENDAR_WEEKDAY_ROW_HEIGHT,
96 cell_size: CALENDAR_CELL_SIZE,
97 cell_radius: CALENDAR_CELL_RADIUS,
98 cell_gap: CALENDAR_CELL_GAP,
99 today_ring_width: CALENDAR_TODAY_RING_WIDTH,
100 nav_icon_size: CALENDAR_NAV_ICON_SIZE,
101 week_number_column_width: CALENDAR_WEEK_NUMBER_COLUMN_WIDTH,
102 nav_arrow_size: CALENDAR_NAV_ARROW_SIZE,
103 nav_arrow_radius: CALENDAR_NAV_ARROW_RADIUS,
104 header_gap: CALENDAR_HEADER_GAP,
105 zoom_cell_radius: CALENDAR_ZOOM_CELL_RADIUS,
106 }
107 }
108}
109
110#[derive(Debug, Default, Clone, Copy)]
112pub struct RecipeCalendarStyle {
113 pub recipe: CalendarRecipe,
114}
115
116impl RecipeCalendarStyle {
117 pub fn new(recipe: CalendarRecipe) -> Self {
118 Self { recipe }
119 }
120}
121
122impl CalendarStyle for RecipeCalendarStyle {
123 fn make_day_cell(&self, cfg: &CalendarDayConfig, ctx: &mut BuildContext) -> WidgetId {
124 let radius = self.recipe.cell_radius;
125 let today_ring_width = self.recipe.today_ring_width;
126
127 let bg_role: Signal<SurfaceRole> = cfg.fill.map(|f| match f {
130 CalendarDayFill::Selected => SurfaceRole::Selected,
131 CalendarDayFill::InRange => SurfaceRole::SelectedInactive,
132 CalendarDayFill::None => SurfaceRole::Transparent,
133 });
134 let bg_widget = RectWidget::new()
135 .background(bg_role)
136 .corner_radius(CornerRadius::uniform(radius));
137 let bg_id = ctx.add(bg_widget);
138
139 let ring_id = if cfg.is_today && !cfg.is_out_of_month {
141 let ring = RectWidget::new()
142 .background(SurfaceRole::Transparent)
143 .border_color(BorderRole::Focused)
144 .border_width(today_ring_width)
145 .corner_radius(CornerRadius::uniform(radius));
146 Some(ctx.add(ring))
147 } else {
148 None
149 };
150
151 let focus_ring_width = ctx.theme_signal().get().shape.focus_ring_width;
155 let focus_ring = RectWidget::new()
156 .background(SurfaceRole::Transparent)
157 .border_color(BorderRole::Focused)
158 .border_width(focus_ring_width)
159 .corner_radius(CornerRadius::uniform(radius));
160 let focus_ring_id = ctx.add(focus_ring);
161 ctx.visible_when(focus_ring_id, cfg.is_focused_cell.clone());
162
163 let text_color: ColorProp = if cfg.is_disabled || cfg.is_out_of_month {
167 TextRole::Disabled.into()
168 } else {
169 let role_signal: Signal<TextRole> = cfg.fill.map(|f| match f {
170 CalendarDayFill::Selected => TextRole::OnAccent,
171 _ => TextRole::Primary,
172 });
173 ColorProp::DynamicTextRole(role_signal)
174 };
175 let label = TextWidget::new(lit!(cfg.label.clone()))
176 .style(TextStyleRole::Body)
177 .color(text_color)
178 .single_line()
179 .a11y_hidden();
180 let label_id = ctx.add(label);
181 let centered = ctx.add(Center::new().child_id(label_id));
182
183 let mut z = ZStack::new().add_child(bg_id);
185 if let Some(ring) = ring_id {
186 z = z.add_child(ring);
187 }
188 z = z.add_child(focus_ring_id).add_child(centered);
189 let z_id = ctx.add(z);
190
191 ctx.add(
192 FixedSize::new()
193 .width(cfg.cell_size)
194 .height(cfg.cell_size)
195 .child_id(z_id),
196 )
197 }
198
199 fn make_zoom_cell(&self, cfg: &CalendarZoomCellConfig, ctx: &mut BuildContext) -> WidgetId {
200 let bg_role = cfg
203 .is_selected
204 .clone()
205 .zip3(&cfg.is_hovered, &cfg.is_pressed)
206 .map(|(sel, hov, prs)| {
207 if *sel {
208 SurfaceRole::Accent
209 } else if *prs {
210 SurfaceRole::Pressed
211 } else if *hov {
212 SurfaceRole::Hover
213 } else {
214 SurfaceRole::Transparent
215 }
216 });
217 let text_role = cfg.is_selected.map(|sel| {
218 if *sel {
219 TextRole::OnAccent
220 } else {
221 TextRole::Primary
222 }
223 });
224
225 let bg = RectWidget::new()
226 .background(ColorProp::DynamicSurfaceRole(bg_role))
227 .corner_radius(CornerRadius::uniform(self.recipe.zoom_cell_radius));
228 let bg_id = ctx.add(bg);
229
230 let text = TextWidget::new(lit!(cfg.label.clone()))
231 .style(TextStyleRole::Body)
232 .color(ColorProp::DynamicTextRole(text_role))
233 .single_line();
234 let text_id = ctx.add(Center::new().child(text));
235
236 let z_id = ctx.add(ZStack::new().add_child(bg_id).add_child(text_id));
237 ctx.add(
238 FixedSize::new()
239 .width(cfg.cell_width)
240 .height(cfg.cell_height)
241 .child_id(z_id),
242 )
243 }
244
245 fn make_header(&self, cfg: &CalendarHeaderConfig, ctx: &mut BuildContext) -> WidgetId {
246 let title_filled = ctx.add(Expand::horizontal().child_id(cfg.title));
250
251 let mut row = HStack::new().spacing(self.recipe.header_gap);
252 if let Some(id) = cfg.prev_double {
253 row = row.add_child(id);
254 }
255 if let Some(id) = cfg.prev {
256 row = row.add_child(id);
257 }
258 row = row.add_child(title_filled);
259 if let Some(id) = cfg.next {
260 row = row.add_child(id);
261 }
262 if let Some(id) = cfg.next_double {
263 row = row.add_child(id);
264 }
265 ctx.add(row)
266 }
267}