use teksilo_core::build_context::BuildContext;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{
CalendarDayConfig, CalendarDayFill, CalendarHeaderConfig, CalendarStyle, CalendarZoomCellConfig,
};
use teksilo_core::widget_id::WidgetId;
use teksilo_i18n::lit;
use teksilo_tokens::{BorderRole, CornerRadius, SurfaceRole, TextRole, TextStyleRole};
use crate::primitives::{Center, Expand, FixedSize, HStack, RectWidget, TextWidget, ZStack};
pub const CALENDAR_OUTER_PADDING: f32 = 8.0;
pub const CALENDAR_SECTION_GAP: f32 = 4.0;
pub const CALENDAR_HEADER_HEIGHT: f32 = 28.0;
pub const CALENDAR_WEEKDAY_ROW_HEIGHT: f32 = 20.0;
pub const CALENDAR_CELL_SIZE: f32 = 32.0;
pub const CALENDAR_CELL_RADIUS: f32 = 4.0;
pub const CALENDAR_CELL_GAP: f32 = 0.0;
pub const CALENDAR_TODAY_RING_WIDTH: f32 = 1.0;
pub const CALENDAR_NAV_ICON_SIZE: f32 = 12.0;
pub const CALENDAR_WEEK_NUMBER_COLUMN_WIDTH: f32 = 28.0;
pub const CALENDAR_NAV_ARROW_SIZE: f32 = 24.0;
pub const CALENDAR_NAV_ARROW_RADIUS: f32 = 4.0;
pub const CALENDAR_HEADER_GAP: f32 = 4.0;
pub const CALENDAR_ZOOM_CELL_RADIUS: f32 = 6.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalendarRecipe {
pub outer_padding: f32,
pub section_gap: f32,
pub header_height: f32,
pub weekday_row_height: f32,
pub cell_size: f32,
pub cell_radius: f32,
pub cell_gap: f32,
pub today_ring_width: f32,
pub nav_icon_size: f32,
pub week_number_column_width: f32,
pub nav_arrow_size: f32,
pub nav_arrow_radius: f32,
pub header_gap: f32,
pub zoom_cell_radius: f32,
}
impl Default for CalendarRecipe {
fn default() -> Self {
Self {
outer_padding: CALENDAR_OUTER_PADDING,
section_gap: CALENDAR_SECTION_GAP,
header_height: CALENDAR_HEADER_HEIGHT,
weekday_row_height: CALENDAR_WEEKDAY_ROW_HEIGHT,
cell_size: CALENDAR_CELL_SIZE,
cell_radius: CALENDAR_CELL_RADIUS,
cell_gap: CALENDAR_CELL_GAP,
today_ring_width: CALENDAR_TODAY_RING_WIDTH,
nav_icon_size: CALENDAR_NAV_ICON_SIZE,
week_number_column_width: CALENDAR_WEEK_NUMBER_COLUMN_WIDTH,
nav_arrow_size: CALENDAR_NAV_ARROW_SIZE,
nav_arrow_radius: CALENDAR_NAV_ARROW_RADIUS,
header_gap: CALENDAR_HEADER_GAP,
zoom_cell_radius: CALENDAR_ZOOM_CELL_RADIUS,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeCalendarStyle {
pub recipe: CalendarRecipe,
}
impl RecipeCalendarStyle {
pub fn new(recipe: CalendarRecipe) -> Self {
Self { recipe }
}
}
impl CalendarStyle for RecipeCalendarStyle {
fn make_day_cell(&self, cfg: &CalendarDayConfig, ctx: &mut BuildContext) -> WidgetId {
let radius = self.recipe.cell_radius;
let today_ring_width = self.recipe.today_ring_width;
let bg_role: Signal<SurfaceRole> = cfg.fill.map(|f| match f {
CalendarDayFill::Selected => SurfaceRole::Selected,
CalendarDayFill::InRange => SurfaceRole::SelectedInactive,
CalendarDayFill::None => SurfaceRole::Transparent,
});
let bg_widget = RectWidget::new()
.background(bg_role)
.corner_radius(CornerRadius::uniform(radius));
let bg_id = ctx.add(bg_widget);
let ring_id = if cfg.is_today && !cfg.is_out_of_month {
let ring = RectWidget::new()
.background(SurfaceRole::Transparent)
.border_color(BorderRole::Focused)
.border_width(today_ring_width)
.corner_radius(CornerRadius::uniform(radius));
Some(ctx.add(ring))
} else {
None
};
let focus_ring_width = ctx.theme_signal().get().shape.focus_ring_width;
let focus_ring = RectWidget::new()
.background(SurfaceRole::Transparent)
.border_color(BorderRole::Focused)
.border_width(focus_ring_width)
.corner_radius(CornerRadius::uniform(radius));
let focus_ring_id = ctx.add(focus_ring);
ctx.visible_when(focus_ring_id, cfg.is_focused_cell.clone());
let text_color: ColorProp = if cfg.is_disabled || cfg.is_out_of_month {
TextRole::Disabled.into()
} else {
let role_signal: Signal<TextRole> = cfg.fill.map(|f| match f {
CalendarDayFill::Selected => TextRole::OnAccent,
_ => TextRole::Primary,
});
ColorProp::DynamicTextRole(role_signal)
};
let label = TextWidget::new(lit!(cfg.label.clone()))
.style(TextStyleRole::Body)
.color(text_color)
.single_line()
.a11y_hidden();
let label_id = ctx.add(label);
let centered = ctx.add(Center::new().child_id(label_id));
let mut z = ZStack::new().add_child(bg_id);
if let Some(ring) = ring_id {
z = z.add_child(ring);
}
z = z.add_child(focus_ring_id).add_child(centered);
let z_id = ctx.add(z);
ctx.add(
FixedSize::new()
.width(cfg.cell_size)
.height(cfg.cell_size)
.child_id(z_id),
)
}
fn make_zoom_cell(&self, cfg: &CalendarZoomCellConfig, ctx: &mut BuildContext) -> WidgetId {
let bg_role = cfg
.is_selected
.clone()
.zip3(&cfg.is_hovered, &cfg.is_pressed)
.map(|(sel, hov, prs)| {
if *sel {
SurfaceRole::Accent
} else if *prs {
SurfaceRole::Pressed
} else if *hov {
SurfaceRole::Hover
} else {
SurfaceRole::Transparent
}
});
let text_role = cfg.is_selected.map(|sel| {
if *sel {
TextRole::OnAccent
} else {
TextRole::Primary
}
});
let bg = RectWidget::new()
.background(ColorProp::DynamicSurfaceRole(bg_role))
.corner_radius(CornerRadius::uniform(self.recipe.zoom_cell_radius));
let bg_id = ctx.add(bg);
let text = TextWidget::new(lit!(cfg.label.clone()))
.style(TextStyleRole::Body)
.color(ColorProp::DynamicTextRole(text_role))
.single_line();
let text_id = ctx.add(Center::new().child(text));
let z_id = ctx.add(ZStack::new().add_child(bg_id).add_child(text_id));
ctx.add(
FixedSize::new()
.width(cfg.cell_width)
.height(cfg.cell_height)
.child_id(z_id),
)
}
fn make_header(&self, cfg: &CalendarHeaderConfig, ctx: &mut BuildContext) -> WidgetId {
let title_filled = ctx.add(Expand::horizontal().child_id(cfg.title));
let mut row = HStack::new().spacing(self.recipe.header_gap);
if let Some(id) = cfg.prev_double {
row = row.add_child(id);
}
if let Some(id) = cfg.prev {
row = row.add_child(id);
}
row = row.add_child(title_filled);
if let Some(id) = cfg.next {
row = row.add_child(id);
}
if let Some(id) = cfg.next_double {
row = row.add_child(id);
}
ctx.add(row)
}
}