use std::rc::Rc;
use teksilo_canvas::{Rect, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::accesskit::{Action, Role};
use teksilo_core::build_context::BuildContext;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{CalendarZoomCellConfig, SharedCalendarStyle};
use teksilo_core::widget::{CursorIcon, EventContext, LayoutContext, Widget, WidgetPlacement};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_i18n::resolve_message_widget;
use crate::common::datetime::month_long_key;
use crate::common::datetime::types::YearMonth;
use crate::primitives::{HStack, VStack};
use crate::styles::recipe_calendar_style::RecipeCalendarStyle;
use super::CalendarMode;
const COLUMNS: usize = 3;
const ROWS: usize = 4;
const CELL_SPACING: f32 = 4.0;
pub(crate) struct MonthsGrid {
visible_month: Signal<YearMonth>,
mode: Signal<CalendarMode>,
enabled: bool,
cell_width: f32,
cell_height: f32,
root_id: Option<WidgetId>,
}
impl std::fmt::Debug for MonthsGrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MonthsGrid").finish()
}
}
impl MonthsGrid {
pub(crate) fn new(
visible_month: Signal<YearMonth>,
mode: Signal<CalendarMode>,
enabled: bool,
cell_width: f32,
cell_height: f32,
) -> Self {
Self {
visible_month,
mode,
enabled,
cell_width,
cell_height,
root_id: None,
}
}
}
impl Widget for MonthsGrid {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let mut rows: Vec<WidgetId> = Vec::with_capacity(ROWS);
for r in 0..ROWS {
let mut row = HStack::new().spacing(CELL_SPACING);
for c in 0..COLUMNS {
let month = (r * COLUMNS + c + 1) as i8;
let visible_month_for_cell = self.visible_month.clone();
let mode_for_cell = self.mode.clone();
let label = resolve_message_widget(month_long_key(month), &[]);
let selected_signal = visible_month_for_cell.map(move |ym| ym.month() == month);
let cell = ZoomCell::new(
label,
selected_signal,
self.enabled,
self.cell_width,
self.cell_height,
Rc::new(move |ctx_evt: &mut EventContext| {
let cur = visible_month_for_cell.get();
visible_month_for_cell.set(YearMonth::new(cur.year(), month));
mode_for_cell.set(CalendarMode::Days);
ctx_evt.request_frame();
}),
);
row = row.add_child(ctx.add(cell));
}
rows.push(ctx.add(row));
}
let mut col = VStack::new().spacing(CELL_SPACING);
for id in rows {
col = col.add_child(id);
}
let root = ctx.add(col);
self.root_id = Some(root);
vec![root]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match self.root_id {
Some(id) => ctx
.child_size(id, proposal)
.unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
None => proposal.resolve(0.0, 0.0),
}
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.root_id.into_iter().collect()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::Grid);
builder.set_name(resolve_message_widget("calendar-months-grid-label", &[]));
}
}
pub(crate) struct YearsGrid {
visible_month: Signal<YearMonth>,
mode: Signal<CalendarMode>,
enabled: bool,
cell_width: f32,
cell_height: f32,
root_id: Option<WidgetId>,
}
impl std::fmt::Debug for YearsGrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("YearsGrid").finish()
}
}
impl YearsGrid {
pub(crate) fn new(
visible_month: Signal<YearMonth>,
mode: Signal<CalendarMode>,
enabled: bool,
cell_width: f32,
cell_height: f32,
) -> Self {
Self {
visible_month,
mode,
enabled,
cell_width,
cell_height,
root_id: None,
}
}
pub(crate) fn decade_of(year: i16) -> i16 {
(year / 10) * 10
}
}
impl Widget for YearsGrid {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let visible = self.visible_month.get();
let decade_start = Self::decade_of(visible.year());
let mut rows: Vec<WidgetId> = Vec::with_capacity(ROWS);
for r in 0..ROWS {
let mut row = HStack::new().spacing(CELL_SPACING);
for c in 0..COLUMNS {
let cell_year = decade_start - 1 + (r * COLUMNS + c) as i16;
let visible_for_cell = self.visible_month.clone();
let mode_for_cell = self.mode.clone();
let label = format!("{cell_year}");
let selected_signal = visible_for_cell.map(move |ym| ym.year() == cell_year);
let cell = ZoomCell::new(
label,
selected_signal,
self.enabled,
self.cell_width,
self.cell_height,
Rc::new(move |ctx_evt: &mut EventContext| {
let cur = visible_for_cell.get();
visible_for_cell.set(YearMonth::new(cell_year, cur.month()));
mode_for_cell.set(CalendarMode::Months);
ctx_evt.request_frame();
}),
);
row = row.add_child(ctx.add(cell));
}
rows.push(ctx.add(row));
}
let mut col = VStack::new().spacing(CELL_SPACING);
for id in rows {
col = col.add_child(id);
}
let root = ctx.add(col);
self.root_id = Some(root);
vec![root]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match self.root_id {
Some(id) => ctx
.child_size(id, proposal)
.unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
None => proposal.resolve(0.0, 0.0),
}
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.root_id.into_iter().collect()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::Grid);
builder.set_name(resolve_message_widget("calendar-years-grid-label", &[]));
}
}
struct ZoomCell {
label: String,
selected: Signal<bool>,
enabled: bool,
cell_width: f32,
cell_height: f32,
on_pick: Rc<dyn Fn(&mut EventContext)>,
root_id: Option<WidgetId>,
}
impl std::fmt::Debug for ZoomCell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ZoomCell")
.field("label", &self.label)
.finish()
}
}
impl ZoomCell {
fn new(
label: String,
selected: Signal<bool>,
enabled: bool,
cell_width: f32,
cell_height: f32,
on_pick: Rc<dyn Fn(&mut EventContext)>,
) -> Self {
Self {
label,
selected,
enabled,
cell_width,
cell_height,
on_pick,
root_id: None,
}
}
}
impl Widget for ZoomCell {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let hover = ctx.signal(false);
let pressed = ctx.signal(false);
let selected = self.selected.clone();
let style = resolve_calendar_style(ctx);
let cfg = CalendarZoomCellConfig {
label: self.label.clone(),
is_selected: selected,
is_hovered: hover.clone(),
is_pressed: pressed.clone(),
cell_width: self.cell_width,
cell_height: self.cell_height,
};
let chrome_id = style.make_zoom_cell(&cfg, ctx);
let on_pick = self.on_pick.clone();
let on_pick_for_access = on_pick.clone();
let hover_for_handler = hover.clone();
let pressed_for_handler = pressed.clone();
let pressed_for_release = pressed.clone();
let enabled = self.enabled;
let handlers = HandlerSet::new()
.focusable(enabled)
.cursor(CursorIcon::Pointer)
.on_hover(move |entered, _| {
hover_for_handler.set(entered);
if !entered {
pressed_for_release.set(false);
}
})
.on_tap(move |_pos, ctx_evt| {
if !enabled {
return;
}
pressed_for_handler.set(false);
(on_pick)(ctx_evt);
})
.on_access_action(move |action, ctx_evt| {
use teksilo_core::event::EventResponse;
if action == Action::Click {
(on_pick_for_access)(ctx_evt);
EventResponse::Handled
} else {
EventResponse::Ignored
}
});
ctx.apply_self_handlers(handlers);
self.root_id = Some(chrome_id);
vec![chrome_id]
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
match self.root_id {
Some(id) => ctx
.child_size(id, proposal)
.unwrap_or_else(|| proposal.resolve(0.0, 0.0)),
None => proposal.resolve(0.0, 0.0),
}
.into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = bounds.origin();
child.size = bounds.size();
}
}
fn children(&self) -> Vec<WidgetId> {
self.root_id.into_iter().collect()
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_role(Role::GridCell);
builder.set_name(self.label.clone());
if self.selected.get() {
builder.set_selected(true);
}
}
}
fn resolve_calendar_style(ctx: &BuildContext) -> SharedCalendarStyle {
ctx.theme_signal()
.get()
.style_slots
.calendar
.clone()
.unwrap_or_else(|| Rc::new(RecipeCalendarStyle::default()) as SharedCalendarStyle)
}