AkurAI Build
Menu

cosmic-mobile

public

Latest change f8690e8904b42dd793e9b0fd47d1a2a2ede3ada9 - Calendar: add form sits above grid with focused title by AkurAI Build

//! Calendar page: iOS-style month grid with agenda, built on the shared ios controls.
use crate::{core_apps, ios};
use chrono::{Datelike, Days, Months, NaiveDate};
use eframe::egui::*;
pub struct Calendar {
    pub month: NaiveDate,
    pub selected: NaiveDate,
    pub adding: bool,
    focus_title: bool,
}
impl Calendar {
    pub fn new(today: NaiveDate) -> Self {
        Self {
            month: today.with_day(1).unwrap_or(today),
            selected: today,
            adding: false,
            focus_title: false,
        }
    }
    /// 6×7 grid of dates starting on the Monday before or on the 1st.
    pub fn grid(month: NaiveDate) -> [NaiveDate; 42] {
        let first = month.with_day(1).unwrap_or(month);
        let back = first.weekday().num_days_from_monday() as u64;
        let start = first.checked_sub_days(Days::new(back)).unwrap_or(first);
        std::array::from_fn(|i| start.checked_add_days(Days::new(i as u64)).unwrap_or(start))
    }
    pub fn shift(&mut self, months: i32) {
        self.month = if months < 0 {
            self.month
                .checked_sub_months(Months::new(months.unsigned_abs()))
        } else {
            self.month.checked_add_months(Months::new(months as u32))
        }
        .unwrap_or(self.month);
    }
    pub fn ui(
        &mut self,
        ui: &mut Ui,
        today: NaiveDate,
        store: &Result<core_apps::Store, std::io::Error>,
        first: &mut String,
        second: &mut String,
    ) -> Option<Action> {
        let mut action = None;
        let events: Vec<_> = match store {
            Ok(s) => s.for_app("Calendar"),
            Err(_) => Vec::new(),
        };
        let has_event = |d: NaiveDate| {
            events
                .iter()
                .any(|r| core_apps::parse_when(&r.1).is_some_and(|(x, _)| x == d))
        };
        let title = self.month.format("%B %Y").to_string();
        ui.allocate_ui_with_layout(
            vec2(ui.available_width(), 44.),
            Layout::left_to_right(Align::Center),
            |ui| {
                if ui
                    .add(
                        Button::new(RichText::new("‹").size(28.).color(ios::BLUE))
                            .frame(false)
                            .min_size(vec2(44., 44.)),
                    )
                    .clicked()
                {
                    self.shift(-1);
                }
                ui.label(RichText::new(title).size(20.).strong());
                ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
                    if ui
                        .add(
                            Button::new(RichText::new("›").size(28.).color(ios::BLUE))
                                .frame(false)
                                .min_size(vec2(44., 44.)),
                        )
                        .clicked()
                    {
                        self.shift(1);
                    }
                    if ui
                        .add(
                            Button::new(RichText::new("Today").size(17.).color(ios::BLUE))
                                .frame(false)
                                .min_size(vec2(60., 44.)),
                        )
                        .clicked()
                    {
                        self.month = today.with_day(1).unwrap_or(today);
                        self.selected = today;
                    }
                });
            },
        );
        if self.adding {
            ios::card(ui, |ui| {
                ui.add_space(8.);
                ios::field(ui, first, "YYYY-MM-DD or YYYY-MM-DD HH:MM");
                ui.add_space(8.);
                let title = ios::field(ui, second, "Title");
                if self.focus_title {
                    title.request_focus();
                    self.focus_title = false;
                }
                if title.lost_focus() && ui.input(|i| i.key_pressed(Key::Enter)) {
                    action = Some(Action::Save);
                }
                ui.add_space(8.);
            });
            if ios::primary(ui, "Add Event", ios::RED) {
                action = Some(Action::Save);
            }
        }
        ui.add_space(8.);
        let cell = ui.available_width() / 7.;
        ui.horizontal(|ui| {
            ui.spacing_mut().item_spacing.x = 0.;
            for d in ["M", "T", "W", "T", "F", "S", "S"] {
                let (r, _) = ui.allocate_exact_size(vec2(cell, 20.), Sense::hover());
                ui.painter().text(
                    r.center(),
                    Align2::CENTER_CENTER,
                    d,
                    FontId::proportional(13.),
                    ios::SECONDARY,
                );
            }
        });
        let grid = Self::grid(self.month);
        for week in grid.chunks(7) {
            ui.horizontal(|ui| {
                ui.spacing_mut().item_spacing.x = 0.;
                for &d in week {
                    let (r, resp) =
                        ui.allocate_exact_size(vec2(cell, cell.min(52.)), Sense::click());
                    resp.widget_info(|| {
                        WidgetInfo::labeled(
                            WidgetType::Button,
                            true,
                            d.format("%Y-%m-%d").to_string(),
                        )
                    });
                    let in_month = d.month() == self.month.month();
                    let color = if d == self.selected {
                        Color32::WHITE
                    } else if d == today {
                        ios::RED
                    } else if in_month {
                        Color32::WHITE
                    } else {
                        ios::SECONDARY.gamma_multiply(0.6)
                    };
                    if d == self.selected {
                        ui.painter().circle_filled(
                            r.center() - vec2(0., 4.),
                            18.,
                            if d == today { ios::RED } else { ios::FIELD },
                        );
                    }
                    ui.painter().text(
                        r.center() - vec2(0., 4.),
                        Align2::CENTER_CENTER,
                        d.day().to_string(),
                        FontId::proportional(if d == today { 18. } else { 17. }),
                        color,
                    );
                    if has_event(d) {
                        ui.painter().circle_filled(
                            r.center() + vec2(0., 18.),
                            3.,
                            if in_month {
                                ios::SECONDARY
                            } else {
                                ios::SECONDARY.gamma_multiply(0.5)
                            },
                        );
                    }
                    if resp.clicked() {
                        self.selected = d;
                        if !in_month {
                            self.month = d.with_day(1).unwrap_or(d);
                        }
                    }
                }
            });
        }
        ui.add_space(12.);
        ui.allocate_ui_with_layout(
            vec2(ui.available_width(), 44.),
            Layout::left_to_right(Align::Center),
            |ui| {
                ui.label(
                    RichText::new(self.selected.format("%A %-d %B").to_string().to_uppercase())
                        .size(13.)
                        .color(ios::SECONDARY),
                );
                ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
                    if ui
                        .add(
                            Button::new(RichText::new("+").size(28.).color(ios::BLUE))
                                .frame(false)
                                .min_size(vec2(44., 44.)),
                        )
                        .clicked()
                    {
                        self.adding = !self.adding;
                        if self.adding {
                            *first = self.selected.format("%Y-%m-%d").to_string();
                            self.focus_title = true;
                        }
                    }
                });
            },
        );
        ui.add_space(6.);
        let day_events: Vec<_> = events
            .iter()
            .filter(|r| core_apps::parse_when(&r.1).is_some_and(|(x, _)| x == self.selected))
            .collect();
        if day_events.is_empty() {
            ui.label(RichText::new("No events").color(ios::SECONDARY));
        } else {
            ios::card(ui, |ui| {
                for (n, row) in day_events.iter().enumerate() {
                    if n > 0 {
                        ui.separator();
                    }
                    ui.horizontal(|ui| {
                        ui.add_space(2.);
                        let (bar, _) = ui.allocate_exact_size(vec2(4., 36.), Sense::hover());
                        ui.painter().rect_filled(bar, 2., ios::RED);
                        ui.add_space(8.);
                        ui.vertical(|ui| {
                            ui.add_space(4.);
                            ui.label(RichText::new(&row.2).size(17.));
                            let time = core_apps::parse_when(&row.1)
                                .and_then(|(_, t)| t)
                                .map(|t| t.format("%H:%M").to_string())
                                .unwrap_or_else(|| "All day".into());
                            ui.label(RichText::new(time).size(15.).color(ios::SECONDARY));
                            ui.add_space(4.);
                        });
                    });
                }
            });
        }
        action
    }
}
#[derive(PartialEq, Debug)]
pub enum Action {
    Save,
}