From 1a08bfbcdc7f8391f5e9dc4fe4d3bb34b054cff1 Mon Sep 17 00:00:00 2001 From: fab Date: Sun, 11 Jun 2023 09:00:48 +0200 Subject: [PATCH] improved event rendering * events that span over multiple days ar rendered as a single bar instead of a series of per-day events Signed-off-by: fab --- src/model/events.rs | 64 ++++++++- src/ui/basics.rs | 10 +- src/ui/calendar.rs | 329 +++++++++++++++++++++++++++----------------- 3 files changed, 270 insertions(+), 133 deletions(-) diff --git a/src/model/events.rs b/src/model/events.rs index d0bef44..7f56392 100644 --- a/src/model/events.rs +++ b/src/model/events.rs @@ -3,7 +3,7 @@ use std::vec::Vec; use std::default::Default; use std::string::String; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Event { pub text: String, pub begin: NaiveDate, @@ -21,9 +21,24 @@ pub struct Event { //} // impl Event { - fn is_in_day(&self, day: NaiveDate) -> bool { + pub fn is_in_day(&self, day: NaiveDate) -> bool { day >= self.begin && day <= self.end } + + pub fn is_in_days(&self, first_day: NaiveDate, last_day: NaiveDate) -> bool { + let mut day = first_day; + while day <= last_day { + if self.is_in_day(day) { + return true + } + day = day.succ_opt().unwrap(); + } + false + } + + pub fn span_days(&self) -> i64 { + (self.end - self.begin).num_days() + } } pub struct EventsCollection { @@ -45,6 +60,21 @@ impl EventsCollection { begin: NaiveDate::from_ymd_opt(2023, 07, 31).unwrap(), end: NaiveDate::from_ymd_opt(2023, 09, 11).unwrap(), }, + Event { + text: String::from("event one"), + begin: NaiveDate::from_ymd_opt(2023, 07, 31).unwrap(), + end: NaiveDate::from_ymd_opt(2023, 08, 03).unwrap(), + }, + Event { + text: String::from("event two"), + begin: NaiveDate::from_ymd_opt(2023, 08, 02).unwrap(), + end: NaiveDate::from_ymd_opt(2023, 08, 04).unwrap(), + }, + Event { + text: String::from("event three"), + begin: NaiveDate::from_ymd_opt(2023, 08, 05).unwrap(), + end: NaiveDate::from_ymd_opt(2023, 08, 06).unwrap(), + }, ], } } @@ -67,6 +97,36 @@ impl EventsCollection { } events_in_day } + + pub fn starting_at(&self, day: NaiveDate) -> Vec { + let mut events_in_day = Vec::new(); + for ev_day in &self.events { + if ev_day.begin == day { + events_in_day.push(ev_day.clone()); + } + } + events_in_day + } + + pub fn starting_before(&self, day: NaiveDate) -> Vec { + let mut events_in_day = Vec::new(); + for ev_day in &self.events { + if ev_day.begin < day { + events_in_day.push(ev_day.clone()); + } + } + events_in_day + } + + pub fn within(&self, first_day: NaiveDate, last_day: NaiveDate) -> Vec { + let mut events_in_day = Vec::new(); + for ev_day in &self.events { + if ev_day.is_in_days(first_day, last_day) { + events_in_day.push(ev_day.clone()); + } + } + events_in_day + } } impl Default for EventsCollection { diff --git a/src/ui/basics.rs b/src/ui/basics.rs index 99ffa97..74292a0 100644 --- a/src/ui/basics.rs +++ b/src/ui/basics.rs @@ -1,5 +1,5 @@ -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Cell { // display coordinates of the cell pub x : f32, @@ -46,6 +46,14 @@ impl CellGrid { } } + pub fn num_rows(&self) -> u32 { + return self.num_y; + } + + pub fn num_cols(&self) -> u32 { + return self.num_x; + } + pub fn rows(&self) -> Self { CellGrid::new(self.x, self.y, self.width, self.height, 1, self.num_y) } diff --git a/src/ui/calendar.rs b/src/ui/calendar.rs index 3ee19ad..e80875a 100644 --- a/src/ui/calendar.rs +++ b/src/ui/calendar.rs @@ -12,9 +12,9 @@ use iced::advanced::{layout, renderer}; use iced::advanced::widget::{Tree, Widget}; use iced::{Color, Element, Length, Point, Rectangle, alignment}; use iced::advanced::text::{self, Text, LineHeight, Shaping}; -use chrono::{NaiveDate, Datelike, Duration, Local}; +use chrono::{NaiveDate, Datelike, Duration, Local, Months}; use super::basics::CellGrid; -use crate::model::events::EventsCollection; +use crate::model::events::{EventsCollection, Event}; use iced::mouse; #[cfg(feature = "tracing")] @@ -84,44 +84,85 @@ impl CalendarParams { //------------------------------------------------------------------------- -fn render_events_in_day( +fn render_events_in_row( params: &CalendarParams, renderer: &mut impl text::Renderer, - current_day: NaiveDate, - day_bounds: Rectangle, + first_day: NaiveDate, + num_days: i64, + row_bounds: Rectangle, font_size: f32, fg: Color, content: &str, events: &EventsCollection ) { + if num_days < 1 { + return + } + + #[derive(Debug)] + struct EventBar<'a> { + ev: &'a Event, + bounds: Rectangle, + } // render events, if enough space - let day_text_size = renderer.measure( + let day_text_height = renderer.measure( content, font_size, LineHeight::default(), renderer.default_font(), - day_bounds.size(), - Shaping::default()); + row_bounds.size(), + Shaping::default()).1; + + let last_day = first_day + Duration::days(num_days - 1); + let all_events = events.within(first_day, last_day); + let x = row_bounds.x; + let y = row_bounds.y + params.day_text_margin + day_text_height; let ev_height = params.ev_height; + let mut ev_y: f32 = 0.0; - let y = day_bounds.y + params.day_text_margin; - for ev_day in events.for_day(current_day) { - if day_bounds.height - day_text_size.1 > ev_y + ev_height { - let ev_bounds = Rectangle { - y: y + ev_y + day_text_size.1, - height: ev_height, - ..day_bounds - }; + + let mut ev_bars : Vec = all_events.iter().map(|e| EventBar{ev: e, bounds: Rectangle {x, y, width: 0.0, height: ev_height}}).collect(); + + // TODO: incompatible types num_days, grid num_cols + let row_grid = CellGrid::new(row_bounds.x, row_bounds.y, row_bounds.width, row_bounds.height, num_days.try_into().unwrap(), 1); + + let mut current_day = first_day; + + // update event bars + for cell in row_grid { + ev_y = y; + for ev_bar in ev_bars.iter_mut() { + if ev_bar.ev.begin == current_day || (ev_bar.ev.begin < first_day && current_day == first_day) { + // start of event + ev_bar.bounds.x = cell.x; + ev_bar.bounds.y = ev_y; + } else if ev_bar.ev.end == current_day { + // end of event -> set width + ev_bar.bounds.width = cell.x + cell.width - ev_bar.bounds.x; + } + if ev_bar.ev.is_in_day(current_day) { + ev_y += ev_height; + } + } + current_day = current_day.succ_opt().unwrap(); + } + + for ev_bar in &mut ev_bars { + // close events that exceed the row + if ev_bar.ev.end >= current_day { + ev_bar.bounds.width = row_bounds.x + row_bounds.width - ev_bar.bounds.x; + } + if row_bounds.y + row_bounds.height > ev_bar.bounds.y + ev_bar.bounds.height { renderer.fill_quad(renderer::Quad { - bounds: ev_bounds, + bounds: ev_bar.bounds, border_radius: 0.0.into(), border_width: 1.0, border_color: params.day_other_month_fg, }, params.ev_bg); renderer.fill_text(Text { - content: ev_day.text.as_str(), - bounds: ev_bounds, + content: ev_bar.ev.text.as_str(), + bounds: ev_bar.bounds, size: params.ev_fontsize, line_height: LineHeight::default(), color: fg, @@ -296,71 +337,82 @@ impl<'a> CalendarMonthView<'a> { let mut current_day = self.first_day_in_view; let grid = CellGrid::new(bounds.x, bounds.y, bounds.width, bounds.height, 7, 6); - for cell in grid { - let day_bounds = Rectangle { - x: cell.x, - y: cell.y, - width: cell.width, - height : cell.height + for row in grid.rows() { + let row_first_day = current_day; + let row_grid = CellGrid::new(row.x, row.y, row.width, row.height, 7, 1); + for cell in row_grid { + let day_bounds = Rectangle { + x: cell.x, + y: cell.y, + width: cell.width, + height : cell.height + }; + + // label (day letter on row 0, day number on the rest) + let t = current_day.day().to_string(); + let content = t.as_str(); + + // color of text + let fg = if current_day.month() == self.first_day.month() { + self.params.day_fg + } else { + self.params.day_other_month_fg + }; + + // background color of the day cell + let bg_color = if current_day == Local::now().date_naive() { + self.params.day_today_bg + } else if current_day.weekday().num_days_from_monday() > 4 { + self.params.day_weekend_bg + } else { + Color::TRANSPARENT + }; + + // where to place the day content + let x = day_bounds.x + self.params.day_text_margin; + let y = day_bounds.y + self.params.day_text_margin; + + renderer.fill_quad(renderer::Quad { + bounds: Rectangle {width: day_bounds.width + 0.5, height: day_bounds.height + 0.5, ..day_bounds}, + border_radius: 0.0.into(), + border_width: 1.0, + border_color: self.params.day_other_month_fg, + }, + bg_color); + + // render day cell text + renderer.fill_text(Text { + content, + bounds: Rectangle {x, y, ..day_bounds}, + size: font_size, + line_height: LineHeight::default(), + color: fg, + font: renderer.default_font(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: Shaping::default() + }); + + current_day = current_day.succ_opt().unwrap(); + } + let row_bounds = Rectangle { + x: row.x, + y: row.y, + width: row.width, + height : row.height }; - - // label (day letter on row 0, day number on the rest) - let t = current_day.day().to_string(); - let content = t.as_str(); - - // color of text - let fg = if current_day.month() == self.first_day.month() { - self.params.day_fg - } else { - self.params.day_other_month_fg - }; - - // background color of the day cell - let bg_color = if current_day == Local::now().date_naive() { - self.params.day_today_bg - } else if current_day.weekday().num_days_from_monday() > 4 { - self.params.day_weekend_bg - } else { - Color::TRANSPARENT - }; - - // where to place the day content - let x = day_bounds.x + self.params.day_text_margin; - let y = day_bounds.y + self.params.day_text_margin; - - renderer.fill_quad(renderer::Quad { - bounds: Rectangle {width: day_bounds.width + 0.5, height: day_bounds.height + 0.5, ..day_bounds}, - border_radius: 0.0.into(), - border_width: 1.0, - border_color: self.params.day_other_month_fg, - }, - bg_color); - - // render day cell text - renderer.fill_text(Text { - content, - bounds: Rectangle {x, y, ..day_bounds}, - size: font_size, - line_height: LineHeight::default(), - color: fg, - font: renderer.default_font(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::default() - }); - - render_events_in_day( + let content = "10"; + render_events_in_row( &self.params, renderer, - current_day, - day_bounds, + row_first_day, + 6, + row_bounds, font_size, - fg, + self.params.day_fg, content, self.events ); - - current_day = current_day.succ_opt().unwrap(); } } @@ -605,77 +657,94 @@ impl<'a> CalendarYearView<'a> { let font_size = renderer.default_size() as f32; let grid = CellGrid::new(bounds.x, bounds.y, bounds.width, bounds.height, 42, 12); - for cell in grid { - let day_bounds = Rectangle { - x: cell.x + 0.5, - y: cell.y + 0.5, - width: cell.width, - height: cell.height - }; + + for row in grid.rows() { + let row_grid = CellGrid::new(row.x, row.y, row.width, row.height, 42, 1); // the row index is the month - let month = cell.pos_y; + let month = row.pos_y; let first_day_of_month = self.first_day.with_day0(0).unwrap().with_month0(month).unwrap(); - let first_weekday = first_day_of_month.weekday().num_days_from_monday(); - let current_day = first_day_of_month + Duration::days((cell.pos_x as i64) - (first_weekday as i64)); + let mut row_bounds: Rectangle = Rectangle{x: row.x, y: row.y, width: row.width, height: row.height}; + let row_days = ((first_day_of_month + Months::new(1)) - first_day_of_month).num_days(); - if current_day.month0() == month { - let weekday = current_day.weekday().num_days_from_monday(); + for cell in row_grid { - // label (day letter on row 0, day number on the rest) - let t = current_day.day().to_string(); - let content = t.as_str(); - - // color of text - let fg = self.params.day_fg; - - // background color of the day cell - let bg_color = if current_day == Local::now().date_naive() { - self.params.day_today_bg - } else if weekday > 4 { - self.params.day_weekend_bg - } else { - Color::TRANSPARENT + let day_bounds = Rectangle { + x: cell.x, + y: cell.y, + width: cell.width, + height : cell.height }; + let current_day = first_day_of_month + Duration::days((cell.pos_x as i64) - (first_weekday as i64)); - // where to place the day content - let x = day_bounds.x + self.params.day_text_margin; - let y = day_bounds.y + self.params.day_text_margin; + if current_day.month0() == month { + if current_day.day() == 1 { + let diff = cell.x - row_bounds.x; + row_bounds.x = cell.x; + row_bounds.width -= diff; + } else if current_day.day() == row_days as u32 { + row_bounds.width = cell.x - row_bounds.x + cell.width; + } + let weekday = current_day.weekday().num_days_from_monday(); - renderer.fill_quad(renderer::Quad { - bounds: day_bounds, - border_radius: 0.0.into(), - border_width: 1.0, - border_color: self.params.day_other_month_fg, - }, - bg_color); + // label (day letter on row 0, day number on the rest) + let t = current_day.day().to_string(); + let content = t.as_str(); - // render day cell text - renderer.fill_text(Text { - content, - bounds: Rectangle {x, y, ..day_bounds}, - size: font_size, - line_height: LineHeight::default(), - color: fg, - font: renderer.default_font(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: Shaping::default(), - }); + // color of text + let fg = self.params.day_fg; - render_events_in_day( + // background color of the day cell + let bg_color = if current_day == Local::now().date_naive() { + self.params.day_today_bg + } else if weekday > 4 { + self.params.day_weekend_bg + } else { + Color::TRANSPARENT + }; + + // where to place the day content + let x = day_bounds.x + self.params.day_text_margin; + let y = day_bounds.y + self.params.day_text_margin; + + renderer.fill_quad(renderer::Quad { + bounds: day_bounds, + border_radius: 0.0.into(), + border_width: 1.0, + border_color: self.params.day_other_month_fg, + }, + bg_color); + + // render day cell text + renderer.fill_text(Text { + content, + bounds: Rectangle {x, y, ..day_bounds}, + size: font_size, + line_height: LineHeight::default(), + color: fg, + font: renderer.default_font(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: Shaping::default(), + }); + + } + } +// let num_days = grid.num_cols() as i64; // ((current_day + Months::new(1)) - current_day).num_days(); + let content = "10"; + render_events_in_row( &self.params, renderer, - current_day, - day_bounds, + first_day_of_month, + row_days as i64, + row_bounds, font_size, - fg, + self.params.day_fg, content, self.events ); } - } } fn compute_month_col_width(&self, renderer: &mut impl text::Renderer) -> f32 {