use crate::{ helper::{month_to_str, ordering_suffix, weekday_to_str}, Globals, }; use chrono::{Datelike, Duration, NaiveDateTime, Timelike}; use egui::{Color32, Rect, Sense, Slider, Stroke, Ui, Vec2}; use egui_extras::{Size, TableBuilder}; use std::{collections::BTreeMap, ops::Range}; pub struct Calendar { offset: i64, height: f32, } impl Default for Calendar { fn default() -> Self { Self { offset: 0, height: 1500.0, } } } impl Calendar { pub fn ui(&mut self, ui: &mut Ui, g: &mut Globals) { let start_date = chrono::Utc::now().date_naive() + chrono::Duration::days(self.offset); let end_date = start_date + chrono::Duration::days(7); let start_dt = start_date.and_hms(0, 0, 0); let end_dt = end_date.and_hms(0, 0, 0); let task_ids = g.tasks.keys().map(|e| e.to_owned()).collect::>(); let instances = BTreeMap::from_iter(task_ids.iter().filter_map(|id| { Some((id, g.get_instances_range(*id, start_dt, end_dt).to_owned()?)) })); ui.add( Slider::new(&mut self.height, 500.0..=5000.0) .logarithmic(true) .text("Line spacing"), ); ui.add( Slider::new(&mut self.offset, -28..=28) .logarithmic(true) .clamp_to_range(false) .text("Offset by days"), ); TableBuilder::new(ui) .column(Size::exact(50.0)) .columns(Size::remainder(), 7) .header(50.0, |mut tr| { tr.col(|_| {}); for d in 0..7 { tr.col(|ui| { let d = (start_dt + Duration::days(d as i64)).date(); ui.heading(&format!( "{} the {}{} {}", weekday_to_str(d.weekday().num_days_from_monday().into()), d.day(), ordering_suffix(d.day()), &month_to_str(d.month0() as i64)[..3] )); }); } }) .body(|mut tb| { tb.row(self.height, |mut tr| { tr.col(|ui| { let (response, p) = ui.allocate_painter(Vec2::new(50.0, self.height), Sense::hover()); for h in 0..24 { p.text( response.rect.min + Vec2::new(0.0, h as f32 / 24.0 * self.height), egui::Align2::LEFT_TOP, format!("{h:02}:00"), egui::FontId::monospace(15.0), Color32::from_gray(150), ); } }); for d in 0..7 { tr.col(|ui| { let time = start_dt + Duration::days(d as i64); let time_end = time + Duration::days(1) - Duration::seconds(1); let instances_here = instances .iter() .map(|(id, rs)| { ( id, rs.iter() .filter(|r| r.overlaps(time..time_end)) .collect::>(), ) }) .filter(|(_, l)| l.len() != 0); ui.horizontal(|ui| { for (id, rs) in instances_here { let task = g.tasks.get(id).unwrap(); let (rect, response) = ui.allocate_exact_size( Vec2::new(10.0, self.height), Sense::hover(), ); for r in &rs { let r = r.start.unwrap_or(time)..r.end.unwrap_or(time_end); let rect_start = (r.start.hour() as f32 + (r.start.minute() as f32 / 60.0)) / 24.0 * self.height; let rect_end = (r.end.hour() as f32 + (r.end.minute() as f32 / 60.0)) / 24.0 * self.height; let rect = Rect::from_two_pos( rect.min + Vec2::new(0.0, rect_start), rect.min + Vec2::new(10.0, rect_end), ); if let Some(p) = response.hover_pos() { if rect.contains(p) { response.clone().on_hover_ui_at_pointer(|ui| { ui.heading(&task.name); ui.label(&format!( "from {} to {}", r.start, r.end )); if let Some(d) = &task.description { ui.label(d); } }); } } ui.painter().rect_filled(rect, 5.0, Color32::KHAKI); } } }); let mut p1 = ui.max_rect().left_top(); let mut p2 = ui.max_rect().right_top(); for _ in 0..24 { ui.painter().line_segment( [p1, p2], Stroke::new(1.0, Color32::from_gray(50).additive()), ); p1.y += self.height / 24.0; p2.y += self.height / 24.0; } }); } }); }); } } pub trait Overlaps { fn overlaps(&self, v: T) -> bool; } impl Overlaps for Range { fn overlaps(&self, v: NaiveDateTime) -> bool { self.start <= v && v < self.end } } impl Overlaps for Range> { fn overlaps(&self, v: NaiveDateTime) -> bool { match (self.start, self.end) { (Some(s), Some(e)) => s <= v && v < e, (Some(s), None) => s <= v, (None, Some(e)) => v < e, (None, None) => false, } } } impl Overlaps> for Range> { fn overlaps(&self, v: Range) -> bool { match (self.start, self.end) { (None, None) => false, (None, Some(e)) => v.start < e, (Some(s), None) => v.end > s, (Some(s), Some(e)) => v.start < e && v.end > s, } } }