1use super::super::{Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
2use crate::color::{Color, TEXT_INVERTED_HARD, TEXT_NORMAL};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::fl;
6use crate::font::{font_from_style, Fonts, DISPLAY_FONT_SIZE, FONT_SIZES, NORMAL_STYLE};
7use crate::framebuffer::Framebuffer;
8use crate::geom::{CornerSpec, Point, Rectangle};
9use chrono::{Datelike, Local, NaiveDate, Timelike};
10use std::time::Duration;
11use tracing::debug;
12
13fn month_name(month: u32) -> String {
15 match month {
16 1 => fl!("calendar-month-january"),
17 2 => fl!("calendar-month-february"),
18 3 => fl!("calendar-month-march"),
19 4 => fl!("calendar-month-april"),
20 5 => fl!("calendar-month-may"),
21 6 => fl!("calendar-month-june"),
22 7 => fl!("calendar-month-july"),
23 8 => fl!("calendar-month-august"),
24 9 => fl!("calendar-month-september"),
25 10 => fl!("calendar-month-october"),
26 11 => fl!("calendar-month-november"),
27 12 => fl!("calendar-month-december"),
28 _ => String::new(),
29 }
30}
31
32fn short_month_name(month: u32) -> String {
34 match month {
35 1 => fl!("calendar-month-short-jan"),
36 2 => fl!("calendar-month-short-feb"),
37 3 => fl!("calendar-month-short-mar"),
38 4 => fl!("calendar-month-short-apr"),
39 5 => fl!("calendar-month-short-may"),
40 6 => fl!("calendar-month-short-jun"),
41 7 => fl!("calendar-month-short-jul"),
42 8 => fl!("calendar-month-short-aug"),
43 9 => fl!("calendar-month-short-sep"),
44 10 => fl!("calendar-month-short-oct"),
45 11 => fl!("calendar-month-short-nov"),
46 12 => fl!("calendar-month-short-dec"),
47 _ => String::new(),
48 }
49}
50
51fn weekday_name(weekday: usize) -> String {
53 match weekday {
54 0 => fl!("calendar-weekday-mon"),
55 1 => fl!("calendar-weekday-tue"),
56 2 => fl!("calendar-weekday-wed"),
57 3 => fl!("calendar-weekday-thu"),
58 4 => fl!("calendar-weekday-fri"),
59 5 => fl!("calendar-weekday-sat"),
60 6 => fl!("calendar-weekday-sun"),
61 _ => String::new(),
62 }
63}
64
65pub(super) struct CalendarView {
71 id: Id,
72 rect: Rectangle,
73 children: Vec<Box<dyn View>>,
74 minutes_until_poweroff: Option<i64>,
75 halt: bool,
77}
78
79impl CalendarView {
80 pub(super) fn new(rect: Rectangle, minutes_until_poweroff: Option<i64>, halt: bool) -> Self {
81 CalendarView {
82 id: ID_FEEDER.next(),
83 rect,
84 children: Vec::new(),
85 minutes_until_poweroff,
86 halt,
87 }
88 }
89}
90
91fn days_in_month(year: i32, month: u32) -> i32 {
93 let (next_year, next_month) = if month == 12 {
94 (year + 1, 1)
95 } else {
96 (year, month + 1)
97 };
98 NaiveDate::from_ymd_opt(next_year, next_month, 1)
99 .and_then(|d| d.pred_opt())
100 .map(|d| d.day() as i32)
101 .unwrap_or(30)
102}
103
104fn month_start_weekday(year: i32, month: u32) -> i32 {
107 NaiveDate::from_ymd_opt(year, month, 1)
108 .map(|d| d.weekday().num_days_from_monday() as i32)
109 .unwrap_or(0)
110}
111
112fn days_in_prev_month(year: i32, month: u32) -> i32 {
114 let (prev_year, prev_month) = if month == 1 {
115 (year - 1, 12)
116 } else {
117 (year, month - 1)
118 };
119 days_in_month(prev_year, prev_month)
120}
121
122fn color_or(is_color: bool, r: u8, g: u8, b: u8, fallback: Color) -> Color {
125 if is_color {
126 Color::Rgb(r, g, b)
127 } else {
128 fallback
129 }
130}
131
132impl View for CalendarView {
133 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
134 fn handle_event(
135 &mut self,
136 _evt: &Event,
137 _hub: &Hub,
138 _bus: &mut Bus,
139 _rq: &mut RenderQueue,
140 _context: &mut Context,
141 ) -> bool {
142 false
143 }
144
145 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, fb, fonts), fields(rect = ?_rect)))]
146 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
147 let scheme = if self.halt {
148 TEXT_INVERTED_HARD
149 } else {
150 TEXT_NORMAL
151 };
152
153 fb.draw_rectangle(&self.rect, scheme[0]);
154
155 let now = Local::now();
156 debug!(timestamp = %now, "Rendering calendar view");
157
158 let year = now.year();
159 let month = now.month();
160 let today = now.day() as i32;
161
162 let is_color = CURRENT_DEVICE.color_samples() > 1;
163 let month_color = color_or(is_color, 180, 60, 40, scheme[1]);
164 let today_bg = color_or(is_color, 70, 100, 150, scheme[1]);
165
166 let dpi = CURRENT_DEVICE.dpi;
167 let screen_width = self.rect.width() as i32;
168 let screen_height = self.rect.height() as i32;
169
170 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
171 font.set_size(DISPLAY_FONT_SIZE * 2, dpi);
172 let time_str = format!("{:02}:{:02}", now.hour(), now.minute());
173 let time_plan = font.plan(&time_str, None, None);
174 let time_x = (screen_width - time_plan.width) / 2;
175 let time_cap_height = font.x_heights.1 as i32;
176 let time_y = screen_height * 20 / 100;
177 font.render(fb, scheme[1], &time_plan, Point::new(time_x, time_y));
178
179 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
180 let normal_line_height = font.x_heights.0 as i32 * 2;
181 let short_month = short_month_name(month);
182 let weekday = weekday_name(now.weekday().num_days_from_monday() as usize);
183 let day_str = format!("{:02}", today);
184 let year_str = year.to_string();
185 let date_str = fl!(
186 "calendar-date-line",
187 day = day_str.as_str(),
188 month = short_month.as_str(),
189 year = year_str.as_str(),
190 weekday = weekday.as_str()
191 );
192 let date_plan = font.plan(&date_str, None, None);
193 let date_x = (screen_width - date_plan.width) / 2;
194 let date_y = time_y + time_cap_height + normal_line_height;
195 font.render(fb, scheme[1], &date_plan, Point::new(date_x, date_y));
196
197 let after_header_y = if let Some(minutes) = self.minutes_until_poweroff.filter(|&m| m > 0) {
198 let duration = Duration::from_secs((minutes * 60).max(0) as u64);
199 let duration_str = humantime::format_duration(duration).to_string();
200 let poweroff_str = fl!("calendar-poweroff", duration = duration_str.as_str());
201 let poweroff_plan = font.plan(&poweroff_str, None, None);
202 let poweroff_x = (screen_width - poweroff_plan.width) / 2;
203 let poweroff_y = date_y + normal_line_height;
204 font.render(
205 fb,
206 scheme[1],
207 &poweroff_plan,
208 Point::new(poweroff_x, poweroff_y),
209 );
210 poweroff_y + normal_line_height
211 } else {
212 date_y + normal_line_height
213 };
214
215 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
216 font.set_size(FONT_SIZES[1] * 2, dpi);
217 let full_month = month_name(month);
218 let month_plan = font.plan(&full_month, None, None);
219 let month_x = (screen_width - month_plan.width) / 2;
220 let calendar_top = (screen_height * 62 / 100).max(after_header_y + normal_line_height);
223 let month_cap_height = font.x_heights.1 as i32;
224 font.render(
225 fb,
226 month_color,
227 &month_plan,
228 Point::new(month_x, calendar_top),
229 );
230
231 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
232 let separator_y = calendar_top + month_cap_height + normal_line_height / 2;
233 fb.draw_segment(
234 pt!(self.rect.min.x, separator_y),
235 pt!(self.rect.max.x, separator_y),
236 0.5,
237 0.5,
238 scheme[1],
239 );
240
241 let cell_width = screen_width / 7;
242 let header_y = separator_y + normal_line_height;
243 for i in 0..7usize {
244 let name = weekday_name(i);
245 let plan = font.plan(&name, None, None);
246 let dx = (cell_width - plan.width) / 2 + (i as i32 * cell_width);
247 font.render(fb, scheme[1], &plan, Point::new(dx, header_y));
248 }
249
250 let x_height = font.x_heights.0 as i32;
251 let cell_height = normal_line_height + x_height / 2;
252 let days_start_y = header_y + cell_height;
253
254 let starting_weekday = month_start_weekday(year, month);
255 let current_month_days = days_in_month(year, month);
256 let prev_month_days = days_in_prev_month(year, month);
257
258 for slot in 0..(6 * 7) {
259 let week = slot / 7;
260 let weekday = slot % 7;
261 let cell_x = weekday * cell_width;
262 let cell_y = days_start_y + week * cell_height;
263
264 let (day_num, is_current_month) = if slot < starting_weekday {
265 (prev_month_days - starting_weekday + slot + 1, false)
266 } else {
267 let d = slot - starting_weekday + 1;
268 if d <= current_month_days {
269 (d, true)
270 } else {
271 (d - current_month_days, false)
272 }
273 };
274
275 let day_str = format!("{:02}", day_num);
276 let plan = font.plan(&day_str, None, None);
277 let dx = (cell_width - plan.width) / 2 + cell_x;
278
279 if is_current_month && day_num == today {
280 let pad = x_height / 3;
281 let highlight = Rectangle::new(
285 pt!(cell_x + pad, cell_y - x_height - pad),
286 pt!(cell_x + cell_width - pad, cell_y + pad),
287 );
288 fb.draw_rounded_rectangle(&highlight, &CornerSpec::Uniform(4), today_bg);
289 font.render(fb, scheme[0], &plan, Point::new(dx, cell_y));
290 } else {
291 let color = if is_current_month {
292 scheme[1]
293 } else {
294 scheme[2]
295 };
296 font.render(fb, color, &plan, Point::new(dx, cell_y));
297 }
298 }
299 }
300
301 fn rect(&self) -> &Rectangle {
302 &self.rect
303 }
304
305 fn rect_mut(&mut self) -> &mut Rectangle {
306 &mut self.rect
307 }
308
309 fn children(&self) -> &Vec<Box<dyn View>> {
310 &self.children
311 }
312
313 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
314 &mut self.children
315 }
316
317 fn id(&self) -> Id {
318 self.id
319 }
320}