rs-calendar/src/ui/calendar.rs

700 lines
22 KiB
Rust

// 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<Message, Renderer> Widget<Message, Renderer> 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<CalendarMonthView> 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<Message, Renderer> Widget<Message, Renderer> 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<CalendarYearView> for Element<'a, Message, Renderer>
where
Renderer: text::Renderer,
{
fn from(year_view: CalendarYearView) -> Self {
Self::new(year_view)
}
}