use chrono::{Datelike, Days, Months, NaiveDate};
use datepicker::events::EventData;
use appcui_proc_macro::CustomControl;
const MINSPACE_FOR_SHORT_DATE: u32 = 15;
const MINSPACE_FOR_LONG_DATE: u32 = 18;
const MINSPACE_FOR_DROPBUTTON_DRAWING: u32 = 3;
const MINSPACE_FOR_DATE_DRAWING: u32 = 5;
const MIN_WIDTH_FOR_DATE_NAME: u32 = 6;
const CALENDAR_WIDTH: u32 = 30;
const CALENDAR_HEIGHT: u32 = 12;
enum DateSize {
Large,
Small,
VerySmall,
}
#[derive(PartialEq, Eq)]
enum HoveredDate {
DoubleLeftArrow,
LeftArrowYear,
RightArrowYear,
DoubleRightArrow,
LeftArrowMonth,
RightArrowMonth,
Day(u32),
None,
}
enum CharOrSpecialChar {
Regular(char),
Special(SpecialChar),
}
#[CustomControl(overwrite=OnPaint+OnDefaultAction+OnExpand+OnMouseEvent+OnKeyPressed, internal=true)]
pub struct DatePicker {
header_y_ofs: i32,
expanded_panel_y: i32,
selected_date: NaiveDate,
date_string: String,
hover_date: HoveredDate,
virtual_date: NaiveDate,
}
impl DatePicker {
const DAYS: [&'static str; 7] = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
const MONTHS: [&'static str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
pub fn with_date(date: NaiveDate, layout: Layout) -> Self {
let mut dp = DatePicker {
base: ControlBase::with_status_flags(layout, StatusFlags::Enabled | StatusFlags::Visible | StatusFlags::AcceptInput),
header_y_ofs: 0,
expanded_panel_y: 1,
selected_date: date,
date_string: Self::format_long_date(date),
hover_date: HoveredDate::None,
virtual_date: date,
};
dp.set_size_bounds(6, 1, u16::MAX, 1);
let date_len = dp.get_date_size();
match date_len {
DateSize::Large => {}
DateSize::Small => {
dp.date_string = Self::format_short_date(date);
}
DateSize::VerySmall => {
dp.date_string = Self::format_very_short_date(date);
}
}
dp
}
pub fn new(date_str: &str, layout: Layout) -> Self {
let date = date_str.parse::<NaiveDate>().unwrap();
Self::with_date(date, layout)
}
pub fn set_date_str(&mut self, date_str: &str) {
self.selected_date = date_str.parse::<NaiveDate>().unwrap();
self.date_string = Self::format_long_date(self.selected_date);
}
pub fn set_date(&mut self, date: NaiveDate) {
self.selected_date = date;
self.date_string = Self::format_long_date(date);
}
#[inline(always)]
pub fn date(&self) -> NaiveDate {
self.selected_date
}
fn update_date(&mut self, date: NaiveDate) {
if date != self.selected_date {
self.selected_date = date;
self.date_string = Self::format_long_date(date);
self.raise_event(ControlEvent {
emitter: self.handle,
receiver: self.event_processor,
data: ControlEventData::DatePicker(EventData { date: self.selected_date }),
});
}
}
fn jump_to_month(date: NaiveDate, target_month: u32) -> NaiveDate {
let year = date.year();
let day = date.day();
let mut new_date = NaiveDate::from_ymd_opt(year, target_month, 1).unwrap();
let last_day_of_month = (1..=31)
.rev()
.find(|&d| NaiveDate::from_ymd_opt(year, target_month, d).is_some())
.unwrap();
new_date = new_date.with_day(day.min(last_day_of_month)).unwrap();
new_date
}
fn mouse_over_calendar(&mut self, x: i32, y: i32) -> HoveredDate {
if !self.is_expanded() {
return HoveredDate::None;
}
if y == 1 + self.expanded_panel_y {
if x == 5 {
return HoveredDate::LeftArrowYear;
}
if x == 12 {
return HoveredDate::RightArrowYear;
}
if x == 2 || x == 3 {
return HoveredDate::DoubleLeftArrow;
}
if x == 14 || x == 15 {
return HoveredDate::DoubleRightArrow;
}
if x == 20 {
return HoveredDate::LeftArrowMonth;
}
if x == 26 {
return HoveredDate::RightArrowMonth;
}
}
let mut col = self.get_first_day_index() * 4 + 3;
let mut row = 4 + self.expanded_panel_y;
let last_day = self.days_in_month() as i32;
for i in 0..last_day {
let day = i + 1;
if (y == row) && (x == col || x == (col - 1)) {
self.virtual_date = self.virtual_date.with_day(day as u32).unwrap();
return HoveredDate::Day(day as u32);
}
col += 4;
if col >= 30 {
col = 3;
row += 1;
}
}
HoveredDate::None
}
fn format_very_short_date(selected_date: NaiveDate) -> String {
selected_date.format("%d.%m.%y").to_string()
}
fn format_short_date(selected_date: NaiveDate) -> String {
selected_date.format("%d.%m.%Y").to_string()
}
fn format_long_date(selected_date: NaiveDate) -> String {
selected_date.format("%Y, %b, %d").to_string()
}
fn get_date_size(&self) -> DateSize {
let width: u32 = self.size().width;
if width > MINSPACE_FOR_LONG_DATE {
DateSize::Large
} else if width > MINSPACE_FOR_SHORT_DATE {
DateSize::Small
} else {
DateSize::VerySmall
}
}
fn get_printed_chars(&self) -> u32 {
let wdth = self.size().width - MIN_WIDTH_FOR_DATE_NAME;
if (self.date_string.len() as u32) > wdth {
wdth
} else {
self.date_string.len() as u32
}
}
fn days_in_month(&self) -> u32 {
let month = self.virtual_date.month();
let year = self.virtual_date.year();
let next_month = if month == 12 { 1 } else { month + 1 };
let next_month_year = if month == 12 { year + 1 } else { year };
let first_of_next_month = NaiveDate::from_ymd_opt(next_month_year, next_month, 1).unwrap();
let first_of_current_month = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
first_of_next_month.signed_duration_since(first_of_current_month).num_days() as u32
}
fn get_first_day_index(&self) -> i32 {
let first_day = self.virtual_date.with_day(1).unwrap().format("%a").to_string();
for i in 0..DatePicker::DAYS.len() {
if first_day.starts_with(DatePicker::DAYS[i]) {
return i as i32;
}
}
0
}
}
impl OnPaint for DatePicker {
fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
let size = self.size();
let col_text = match () {
_ if !self.is_enabled() => theme.button.regular.text.inactive,
_ if self.has_focus() => theme.button.regular.text.focused,
_ if self.is_mouse_over() => theme.button.regular.text.hovered,
_ => theme.button.regular.text.normal,
};
let space_char = Character::with_attributes(' ', col_text);
let date_size = self.get_date_size();
let mut format = TextFormatBuilder::new()
.position(1, self.header_y_ofs)
.attribute(col_text)
.align(TextAlignment::Left)
.wrap_type(WrapType::SingleLineWrap((size.width - MIN_WIDTH_FOR_DATE_NAME) as u16))
.build();
surface.fill_horizontal_line(0, self.header_y_ofs, (size.width - MINSPACE_FOR_DATE_DRAWING) as i32, space_char);
let mut buf: [u8; 16] = [0; 16];
match date_size {
DateSize::Large => {
if let Some(txt) = crate::utils::FormatDate::normal(&self.selected_date, &mut buf) {
format.set_chars_count(txt.len() as u16);
surface.write_text(txt, &format);
}
}
DateSize::Small => {
if let Some(txt) = crate::utils::FormatDate::dmy(&self.selected_date, &mut buf, b'.') {
format.set_chars_count(txt.len() as u16);
surface.write_text(txt, &format);
}
}
DateSize::VerySmall => {
if let Some(txt) = crate::utils::FormatDate::short(&self.selected_date, &mut buf, b'.') {
format.set_chars_count(txt.len() as u16);
surface.write_text(txt, &format);
}
}
}
if size.width >= MINSPACE_FOR_DROPBUTTON_DRAWING {
let px = (size.width - MINSPACE_FOR_DROPBUTTON_DRAWING) as i32;
surface.fill_horizontal_line_with_size(px, self.header_y_ofs, 3, space_char);
surface.write_char(px + 1, self.header_y_ofs, Character::with_attributes(SpecialChar::TriangleDown, col_text));
}
if self.is_expanded() {
let size = self.expanded_size();
let col = theme.menu.text.normal;
let space_char = Character::with_attributes(' ', col);
surface.fill_rect(
Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
space_char,
);
surface.draw_rect(
Rect::with_size(0, self.expanded_panel_y, size.width as u16, (size.height - 1) as u16),
LineType::Single,
col,
);
surface.draw_horizontal_line(1, 2 + self.expanded_panel_y, (CALENDAR_WIDTH - 1) as i32, LineType::SingleRound, col);
surface.write_char(0, 2 + self.expanded_panel_y, Character::with_attributes(SpecialChar::BoxMidleLeft, col));
surface.write_char(
(CALENDAR_WIDTH - 1) as i32,
2 + self.expanded_panel_y,
Character::with_attributes(SpecialChar::BoxMidleRight, col),
);
let year = self.virtual_date.year();
let p_y = 1 + self.expanded_panel_y;
surface.write_char(7, p_y, Character::with_attributes(((year / 1000) + 48) as u8, col));
surface.write_char(8, p_y, Character::with_attributes(((year / 100) % 10 + 48) as u8, col));
surface.write_char(9, p_y, Character::with_attributes(((year / 10) % 10 + 48) as u8, col));
surface.write_char(10, p_y, Character::with_attributes((year % 10 + 48) as u8, col));
fn set_char(surface: &mut Surface, x: i32, y: i32, char_or_special: CharOrSpecialChar, condition: bool, theme: &Theme) {
let attr = if condition { theme.menu.text.hovered } else { theme.menu.text.normal };
let character = match char_or_special {
CharOrSpecialChar::Regular(c) => Character::with_attributes(c, attr),
CharOrSpecialChar::Special(sc) => Character::with_attributes(sc, attr),
};
surface.write_char(x, y, character);
}
let y_pos = 1 + self.expanded_panel_y;
set_char(
surface,
5,
y_pos,
CharOrSpecialChar::Special(SpecialChar::TriangleLeft),
self.hover_date == HoveredDate::LeftArrowYear,
theme,
);
set_char(
surface,
12,
y_pos,
CharOrSpecialChar::Special(SpecialChar::TriangleRight),
self.hover_date == HoveredDate::RightArrowYear,
theme,
);
set_char(
surface,
2,
y_pos,
CharOrSpecialChar::Regular('<'),
self.hover_date == HoveredDate::DoubleLeftArrow,
theme,
);
set_char(
surface,
3,
y_pos,
CharOrSpecialChar::Regular('<'),
self.hover_date == HoveredDate::DoubleLeftArrow,
theme,
);
set_char(
surface,
14,
y_pos,
CharOrSpecialChar::Regular('>'),
self.hover_date == HoveredDate::DoubleRightArrow,
theme,
);
set_char(
surface,
15,
y_pos,
CharOrSpecialChar::Regular('>'),
self.hover_date == HoveredDate::DoubleRightArrow,
theme,
);
set_char(
surface,
20,
y_pos,
CharOrSpecialChar::Special(SpecialChar::TriangleLeft),
self.hover_date == HoveredDate::LeftArrowMonth,
theme,
);
set_char(
surface,
26,
y_pos,
CharOrSpecialChar::Special(SpecialChar::TriangleRight),
self.hover_date == HoveredDate::RightArrowMonth,
theme,
);
let month = Self::MONTHS[self.virtual_date.month0() as usize];
surface.write_ascii(22, 1 + self.expanded_panel_y, month.as_bytes(), col, false);
let mut x = 2;
for i in 0..7 {
surface.write_ascii(
x,
3 + self.expanded_panel_y,
DatePicker::DAYS[i].as_bytes(),
theme.menu.text.inactive,
false,
);
x += 4;
}
let mut day_row = 4 + self.expanded_panel_y;
let mut day_col = self.get_first_day_index() * 4 + 3;
let last_day = self.days_in_month();
for i in 0..last_day {
let day = i + 1;
surface.write_char(day_col, day_row, Character::with_attributes((day % 10 + 48) as u8 as char, col));
if day > 9 {
surface.write_char(day_col - 1, day_row, Character::with_attributes((day / 10 + 48) as u8 as char, col));
}
if day == self.selected_date.day()
&& self.selected_date.month() == self.virtual_date.month()
&& self.selected_date.year() == self.virtual_date.year()
{
surface.fill_horizontal_line_with_size(
day_col - 2,
day_row,
4,
Character::with_attributes(0, theme.menu.text.pressed_or_selected),
);
} else if self.virtual_date.day() == day {
surface.fill_horizontal_line_with_size(day_col - 2, day_row, 4, Character::with_attributes(0, theme.menu.text.hovered));
}
day_col += 4;
if day_col >= 30 {
day_col = 3;
day_row += 1;
}
}
}
}
}
impl OnDefaultAction for DatePicker {
fn on_default_action(&mut self) {
if self.is_expanded() {
self.pack();
} else {
self.expand(Size::new(CALENDAR_WIDTH, CALENDAR_HEIGHT), Size::new(CALENDAR_WIDTH, CALENDAR_HEIGHT));
}
}
}
impl OnExpand for DatePicker {
fn on_expand(&mut self, direction: ExpandedDirection) {
self.virtual_date = self.selected_date;
match direction {
ExpandedDirection::OnTop => {
self.expanded_panel_y = 0;
self.header_y_ofs = (self.expanded_size().height as i32) - 1;
}
ExpandedDirection::OnBottom => {
self.expanded_panel_y = 1;
self.header_y_ofs = 0;
}
}
self.hover_date = HoveredDate::None;
}
fn on_pack(&mut self) {
self.expanded_panel_y = 1;
self.header_y_ofs = 0;
}
}
impl OnMouseEvent for DatePicker {
fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
match event {
MouseEvent::Enter => {
if !self.is_expanded() && (self.date_string.len() as i32) > (self.get_printed_chars() as i32) {
self.show_tooltip(self.date_string.as_str())
}
EventProcessStatus::Processed
}
MouseEvent::Leave => {
self.hide_tooltip();
EventProcessStatus::Processed
}
MouseEvent::Over(p) => {
let hd = self.mouse_over_calendar(p.x, p.y);
if hd != self.hover_date {
self.hover_date = hd;
}
EventProcessStatus::Processed
}
MouseEvent::Pressed(data) => {
let hd = self.mouse_over_calendar(data.x, data.y);
if hd != HoveredDate::None {
match hd {
HoveredDate::DoubleLeftArrow => {
self.virtual_date = self.virtual_date - Months::new(120);
}
HoveredDate::LeftArrowYear => {
self.virtual_date = self.virtual_date - Months::new(12);
}
HoveredDate::RightArrowYear => {
self.virtual_date = self.virtual_date + Months::new(12);
}
HoveredDate::DoubleRightArrow => {
self.virtual_date = self.virtual_date + Months::new(120);
}
HoveredDate::LeftArrowMonth => {
self.virtual_date = self.virtual_date - Months::new(1);
}
HoveredDate::RightArrowMonth => {
self.virtual_date = self.virtual_date + Months::new(1);
}
HoveredDate::Day(day) => {
self.update_date(self.virtual_date.with_day(day).unwrap());
self.on_default_action();
}
_ => {}
}
return EventProcessStatus::Processed;
}
self.on_default_action();
EventProcessStatus::Processed
}
_ => EventProcessStatus::Ignored,
}
}
}
impl OnKeyPressed for DatePicker {
fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
let expanded = self.is_expanded();
match key.value() {
key!("F") | key!("S") | key!("O") | key!("N") | key!("D") => {
let month = match key.value() {
key!("F") => 2,
key!("S") => 9,
key!("O") => 10,
key!("N") => 11,
key!("D") => 12,
_ => unreachable!(),
};
if expanded {
self.virtual_date = Self::jump_to_month(self.virtual_date, month);
} else {
self.update_date(Self::jump_to_month(self.selected_date, month));
}
return EventProcessStatus::Processed;
}
key!("J") | key!("A") | key!("M") | key!("Shift+J") | key!("Shift+A") | key!("Shift+M") => {
let mut val = 1i32;
let month_char = match key.value() {
key!("J") => "J",
key!("A") => "A",
key!("M") => "M",
key!("Shift+J") => {
val = -1;
"J"
}
key!("Shift+A") => {
val = -1;
"A"
}
key!("Shift+M") => {
val = -1;
"M"
}
_ => unreachable!(),
};
let target_month: &mut NaiveDate = if expanded { &mut self.virtual_date } else { &mut self.selected_date };
let month = {
let mut current_month = target_month.month() as i32 + val;
if current_month > 12 {
current_month = 1;
}
if current_month < 1 {
current_month = 12;
}
for _ in 0..Self::MONTHS.len() {
if Self::MONTHS[(current_month - 1) as usize].starts_with(month_char) {
break;
} else {
current_month += val;
if current_month > 12 {
current_month = 1;
}
if current_month < 1 {
current_month = 12;
}
}
}
current_month
};
if expanded {
self.virtual_date = Self::jump_to_month(self.virtual_date, month as u32);
} else {
self.update_date(Self::jump_to_month(self.selected_date, month as u32));
}
return EventProcessStatus::Processed;
}
_ => {}
}
if !expanded {
match key.value() {
key!("Escape") => {
return EventProcessStatus::Ignored;
}
key!("Up") => {
self.update_date(self.selected_date + Days::new(1));
return EventProcessStatus::Processed;
}
key!("Down") => {
self.update_date(self.selected_date - Days::new(1));
return EventProcessStatus::Processed;
}
key!("Shift+Up") => {
self.update_date(self.selected_date + Months::new(1));
return EventProcessStatus::Processed;
}
key!("Shift+Down") => {
self.update_date(self.selected_date - Months::new(1));
return EventProcessStatus::Processed;
}
key!("Ctrl+Up") => {
self.update_date(self.selected_date + Months::new(12));
return EventProcessStatus::Processed;
}
key!("Ctrl+Down") => {
self.update_date(self.selected_date - Months::new(12));
return EventProcessStatus::Processed;
}
key!("Ctrl+Shift+Up") => {
self.update_date(self.selected_date + Months::new(120));
return EventProcessStatus::Processed;
}
key!("Ctrl+Shift+Down") => {
self.update_date(self.selected_date - Months::new(120));
return EventProcessStatus::Processed;
}
key!("Enter") | key!("Space") => {
self.on_default_action();
return EventProcessStatus::Processed;
}
_ => {}
}
EventProcessStatus::Ignored
} else {
match key.value() {
key!("Escape") => {
self.pack();
return EventProcessStatus::Processed;
}
key!("Up") => {
self.virtual_date = self.virtual_date - Days::new(7);
return EventProcessStatus::Processed;
}
key!("Down") => {
self.virtual_date = self.virtual_date + Days::new(7);
return EventProcessStatus::Processed;
}
key!("Left") => {
self.virtual_date = self.virtual_date - Days::new(1);
return EventProcessStatus::Processed;
}
key!("Right") => {
self.virtual_date = self.virtual_date + Days::new(1);
return EventProcessStatus::Processed;
}
key!("Shift+Left") => {
self.virtual_date = self.virtual_date - Months::new(1);
return EventProcessStatus::Processed;
}
key!("Shift+Right") => {
self.virtual_date = self.virtual_date + Months::new(1);
return EventProcessStatus::Processed;
}
key!("Ctrl+Left") => {
self.virtual_date = self.virtual_date - Months::new(12);
return EventProcessStatus::Processed;
}
key!("Ctrl+Right") => {
self.virtual_date = self.virtual_date + Months::new(12);
return EventProcessStatus::Processed;
}
key!("Ctrl+Shift+Left") => {
self.virtual_date = self.virtual_date - Months::new(120);
return EventProcessStatus::Processed;
}
key!("Ctrl+Shift+Right") => {
self.virtual_date = self.virtual_date + Months::new(120);
return EventProcessStatus::Processed;
}
key!("Enter") => {
self.update_date(self.virtual_date);
self.on_default_action();
return EventProcessStatus::Processed;
}
_ => {}
}
EventProcessStatus::Ignored
}
}
}