// 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 super::row::CalendarRow; use super::row::RowDay; use crate::model::events::{Event, EventsCollection}; use chrono::{Datelike, Duration, Local, Months, NaiveDate}; use iced::advanced::text::Paragraph as _; // this is necessary to have Paragraph in scope and use its methods use iced::advanced::text::{self, LineHeight, Shaping, Text}; 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"]; // 5 weeks plus two extra days is enough to accomodate the longest months of 31 days const YEAR_VIEW_DAYS_PER_ROW: u32 = 5 * 7 + 2; //------------------------------------------------------------------------- #[derive(Clone)] pub struct CalendarParams { show_sidebar: 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_margin: f32, ev_bg: Color, ev_fontsize: f32, } impl CalendarParams { pub fn new() -> Self { Self { show_sidebar: 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_margin: 2.0, ev_bg: Color::from_rgb8(200, 245, 200), ev_fontsize: 16.0, } } pub fn bg_for_day(self: &Self, day: NaiveDate) -> Color { let weekday = day.weekday().num_days_from_monday(); if day == Local::now().date_naive() { self.day_today_bg } else if weekday > 4 { self.day_weekend_bg } else { Color::TRANSPARENT } } } //------------------------------------------------------------------------- fn render_events_in_row( params: &CalendarParams, renderer: &mut Renderer, cal_row: CalendarRow, row_bounds: Rectangle, min_row_height: f32, font_size: Pixels, fg: Color, content: &str, events: &EventsCollection, ) where Renderer: text::Renderer, { if cal_row.begin >= cal_row.end { 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::Paragraph::with_text(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 = cal_row.end.pred_opt().unwrap(); let all_events = events.within(cal_row.begin, 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 ev_margin = params.ev_margin; 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, cal_row.num_days().try_into().unwrap(), 1, ); let mut current_day = cal_row.begin; // 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 < cal_row.begin && current_day == cal_row.begin) { // start of event ev_bar.bounds.x = cell.x; ev_bar.bounds.y = ev_y + ev_margin; } 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 + 2.0 * ev_margin; } } 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_bar.bounds, ); ev_y += ev_height } } #[cfg(feature = "tracing")] tracepoints::calendar::draw_days_exit(row_bounds.width as i32, row_bounds.height as i32); } //------------------------------------------------------------------------- fn compute_month_name_width(renderer: &Renderer, bounds: Size, margin: f32, font_size: Pixels) -> f32 where Renderer: text::Renderer, { let mut max_month_width = 0.0; for month_name in MONTH_NAMES { let paragraph = Renderer::Paragraph::with_text(Text { content: month_name, bounds, 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 month_width = paragraph.min_width(); if month_width > max_month_width { max_month_width = month_width; } } max_month_width + margin } //------------------------------------------------------------------------- fn compute_week_num_width(renderer: &Renderer, bounds: Size, margin: f32, font_size: Pixels) -> f32 where Renderer: text::Renderer, { let mut max_month_width = 0.0; let paragraph = Renderer::Paragraph::with_text(Text { content: "55", bounds, 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 month_width = paragraph.min_width(); if month_width > max_month_width { max_month_width = month_width; } max_month_width + margin } //------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CalendarViewMode { Week, Month, Year, } pub struct CalendarView<'a> { first_day: NaiveDate, params: CalendarParams, mode: CalendarViewMode, events: &'a EventsCollection, week_column_width: f32, row_name_font_size: f32, margin: f32, } impl<'a> CalendarView<'a> { pub fn new(mode: CalendarViewMode, params: &CalendarParams, day: NaiveDate, events: &'a EventsCollection) -> Self { let first_day = match mode { CalendarViewMode::Week => day.week(chrono::Weekday::Mon).first_day(), CalendarViewMode::Month => day.with_day0(0).unwrap(), CalendarViewMode::Year => day.with_month0(0).unwrap().with_day0(0).unwrap() }; CalendarView { first_day, params: params.clone(), mode, events, week_column_width: 30.0, row_name_font_size: 24.0, margin: 10.0, } } fn get_days_per_row(&self) -> u32 { match self.mode { CalendarViewMode::Week => 7, // one week -> 7 days CalendarViewMode::Month => 7, // one week per row -> 7 days CalendarViewMode::Year => YEAR_VIEW_DAYS_PER_ROW, // one month per row, aligned by weekday } } fn get_row_count(&self) -> u32 { match self.mode { CalendarViewMode::Week => 1, // just one week CalendarViewMode::Month => 6, // one week per row -> max 6 (incomplate) in a month CalendarViewMode::Year => 12, // one month per row } } fn get_calendar_row(&self, day: NaiveDate, row: u32) -> CalendarRow { match self.mode { CalendarViewMode::Week => CalendarRow::for_week(day + Duration::weeks(row.into())), CalendarViewMode::Month => CalendarRow::for_week(day + Duration::weeks(row.into())), CalendarViewMode::Year => CalendarRow::for_month(day + Months::new(row)), } } fn get_row_label(&self, cal_row: CalendarRow) -> String { match self.mode { CalendarViewMode::Week => (cal_row.begin.iso_week().week()).to_string(), CalendarViewMode::Month => (cal_row.begin.iso_week().week()).to_string(), CalendarViewMode::Year => MONTH_NAMES[cal_row.begin.month0() as usize].to_string(), } } fn get_sidebar_width(&self, renderer: &mut impl text::Renderer, bounds: Size) -> f32 { let sidebar_width = match self.mode { CalendarViewMode::Week => compute_week_num_width(renderer, bounds, self.margin, self.row_name_font_size.into()), CalendarViewMode::Month => compute_week_num_width(renderer, bounds, self.margin, self.row_name_font_size.into()), CalendarViewMode::Year => compute_month_name_width(renderer, bounds, self.margin, self.row_name_font_size.into()), }; // side column only visible if there is enough space if self.params.show_sidebar && bounds.width > sidebar_width { sidebar_width } else { 0.0 } } fn draw_header(&self, renderer: &mut impl text::Renderer, bounds: Rectangle, side_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 side column let bounds = Rectangle { x: bounds.x + side_w, y: bounds.y, width: bounds.width - side_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, self.get_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, bounds, ); } } fn draw_sidebar(&self, renderer: &mut impl text::Renderer, bounds: Rectangle) { // dimensions of each box representing a row name let h: f32 = bounds.height / (self.get_row_count() as f32); for row in 0..self.get_row_count() { let cal_row = self.get_calendar_row(self.first_day, row); // where to place the row name let row_name_bounds = Rectangle { x: bounds.x, y: (row as f32) * h + bounds.y + self.params.day_text_margin, width: bounds.width, height: h, }; // render row name renderer.fill_text( Text { content: &self.get_row_label(cal_row), bounds: row_name_bounds.size(), size: self.row_name_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: row_name_bounds.x + self.margin, y: row_name_bounds.center_y(), }, self.params.day_fg, row_name_bounds, ); } } 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, self.get_days_per_row(), self.get_row_count(), ); // 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 = grid.compute_min_height(); for row in grid.rows().iter() { let row_grid = CellGrid::new( row.x, row.y, row.width, row.height, self.get_days_per_row(), 1, ); let mut row_bounds: Rectangle = Rectangle { x: row.x, y: row.y, width: row.width, height: row.height, }; let cal_row = self.get_calendar_row(self.first_day, row.pos_y); for cell in row_grid.iter() { let dat_for_col = cal_row.date_for_col(cell.pos_x.into()); if let RowDay::InRange(current_day) = dat_for_col { let day_bounds = Rectangle { x: cell.x, y: cell.y, width: cell.width, height: cell.height, }; // update bounds for the rectangle with the actual days if current_day == cal_row.begin { let diff = cell.x - row_bounds.x; row_bounds.x = cell.x; row_bounds.width -= diff; } else if current_day + Duration::days(1) == cal_row.end { row_bounds.width = cell.x - row_bounds.x + cell.width; } // label: day number 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 = self.params.bg_for_day(current_day); 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, day_bounds, ); } } let content = "10"; render_events_in_row( &self.params, renderer, cal_row, row_bounds, min_row_height, font_size, self.params.day_fg, content, self.events, ); } } } impl Widget for CalendarView<'_> where Renderer: text::Renderer, { fn size(&self) -> Size { Size { width: Length::Fill, height: Length::Fill } } 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; // side column only visible if there is enough space let sidebar_width = self.get_sidebar_width(renderer, bounds.size()); // 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 }, sidebar_width, ); // week column if sidebar_width > 0.0 { let x = bounds.x; let y = bounds.y + first_row_h; let width = sidebar_width; let height = bounds.height - first_row_h; self.draw_sidebar( renderer, Rectangle { x, y, width, height, }, ); } // monthly calendar cells let x = bounds.x + sidebar_width; let y = bounds.y + first_row_h; let width = bounds.width - sidebar_width; 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: CalendarView<'a>) -> Self { Self::new(month_view) } }