Skip to main content

teksilo_core/styles/
calendar_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for `Calendar`. See `docs/styling-system.md`.
5//!
6//! Multi-method trait because `Calendar` paints three distinct cell
7//! shapes, each with its own state semantics:
8//!
9//! * **Day cell** (`make_day_cell`) — the 6×7 grid of day numbers.
10//!   Static state (today / out-of-month / disabled) is computed at
11//!   build time; the selection-derived fill role and the roving-focus
12//!   ring are reactive signals so navigation doesn't rebuild 42 cells.
13//! * **Zoom cell** (`make_zoom_cell`) — the 4×3 month / year picker
14//!   cells. Hover and pressed are reactive (cells handle their own
15//!   pointer state); `selected` is reactive against the visible month.
16//! * **Header** (`make_header`) — the prev-double / prev / title /
17//!   next / next-double row. The four arrow buttons and the title
18//!   button are pre-built (the widget computes the mode-aware step
19//!   callbacks); the style only lays them out.
20//!
21//! Outer chrome (the calendar's background frame + padding) is a thin
22//! popover-style surface and stays widget-owned via raw shape tokens —
23//! not an additional style method.
24
25use std::rc::Rc;
26
27use crate::build_context::BuildContext;
28use crate::signal::Signal;
29use crate::widget_id::WidgetId;
30
31/// Selection-derived fill state for a day cell. The reactive signal in
32/// `CalendarDayConfig` recomputes on every selection change without
33/// re-`build()`ing the cell.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum CalendarDayFill {
36    /// No selection touches this cell.
37    #[default]
38    None,
39    /// This cell is the single-selection target *or* a range endpoint.
40    Selected,
41    /// This cell falls inside a multi-day range, not on an endpoint.
42    InRange,
43}
44
45/// Per-day cell input to `CalendarStyle::make_day_cell`.
46///
47/// The static fields (`is_today`, `is_out_of_month`, `is_disabled`)
48/// don't change within a build session — the calendar binds
49/// `visible_month` at `Rebuild` level, so cells are regenerated when
50/// the visible month moves. Everything reactive (selection fill, the
51/// roving-focus ring) flows through `Signal`s so per-click navigation
52/// doesn't tear down the 42-cell subtree.
53///
54/// `label` is passed as a plain string rather than a pre-built widget
55/// id so the recipe owns the label's text-colour binding — the label
56/// colour depends on the reactive fill state (Selected → `OnAccent`,
57/// otherwise `Primary`) which is paint-time data, not widget-construction
58/// data.
59pub struct CalendarDayConfig {
60    /// Day-number string (e.g. `"15"`). The recipe builds the
61    /// `TextWidget` itself so it can drive the text colour from
62    /// `fill` (Selected → `OnAccent`, otherwise `Primary`). Accepts
63    /// either a plain `String` or a `LocalizedString` from `tr!(…)` —
64    /// `LocalizedString` implements `From<…> for String`,
65    /// so `.into()` covers both shapes. Day numbers are pure digits
66    /// in IntUI and pass through as untranslated literals, but a
67    /// custom recipe is free to localize.
68    pub label: String,
69    /// Reactive selection-derived fill.
70    pub fill: Signal<CalendarDayFill>,
71    /// `true` when this cell's date equals the local "today". The
72    /// recipe paints the today ring on top of any selection fill.
73    pub is_today: bool,
74    /// `true` when this cell's date falls outside the currently-visible
75    /// month (leading / trailing 7-day padding). The recipe usually
76    /// dims the label.
77    pub is_out_of_month: bool,
78    /// `true` when this cell is unselectable (filter, min/max, etc.).
79    /// The recipe forces a disabled appearance regardless of fill.
80    pub is_disabled: bool,
81    /// `true` only while the parent calendar holds keyboard focus AND
82    /// this cell's date is the currently-focused one (roving focus).
83    pub is_focused_cell: Signal<bool>,
84    /// Cell edge length — the recipe sizes its rect to this.
85    pub cell_size: f32,
86}
87
88/// Per-cell input to `CalendarStyle::make_zoom_cell` (used for both
89/// `MonthsGrid` and `YearsGrid` cells).
90pub struct CalendarZoomCellConfig {
91    /// Month-name / year-number string. The recipe builds the
92    /// `TextWidget` itself so it can drive the text colour from
93    /// `is_selected` (Selected → `OnAccent`, otherwise `Primary`).
94    /// Month names ride in as `LocalizedString` from `tr!(…)`
95    /// converted via `.into()`; year numbers ride in as untranslated
96    /// literals.
97    pub label: String,
98    /// `true` when this cell represents the visible-month's month
99    /// (Months grid) or year (Years grid).
100    pub is_selected: Signal<bool>,
101    /// Pointer-hover state (managed by the cell widget).
102    pub is_hovered: Signal<bool>,
103    /// Pointer-press state (managed by the cell widget).
104    pub is_pressed: Signal<bool>,
105    /// Cell footprint.
106    pub cell_width: f32,
107    pub cell_height: f32,
108}
109
110/// Pre-built header components passed to `make_header`. All five slots
111/// are pre-built widgets; the recipe lays them out into a horizontal
112/// strip. When `show_navigation = false` on the calendar, every arrow
113/// slot is `None` and the recipe still returns a row containing just
114/// the title.
115pub struct CalendarHeaderConfig {
116    /// Far-left "step coarser" arrow (« — prev year in Days mode, prev
117    /// decade in Months mode, etc.).
118    pub prev_double: Option<WidgetId>,
119    /// Single-step prev arrow (‹).
120    pub prev: Option<WidgetId>,
121    /// Center title — a `Button` whose label reactively reflects the
122    /// visible month / year / decade and whose `on_activate` demotes
123    /// the calendar mode.
124    pub title: WidgetId,
125    /// Single-step next arrow (›).
126    pub next: Option<WidgetId>,
127    /// Far-right "step coarser" next arrow (»).
128    pub next_double: Option<WidgetId>,
129}
130
131pub trait CalendarStyle: 'static {
132    fn make_day_cell(&self, cfg: &CalendarDayConfig, ctx: &mut BuildContext) -> WidgetId;
133    fn make_zoom_cell(&self, cfg: &CalendarZoomCellConfig, ctx: &mut BuildContext) -> WidgetId;
134    fn make_header(&self, cfg: &CalendarHeaderConfig, ctx: &mut BuildContext) -> WidgetId;
135}
136
137pub type SharedCalendarStyle = Rc<dyn CalendarStyle>;