// For now, to implement a custom native widget you will need to add // `iced_native` and `iced_wgpu` to your dependencies. // // Then, you simply need to define your widget type and implement the // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. // // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. use super::basics::CellGrid; use crate::model::events::{Event, EventsCollection}; use chrono::{Datelike, Duration, Local, Months, NaiveDate}; use iced::advanced::text::{self, LineHeight, Shaping, Text, Paragraph}; use iced::advanced::widget::{Tree, Widget}; use iced::advanced::{layout, renderer}; use iced::mouse; use iced::{alignment, Color, Element, Length, Pixels, Point, Rectangle, Size}; #[cfg(feature = "tracing")] extern crate lttng_ust; #[cfg(feature = "tracing")] use lttng_ust::import_tracepoints; #[cfg(feature = "tracing")] import_tracepoints!(concat!(env!("OUT_DIR"), "/tracepoints.rs"), tracepoints); const MONTH_NAMES: [&str; 12] = [ "gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic", ]; const DAY_NAMES: [&str; 7] = ["LUN", "MAR", "MER", "GIO", "VEN", "SAB", "DOM"]; //------------------------------------------------------------------------- #[derive(Clone)] pub struct CalendarParams { show_weeks: bool, header_fg: Color, header_bg: Color, day_fg: Color, day_other_month_fg: Color, day_weekend_bg: Color, day_today_bg: Color, day_text_margin: f32, ev_height: f32, ev_bg: Color, ev_fontsize: f32, } impl CalendarParams { pub fn new() -> Self { Self { show_weeks: true, header_fg: Color::BLACK, header_bg: Color::TRANSPARENT, day_today_bg: Color::from_rgb8(214, 242, 252), day_fg: Color::BLACK, day_other_month_fg: Color::from_rgb8(220, 220, 220), day_weekend_bg: Color::from_rgb8(245, 245, 245), day_text_margin: 5.0, ev_height: 20.0, ev_bg: Color::from_rgb8(200, 245, 200), ev_fontsize: 16.0, } } } //------------------------------------------------------------------------- fn render_events_in_row( params: &CalendarParams, renderer: &mut impl text::Renderer, first_day: NaiveDate, num_days: i64, row_bounds: Rectangle, font_size: Pixels, fg: Color, content: &str, events: &EventsCollection, ) { if num_days < 1 { return; } #[cfg(feature = "tracing")] tracepoints::calendar::draw_days_entry(row_bounds.width as i32, row_bounds.height as i32); #[derive(Debug)] struct EventBar<'a> { ev: &'a Event, bounds: Rectangle, } let paragraph = renderer.create_paragraph(Text { content, bounds: row_bounds.size(), size: font_size, line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }); let day_text_height = paragraph.min_height(); // render events, if enough space 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 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; // use the minimum row height to compute available space for event bars // to avoid inconsistentencies when rowas have slightly different heights // and some can fit more event bars than others let min_row_height = row_grid.compute_min_height(); // update event bars for cell in row_grid.iter() { 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; } 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 + min_row_height > ev_bar.bounds.y + ev_bar.bounds.height { renderer.fill_quad( renderer::Quad { 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_bar.ev.text.as_str(), bounds: ev_bar.bounds.size(), size: params.ev_fontsize.into(), line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }, ev_bar.bounds.position(), fg, ); ev_y += ev_height } } #[cfg(feature = "tracing")] tracepoints::calendar::draw_days_exit(row_bounds.width as i32, row_bounds.height as i32); } //------------------------------------------------------------------------- pub struct CalendarMonthView<'a> { first_day: NaiveDate, first_day_in_view: NaiveDate, params: CalendarParams, week_column_width: f32, week_column_font_size: f32, events: &'a EventsCollection, } impl<'a> CalendarMonthView<'a> { pub fn new(params: &CalendarParams, day: NaiveDate, events: &'a EventsCollection) -> Self { // first day of the month let first_day = if day.day() == 1 { day } else { NaiveDate::from_ymd_opt(day.year(), day.month(), 1).unwrap() }; // weekday on first day of the month let weekday_on_first = first_day.weekday(); // first visible day in the view let first_day_in_view = first_day - Duration::days(weekday_on_first.num_days_from_monday() as i64); Self { first_day, first_day_in_view, params: params.clone(), week_column_width: 30.0, week_column_font_size: 18.0, events, } } pub fn set_month(&mut self, day: NaiveDate) { // first day of the month let first_day = if day.day() == 1 { day } else { NaiveDate::from_ymd_opt(day.year(), day.month(), 1).unwrap() }; // weekday on first day of the month let weekday_on_first = first_day.weekday(); // first visible day in the view let first_day_in_view = first_day - Duration::days(weekday_on_first.num_days_from_monday() as i64); self.first_day = first_day; self.first_day_in_view = first_day_in_view; } fn draw_header(&self, renderer: &mut impl text::Renderer, bounds: Rectangle, week_w: f32) { // paint background over full width if self.params.header_bg != Color::TRANSPARENT { renderer.fill_quad( renderer::Quad { bounds, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, self.params.header_bg, ); } // redefine bounds to skip the week column let bounds = Rectangle { x: bounds.x + week_w, y: bounds.y, width: bounds.width - week_w, height: bounds.height, }; // font dimension let font_size = renderer.default_size(); let h_axis = CellGrid::new(bounds.x, bounds.y, bounds.width, bounds.height, 7, 1); for cell in h_axis.iter() { let bounds = Rectangle { x: cell.x + self.params.day_text_margin, y: bounds.center_y(), width: cell.width, height: bounds.height, }; // label (day letter on row 0, day number on the rest) let t = DAY_NAMES[cell.pos_x as usize]; // color of text let fg = self.params.header_fg; renderer.fill_text( Text { content: t, bounds: bounds.size(), size: font_size, line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::default(), }, bounds.position(), fg, ); } } fn draw_week_column(&self, renderer: &mut impl text::Renderer, bounds: Rectangle) { let mut day = self.first_day; let v_axis = CellGrid::new( bounds.x, bounds.y, self.week_column_width, bounds.height, 1, 6, ); for cell in v_axis.iter() { // where to place the week number let day_bounds = Rectangle { x: bounds.x, y: cell.y + self.params.day_text_margin, width: cell.width, height: cell.height, }; let week_of_first_day_of_month = day.iso_week().week(); day += Duration::weeks(1); // render week cell text renderer.fill_text( Text { content: &(week_of_first_day_of_month).to_string(), bounds: day_bounds.size(), size: self.week_column_font_size.into(), line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }, Point { x: day_bounds.center_x(), y: day_bounds.y, }, self.params.day_fg, ); } } fn draw_days(&self, renderer: &mut impl text::Renderer, bounds: Rectangle) { // font dimension let font_size = renderer.default_size(); let mut current_day = self.first_day_in_view; let grid = CellGrid::new(bounds.x, bounds.y, bounds.width, bounds.height, 7, 6); let min_row_height = grid.compute_min_height(); for row in grid.rows().iter() { 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.iter() { 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 }; 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: day_bounds.size(), size: font_size, line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }, bounds.position(), fg, ); current_day = current_day.succ_opt().unwrap(); } let row_bounds = Rectangle { x: row.x, y: row.y, width: row.width, height: row.height, }; let content = "10"; render_events_in_row( &self.params, renderer, row_first_day, 6, row_bounds, font_size, self.params.day_fg, content, self.events, ); } } } // CalendarMonthView impl Widget for CalendarMonthView<'_> where Renderer: text::Renderer, { fn width(&self) -> Length { Length::Shrink } fn height(&self) -> Length { Length::Shrink } fn layout( &self, _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout::Node::new(limits.max()) } fn draw( &self, _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, layout: layout::Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let margin: f32 = 20.0; // week column only visible if there is enough space let week_w = if self.params.show_weeks && bounds.width > self.week_column_width { self.week_column_width } else { 0.0 }; // font and header dimension let font_size = f32::from(renderer.default_size()); let first_row_h = font_size + margin; // header self.draw_header( renderer, Rectangle { height: first_row_h, ..bounds }, week_w, ); // week column if week_w > 0.0 { let x = bounds.x; let y = bounds.y + first_row_h; let width = self.week_column_width; let height = bounds.height - first_row_h; self.draw_week_column( renderer, Rectangle { x, y, width, height, }, ); } // monthly calendar cells let x = bounds.x + week_w; let y = bounds.y + first_row_h; let width = bounds.width - week_w; let height = bounds.height - first_row_h; self.draw_days( renderer, Rectangle { x, y, width, height, }, ); } } impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Renderer: text::Renderer, { fn from(month_view: CalendarMonthView<'a>) -> Self { Self::new(month_view) } } //------------------------------------------------------------------------- // 5 weeks plus two extra days is enough to accomodate the longest months of 31 days static YEAR_VIEW_DAYS_PER_ROW: u32 = 5 * 7 + 2; pub struct CalendarYearView<'a> { first_day: NaiveDate, first_day_in_view: NaiveDate, params: CalendarParams, month_column_font_size: f32, margin: f32, events: &'a EventsCollection, } impl<'a> CalendarYearView<'a> { pub fn new(params: &CalendarParams, day: NaiveDate, events: &'a EventsCollection) -> Self { // first day of the year let first_day = NaiveDate::from_ymd_opt(day.year(), 1, 1).unwrap(); // weekday on first day of the year let weekday_on_first = first_day.weekday(); // first visible day in the view let first_day_in_view = first_day - Duration::days(weekday_on_first.num_days_from_monday() as i64); Self { first_day, first_day_in_view, params: params.clone(), month_column_font_size: 24.0, margin: 10.0, events, } } pub fn set_year(&mut self, day: NaiveDate) { // first day of the year self.first_day = NaiveDate::from_ymd_opt(day.year(), 1, 1).unwrap(); // weekday on first day of the year let weekday_on_first = self.first_day.weekday(); // first visible day in the view self.first_day_in_view = self.first_day - Duration::days(weekday_on_first.num_days_from_monday() as i64); } fn draw_header(&self, renderer: &mut impl text::Renderer, bounds: Rectangle, week_w: f32) { // paint background over full width if self.params.header_bg != Color::TRANSPARENT { renderer.fill_quad( renderer::Quad { bounds, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, self.params.header_bg, ); } // redefine bounds to skip the week column let bounds = Rectangle { x: bounds.x + week_w, y: bounds.y, width: bounds.width - week_w, height: bounds.height, }; // font dimension let font_size = renderer.default_size(); let days_of_week = ["L", "M", "M", "G", "V", "S", "D"]; let grid = CellGrid::new( bounds.x, bounds.y, bounds.width, bounds.height, YEAR_VIEW_DAYS_PER_ROW, 1, ); for cell in grid.iter() { let bounds = Rectangle { x: cell.x + 0.5, y: cell.y + 0.5, width: cell.width, height: cell.height, }; let weekday = (cell.pos_x as usize) % 7; // background color of the day cell let bg_color = if weekday > 4 { self.params.day_weekend_bg } else { Color::TRANSPARENT }; renderer.fill_quad( renderer::Quad { bounds, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, bg_color, ); // label (day letter on row 0, day number on the rest) let t = days_of_week[weekday]; // color of text let fg = self.params.header_fg; let x = bounds.x + self.params.day_text_margin; let y = bounds.center_y(); renderer.fill_text( Text { content: t, bounds: bounds.size(), size: font_size, line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::default(), }, Point { x, y }, fg, ); } } fn draw_month_column(&self, renderer: &mut impl text::Renderer, bounds: Rectangle) { // dimensions of each box representing a month name let h: f32 = bounds.height / 12.0; for month in 0..12usize { // where to place the month name let month_name_bounds = Rectangle { x: bounds.x, y: (month as f32) * h + bounds.y + self.params.day_text_margin, width: bounds.width, height: h, }; // render month name renderer.fill_text( Text { content: MONTH_NAMES[month], bounds: month_name_bounds.size(), size: self.month_column_font_size.into(), line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::default(), }, Point { x: month_name_bounds.x + self.margin, y: month_name_bounds.center_y(), }, self.params.day_fg, ); } } fn draw_days(&self, renderer: &mut impl text::Renderer, bounds: Rectangle) { // font dimension let font_size = renderer.default_size(); let grid = CellGrid::new( bounds.x, bounds.y, bounds.width, bounds.height, YEAR_VIEW_DAYS_PER_ROW, 12, ); for row in grid.rows().iter() { let row_grid = CellGrid::new( row.x, row.y, row.width, row.height, YEAR_VIEW_DAYS_PER_ROW, 1, ); // the row index is the month 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 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(); for cell in row_grid.iter() { 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)); 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(); // 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 }; 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: day_bounds.size(), size: font_size, line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }, day_bounds.position(), fg, ); } } let content = "10"; render_events_in_row( &self.params, renderer, first_day_of_month, row_days as i64, row_bounds, font_size, self.params.day_fg, content, self.events, ); } } fn compute_month_col_width(&self, renderer: &mut impl text::Renderer, bounds: Size) -> f32 { let mut max_max_font_width = 0.0; for month_name in MONTH_NAMES { let paragraph = renderer.create_paragraph(Text { content: month_name, bounds, size: renderer.default_size(), line_height: LineHeight::default(), font: renderer.default_font(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::default(), }); let month_width = paragraph.min_width(); if month_width > max_max_font_width { max_max_font_width = month_width; } } return max_max_font_width + self.margin; } } // CalendarYearView impl Widget for CalendarYearView<'_> where Renderer: text::Renderer, { fn width(&self) -> Length { Length::Shrink } fn height(&self) -> Length { Length::Shrink } fn layout( &self, _tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { layout::Node::new(limits.max()) } fn draw( &self, _state: &Tree, renderer: &mut Renderer, _theme: &Renderer::Theme, _style: &renderer::Style, layout: layout::Layout<'_>, _cursor: mouse::Cursor, _viewport: &Rectangle, ) { let bounds = layout.bounds(); let margin: f32 = 20.0; #[cfg(feature = "tracing")] tracepoints::calendar::draw_entry(bounds.width as i32, bounds.height as i32); // week column only visible if there is enough space let month_w = if self.params.show_weeks { //self.month_column_width self.compute_month_col_width(renderer, bounds.size()) + self.margin } else { 0.0 }; // font and header dimension let font_size = f32::from(renderer.default_size()); let first_row_h = font_size + margin; // header self.draw_header( renderer, Rectangle { height: first_row_h, ..bounds }, month_w, ); // month column if month_w > 0.0 && bounds.width > month_w { let x = bounds.x; let y = bounds.y + first_row_h; let width = month_w; let height = bounds.height - first_row_h; self.draw_month_column( renderer, Rectangle { x, y, width, height, }, ); } // monthly calendar cells let x = bounds.x + month_w; let y = bounds.y + first_row_h; let width = bounds.width - month_w; let height = bounds.height - first_row_h; self.draw_days( renderer, Rectangle { x, y, width, height, }, ); #[cfg(feature = "tracing")] tracepoints::calendar::draw_exit(bounds.width as i32, bounds.height as i32); } } impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Renderer: text::Renderer, { fn from(year_view: CalendarYearView<'a>) -> Self { Self::new(year_view) } }