// 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 iced_native::layout::{self, Layout}; use iced_native::renderer; use iced_native::{Color, Element, Length, Point, Rectangle, Size, Widget}; use iced_native::text; use iced_native::alignment; use iced_native::widget::Tree; use chrono::{NaiveDate, Datelike, Duration, Weekday, Local}; const MONTH_NAMES: [&str;12] = [ "gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic", ]; //------------------------------------------------------------------------- #[derive(Clone)] pub struct CalendarParams { show_weeks: bool, header_fg: Color, header_bg: Color, day_text: Color, day_text_other_month: Color, day_weekend_bg: Color, day_today_bg: Color, day_text_margin: 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_text: Color::BLACK, day_text_other_month: Color::from_rgb8(220, 220, 220), // day_background: Color::from_rgb8(230, 230, 255), day_weekend_bg: Color::from_rgb8(245, 245, 245), day_text_margin: 5.0, } } } //------------------------------------------------------------------------- pub struct CalendarMonthView { first_day: NaiveDate, first_day_in_view: NaiveDate, params: CalendarParams, weekday_on_first: Weekday, week_column_width: f32, week_column_font_size: f32, } impl CalendarMonthView { pub fn new(params: &CalendarParams, day: NaiveDate) -> Self { // first day of the month let first_day = if day.day() == 1 { day } else { NaiveDate::from_ymd(day.year(), day.month(), 1) }; // 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(), weekday_on_first, week_column_width: 30.0, week_column_font_size: 18.0, } } 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(day.year(), day.month(), 1) }; // 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.weekday_on_first = weekday_on_first; 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, 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}; let origin = bounds.position(); // font dimension let font_size = renderer.default_size() as f32; // dimensions of each box representing a day let w: f32 = bounds.width / 7.0; let h: f32 = bounds.height; let days_of_week = ["LUN", "MAR", "MER", "GIO", "VEN", "SAB", "DOM"]; for weekday in 0..7i32 { let bounds = Rectangle { x: (weekday as f32) * w + origin.x, y: origin.y, width: w, height: h }; // label (day letter on row 0, day number on the rest) let t = days_of_week[weekday as usize]; // 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::Text { content : t, size: font_size, bounds: Rectangle {x, y, ..bounds}, color: fg, font: Default::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, }); } } fn draw_week_column( &self, renderer: &mut impl text::Renderer, bounds: Rectangle, ) { // dimensions of each box representing a week number let h: f32 = bounds.height / 6.0; let r: f32 = if h > self.week_column_width { self.week_column_width } else { h } / 2.0; let mut day = self.first_day; for week in 0..6u32 { // where to place the week number let day_bounds = Rectangle { x: bounds.x, y: (week as f32) * h + bounds.y + self.params.day_text_margin, width: r * 2.0, height: r * 2.0 }; let week_of_first_day_of_month = day.iso_week().week(); day += Duration::weeks(1); // render week cell text renderer.fill_text(text::Text { content : &(week_of_first_day_of_month).to_string(), size: self.week_column_font_size, bounds: Rectangle { x: day_bounds.center_x(), y: day_bounds.y, ..day_bounds }, color: self.params.day_text, font: Default::default(), horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Top, }); } } fn draw_days( &self, renderer: &mut impl text::Renderer, bounds: Rectangle, ) { let size: Size = bounds.size(); let origin = bounds.position(); // font dimension let font_size = renderer.default_size() as f32; // dimensions of each box representing a day let w: f32 = size.width / 7.0; let h: f32 = size.height / 6.0; let mut current_day = self.first_day_in_view; for week in 0..6i32 { for weekday in 0..7i32 { let day_bounds = Rectangle { x: (weekday as f32) * w + origin.x, y: (week as f32) * h + origin.y, width: w, height: h }; // 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_text } else { self.params.day_text_other_month }; // background color of the day cell let bg_color = if current_day == Local::today().naive_local() { 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: Rectangle {width: day_bounds.width + 0.5, height: day_bounds.height + 0.5, ..day_bounds}, border_radius: 0.0, border_width: 1.0, border_color: self.params.day_text_other_month, }, bg_color); // render day cell text renderer.fill_text(text::Text { content, size: font_size, bounds: Rectangle {x, y, ..day_bounds}, color: fg, font: Default::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, }); current_day = current_day.succ(); } } } } // CalendarMonthView impl Widget for CalendarMonthView where Renderer: text::Renderer, { fn width(&self) -> Length { Length::Shrink } fn height(&self) -> Length { Length::Shrink } fn layout( &self, _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<'_>, _cursor_position: Point, _viewport: &Rectangle, ) { let size: Size = layout.bounds().size(); let origin = layout.bounds().position(); let margin: f32 = 20.0; // week column only visible if there is enough space let week_w = if self.params.show_weeks && size.width > self.week_column_width { self.week_column_width } else { 0.0 }; // font and header dimension let font_size = renderer.default_size() as f32; let first_row_h = font_size + margin; // header let x = origin.x; let y = origin.y; let width = size.width; let height = first_row_h; self.draw_header(renderer, Rectangle {x, y, width, height}, week_w); // week column if week_w > 0.0 { let x = origin.x; let y = origin.y + first_row_h; let width = self.week_column_width; let height = size.height - first_row_h; self.draw_week_column(renderer, Rectangle{x, y, width, height}); } // monthly calendar cells let x = origin.x + week_w; let y = origin.y + first_row_h; let width = size.width - week_w; let height = size.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) -> Self { Self::new(month_view) } } //------------------------------------------------------------------------- pub struct CalendarYearView { first_day: NaiveDate, first_day_in_view: NaiveDate, params: CalendarParams, weekday_on_first: Weekday, month_column_font_size: f32, margin: f32, } impl CalendarYearView { pub fn new(params: &CalendarParams, day: NaiveDate) -> Self { // first day of the year let first_day = NaiveDate::from_ymd(day.year(), 1, 1); // 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(), weekday_on_first, month_column_font_size: 24.0, margin: 10.0 } } pub fn set_year(&mut self, day: NaiveDate) { // first day of the year self.first_day = NaiveDate::from_ymd(day.year(), 1, 1); // weekday on first day of the year self.weekday_on_first = self.first_day.weekday(); // first visible day in the view self.first_day_in_view = self.first_day - Duration::days(self.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, 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}; let origin = bounds.position(); // font dimension let font_size = renderer.default_size() as f32; // dimensions of each box representing a day let w: f32 = bounds.width / (7.0 * 6.0); let h: f32 = bounds.height; let days_of_week = ["L", "M", "M", "G", "V", "S", "D"]; for col in 0..42i32 { let bounds = Rectangle { x: (col as f32) * w + origin.x, y: origin.y, width: w, height: h }; let weekday = (col 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, border_width: 1.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::Text { content : t, size: font_size, bounds: Rectangle {x, y, ..bounds}, color: fg, font: Default::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, }); } } 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::Text { content : MONTH_NAMES[month], size: self.month_column_font_size, bounds: Rectangle { x: month_name_bounds.x + self.margin, y: month_name_bounds.center_y(), ..month_name_bounds }, color: self.params.day_text, font: Default::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, }); } } fn draw_days( &self, renderer: &mut impl text::Renderer, bounds: Rectangle, ) { let size: Size = bounds.size(); let origin = bounds.position(); // font dimension let font_size = renderer.default_size() as f32; // dimensions of each box representing a day let w: f32 = size.width / 42.0; let h: f32 = size.height / 12.0; for current_day in self.first_day.iter_days() { if current_day.year() != self.first_day.year() { break; } let month = current_day.month0(); let weekday = current_day.weekday().num_days_from_monday(); let first_day_of_month = current_day.with_day0(0).unwrap().weekday().num_days_from_monday(); let monthday = current_day.day0() + first_day_of_month; let day_bounds = Rectangle { x: (monthday as f32) * w + origin.x, y: (month as f32) * h + origin.y, width: w, height: h }; // 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_text; // background color of the day cell let bg_color = if current_day == Local::today().naive_local() { 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: Rectangle {width: day_bounds.width + 0.5, height: day_bounds.height + 0.5, ..day_bounds}, border_radius: 0.0, border_width: 1.0, border_color: self.params.day_text_other_month, }, bg_color); // render day cell text renderer.fill_text(text::Text { content, size: font_size, bounds: Rectangle {x, y, ..day_bounds}, color: fg, font: Default::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, }); } } fn compute_month_col_width(&self, renderer: &mut impl text::Renderer) -> f32 { let mut max_max_font_width = 0.0; for month_name in MONTH_NAMES { let month_width = renderer.measure_width(month_name, self.month_column_font_size as u16, Default::default()); if month_width > max_max_font_width { max_max_font_width = month_width; } } return max_max_font_width; } } // CalendarYearView impl Widget for CalendarYearView where Renderer: text::Renderer, { fn width(&self) -> Length { Length::Shrink } fn height(&self) -> Length { Length::Shrink } fn layout( &self, _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<'_>, _cursor_position: Point, _viewport: &Rectangle, ) { let size: Size = layout.bounds().size(); let origin = layout.bounds().position(); let margin: f32 = 20.0; // 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) + self.margin } else { 0.0 }; // font and header dimension let font_size = renderer.default_size() as f32; let first_row_h = font_size + margin; // header let x = origin.x; let y = origin.y; let width = size.width; let height = first_row_h; self.draw_header(renderer, Rectangle {x, y, width, height}, month_w); // month column if month_w > 0.0 && size.width > month_w { let x = origin.x; let y = origin.y + first_row_h; let width = month_w; let height = size.height - first_row_h; self.draw_month_column(renderer, Rectangle{x, y, width, height}); } // monthly calendar cells let x = origin.x + month_w; let y = origin.y + first_row_h; let width = size.width - month_w; let height = size.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(year_view: CalendarYearView) -> Self { Self::new(year_view) } }