Menu
cosmic-mobile
publicLatest 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,
}