use super::calendar::Calendar;
use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::shortcut::Key;
use crate::signal::{GenericSignal, Signal1};
use crate::undo::{UndoStack, ValueCommand};
use crate::widget::capability::access::{date_to_string, weekday_to_str};
use crate::widget::capability::coercion::{
expect_bool, expect_date, expect_horizontal_alignment, expect_string, expect_weekday,
horizontal_alignment_to_str,
};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::input_widgets::shortcut_editor::primary_shortcut_pressed;
use crate::widget::metrics::{dimensions, effective_font};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use chrono::{Datelike, NaiveDate, Weekday};
use std::cell::RefCell;
use std::fmt::Write as _;
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Date {
year: i32,
month: u8, day: u8, }
impl Date {
pub fn new(year: i32, month: u8, day: u8) -> Self {
Self { year, month, day }
}
pub fn today() -> Self {
Self { year: 2024, month: 1, day: 1 }
}
pub fn year(&self) -> i32 {
self.year
}
pub fn month(&self) -> u8 {
self.month
}
pub fn day(&self) -> u8 {
self.day
}
pub fn set_year(&mut self, year: i32) {
self.year = year;
}
pub fn set_month(&mut self, month: u8) {
self.month = month.clamp(1, 12);
}
pub fn set_day(&mut self, day: u8) {
self.day = day.clamp(1, 31);
}
pub fn days_in_month(&self) -> u8 {
match self.month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if self.is_leap_year() {
29
} else {
28
}
}
_ => 30,
}
}
pub fn is_leap_year(&self) -> bool {
(self.year % 4 == 0 && self.year % 100 != 0) || (self.year % 400 == 0)
}
pub fn is_valid(&self) -> bool {
self.month >= 1 && self.month <= 12 && self.day >= 1 && self.day <= self.days_in_month()
}
pub fn weekday(&self) -> u8 {
let month = self.month.clamp(1, 12) as usize;
const T: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let t = T[month - 1];
let year = if self.month < 3 { self.year - 1 } else { self.year };
((year + year / 4 - year / 100 + year / 400 + t + self.day as i32).rem_euclid(7)) as u8
}
}
impl std::fmt::Display for Date {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
pub(crate) fn format_with_pattern_into(
pattern: &str,
components: DateTimeComponents,
out: &mut String,
) -> bool {
type Token = (&'static str, fn(&DateTimeComponents, &mut String) -> bool);
const TOKENS: [Token; 16] = [
("hh", |c, out| match c.hour {
Some(h) => {
let hour12 = if h % 12 == 0 { 12 } else { h % 12 };
let _ = write!(out, "{hour12:02}");
true
}
None => false,
}),
("h", |c, out| match c.hour {
Some(h) => {
let hour12 = if h % 12 == 0 { 12 } else { h % 12 };
let _ = write!(out, "{hour12}");
true
}
None => false,
}),
("zzz", |c, out| match c.millisecond {
Some(ms) => {
let _ = write!(out, "{ms:03}");
true
}
None => false,
}),
("AP", |c, out| match c.hour {
Some(h) => {
out.push_str(if h < 12 { "AM" } else { "PM" });
true
}
None => false,
}),
("ap", |c, out| match c.hour {
Some(h) => {
out.push_str(if h < 12 { "am" } else { "pm" });
true
}
None => false,
}),
("yyyy", |c, out| match c.year {
Some(y) => {
let _ = write!(out, "{y:04}");
true
}
None => false,
}),
("SSS", |c, out| match c.millisecond {
Some(ms) => {
let _ = write!(out, "{ms:03}");
true
}
None => false,
}),
("yy", |c, out| match c.year {
Some(y) => {
let _ = write!(out, "{:02}", y.rem_euclid(100));
true
}
None => false,
}),
("MM", |c, out| match c.month {
Some(m) => {
let _ = write!(out, "{m:02}");
true
}
None => false,
}),
("dd", |c, out| match c.day {
Some(d) => {
let _ = write!(out, "{d:02}");
true
}
None => false,
}),
("HH", |c, out| match c.hour {
Some(h) => {
let _ = write!(out, "{h:02}");
true
}
None => false,
}),
("mm", |c, out| match c.minute {
Some(m) => {
let _ = write!(out, "{m:02}");
true
}
None => false,
}),
("ss", |c, out| match c.second {
Some(s) => {
let _ = write!(out, "{s:02}");
true
}
None => false,
}),
("M", |c, out| match c.month {
Some(m) => {
let _ = write!(out, "{m}");
true
}
None => false,
}),
("d", |c, out| match c.day {
Some(d) => {
let _ = write!(out, "{d}");
true
}
None => false,
}),
("H", |c, out| match c.hour {
Some(h) => {
let _ = write!(out, "{h}");
true
}
None => false,
}),
];
let mut substituted = false;
let mut rest = pattern;
let mut at_token_start = true;
'scan: while !rest.is_empty() {
if at_token_start {
for (token, render) in TOKENS {
if let Some(after) = rest.strip_prefix(token) {
if render(&components, out) {
substituted = true;
}
rest = after;
at_token_start = true;
continue 'scan;
}
}
}
let Some(ch) = rest.chars().next() else {
break;
};
out.push(ch);
rest = &rest[ch.len_utf8()..];
at_token_start = !ch.is_alphabetic();
}
substituted
}
pub(crate) fn format_with_pattern(pattern: &str, components: DateTimeComponents) -> Option<String> {
let mut out = crate::compat::String::new();
if format_with_pattern_into(pattern, components, &mut out) {
Some(out)
} else {
None
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct DateTimeComponents {
pub year: Option<i32>,
pub month: Option<u8>,
pub day: Option<u8>,
pub hour: Option<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,
pub millisecond: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateSection {
Year,
Month,
Day,
}
impl DateSection {
pub const fn next(self) -> Self {
match self {
Self::Year => Self::Month,
Self::Month => Self::Day,
Self::Day => Self::Year,
}
}
pub const fn previous(self) -> Self {
match self {
Self::Year => Self::Day,
Self::Month => Self::Year,
Self::Day => Self::Month,
}
}
const fn digit_capacity(self) -> usize {
match self {
Self::Year => 4,
_ => 2,
}
}
}
pub struct DateEdit {
base: BaseWidget,
date: Date,
minimum: Date,
maximum: Date,
display_format: String,
calendar_popup: bool,
calendar_open: bool,
pub popup_visibility_changed: Signal1<bool>,
alignment: crate::core::Alignment,
pub date_changed: Signal1<Date>,
undo_stack: UndoStack,
history_target: Rc<RefCell<Date>>,
restoring_history: bool,
current_section: Option<DateSection>,
digit_buffer: String,
spin_buttons: bool,
calendar_reveal: crate::widget::menu_toolbar::popup_reveal::PopupReveal,
special_value_text: Option<String>,
calendar: Calendar,
calendar_first_day: Weekday,
pub calendar_opened: GenericSignal,
pub calendar_closed: GenericSignal,
pub editing_finished: GenericSignal,
calendar_sync_dirty: bool,
calendar_sync_rect: Rect,
}
impl DateEdit {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::DatePicker, geometry, "DateEdit"),
date: Date::today(),
minimum: Date::new(1752, 9, 14),
maximum: Date::new(9999, 12, 31),
display_format: "yyyy-MM-dd".to_string(),
calendar_popup: false,
calendar_open: false,
alignment: crate::core::Alignment::Left,
date_changed: Signal1::new(),
popup_visibility_changed: Signal1::new(),
undo_stack: UndoStack::new(),
history_target: Rc::new(RefCell::new(Date::today())),
restoring_history: false,
current_section: None,
digit_buffer: String::new(),
spin_buttons: false,
calendar_reveal: crate::widget::menu_toolbar::popup_reveal::PopupReveal::new(false),
special_value_text: None,
calendar: {
let mut calendar = Calendar::new(calendar_popup_rect(geometry));
calendar.set_first_day_of_week(Weekday::Sun);
calendar
},
calendar_first_day: Weekday::Sun,
calendar_opened: GenericSignal::new(),
calendar_closed: GenericSignal::new(),
editing_finished: GenericSignal::new(),
calendar_sync_dirty: true,
calendar_sync_rect: Rect::new(0, 0, 0, 0),
}
}
pub fn date(&self) -> Date {
self.date
}
pub fn alignment(&self) -> crate::core::Alignment {
self.alignment
}
pub fn set_alignment(&mut self, alignment: crate::core::Alignment) {
if alignment.to_horizontal().is_none() || self.alignment == alignment {
return;
}
self.alignment = alignment;
self.base.request_redraw();
}
pub fn minimum_date(&self) -> Date {
self.minimum
}
pub fn maximum_date(&self) -> Date {
self.maximum
}
pub fn display_format(&self) -> &str {
&self.display_format
}
pub fn calendar_popup(&self) -> bool {
self.calendar_popup
}
pub fn calendar_open(&self) -> bool {
self.calendar_open
}
pub fn set_date(&mut self, date: Date) {
if date.is_valid() && date >= self.minimum && date <= self.maximum && self.date != date {
let before = self.date;
self.date = date;
if !self.restoring_history {
*self.history_target.borrow_mut() = self.date;
self.undo_stack.push(Box::new(ValueCommand::new(
self.history_target.clone(),
before,
self.date,
"Edit date",
"date_edit",
)));
}
self.date_changed.emit(date);
self.base.request_redraw();
}
}
pub fn set_minimum_date(&mut self, date: Date) {
if self.minimum == date {
return;
}
self.minimum = date;
self.calendar_sync_dirty = true;
self.clamp_to_range();
self.base.request_redraw();
}
pub fn set_maximum_date(&mut self, date: Date) {
if self.maximum == date {
return;
}
self.maximum = date;
self.calendar_sync_dirty = true;
self.clamp_to_range();
self.base.request_redraw();
}
pub fn set_date_range(&mut self, min: Date, max: Date) {
if self.minimum == min && self.maximum == max {
return;
}
self.minimum = min;
self.maximum = max;
self.calendar_sync_dirty = true;
self.clamp_to_range();
self.base.request_redraw();
}
fn clamp_to_range(&mut self) {
self.date = super::clamp_ordered_range(self.date, self.minimum, self.maximum);
}
pub fn set_display_format(&mut self, fmt: String) {
if self.display_format == fmt {
return;
}
self.display_format = fmt;
self.base.request_redraw();
}
pub fn set_calendar_popup(&mut self, popup: bool) {
if self.calendar_popup == popup {
return;
}
self.calendar_popup = popup;
if !popup {
self.set_calendar_open(false);
}
self.base.request_redraw();
}
pub fn set_calendar_open(&mut self, open: bool) {
let open = open && self.calendar_popup;
if self.calendar_open == open {
return;
}
if open {
self.sync_calendar_open_state();
}
self.calendar_open = open;
self.calendar_reveal.set_open(open);
self.popup_visibility_changed.emit(open);
if open {
self.calendar_opened.emit();
} else {
self.calendar_closed.emit();
}
self.base.request_redraw();
}
pub fn toggle_calendar_popup(&mut self) {
if self.calendar_popup {
self.set_calendar_open(!self.calendar_open);
}
}
pub fn calendar(&self) -> &Calendar {
&self.calendar
}
pub fn calendar_mut(&mut self) -> &mut Calendar {
&mut self.calendar
}
pub fn set_calendar(&mut self, mut calendar: Calendar) {
calendar.set_first_day_of_week(self.calendar_first_day);
self.calendar = calendar;
self.calendar_sync_dirty = true;
if self.calendar_open {
self.sync_calendar_open_state();
}
self.base.request_redraw();
}
pub fn calendar_first_day(&self) -> Weekday {
self.calendar_first_day
}
pub fn set_calendar_first_day(&mut self, weekday: Weekday) {
if self.calendar_first_day == weekday {
return;
}
self.calendar_first_day = weekday;
self.calendar.set_first_day_of_week(weekday);
self.calendar_sync_dirty = true;
self.base.request_redraw();
}
pub fn open_calendar(&mut self) {
if !self.calendar_popup || self.calendar_open {
return;
}
self.set_calendar_open(true);
}
pub fn close_calendar(&mut self) {
if self.calendar_open {
self.set_calendar_open(false);
}
}
fn sync_calendar_open_state(&mut self) {
let popup = calendar_popup_rect(self.geometry());
self.calendar.set_geometry(popup);
self.calendar.set_first_day_of_week(self.calendar_first_day);
if let Some(minimum) = date_to_naive(self.minimum) {
self.calendar.set_minimum_date(minimum);
}
if let Some(maximum) = date_to_naive(self.maximum) {
self.calendar.set_maximum_date(maximum);
}
if let Some(date) = date_to_naive(self.date) {
self.calendar.set_display_month(date);
self.calendar.set_selected_date(date);
}
self.calendar_sync_rect = popup;
self.calendar_sync_dirty = false;
}
fn sync_calendar_page(&mut self) {
let popup = calendar_popup_rect(self.geometry());
if !self.calendar_sync_dirty && self.calendar_sync_rect == popup {
return;
}
self.calendar.set_geometry(popup);
if let Some(minimum) = date_to_naive(self.minimum) {
self.calendar.set_minimum_date(minimum);
}
if let Some(maximum) = date_to_naive(self.maximum) {
self.calendar.set_maximum_date(maximum);
}
self.calendar_sync_rect = popup;
self.calendar_sync_dirty = false;
}
fn commit_calendar_selection(&mut self) {
let selected = naive_to_date(self.calendar.selected_date());
if selected != self.date {
self.set_date(selected);
}
self.close_calendar();
}
pub fn current_section(&self) -> Option<DateSection> {
self.current_section
}
pub fn set_current_section(&mut self, section: Option<DateSection>) {
if self.current_section == section {
return;
}
self.current_section = section;
self.digit_buffer.clear();
self.base.request_redraw();
}
pub fn section_zone(&self, section: DateSection) -> Rect {
let rect = self.geometry();
let trailing = if self.calendar_popup { CALENDAR_ARROW_W } else { 0 }
+ if self.spin_buttons { 24 } else { 0 };
let width = rect.width.saturating_sub(12 + trailing);
let third = width / 3;
let index = match section {
DateSection::Year => 0,
DateSection::Month => 1,
DateSection::Day => 2,
};
Rect::new(rect.x + 6 + index * third as i32, rect.y, third, rect.height)
}
fn section_at(&self, pos: Point) -> Option<DateSection> {
let rect = self.geometry();
if !rect.contains_point(pos) {
return None;
}
[DateSection::Year, DateSection::Month, DateSection::Day]
.into_iter()
.find(|§ion| self.section_zone(section).contains_point(pos))
}
pub fn step_current_section(&mut self, delta: i32) {
let Some(section) = self.current_section else {
if delta >= 0 {
self.step_up();
} else {
self.step_down();
}
return;
};
if delta == 0 {
return;
}
let mut next = self.date;
match section {
DateSection::Year => {
next.set_year(self.date.year() + delta);
}
DateSection::Month => {
let total = self.date.year() * 12 + self.date.month() as i32 - 1 + delta;
next.set_year(total.div_euclid(12));
next.set_month((total.rem_euclid(12) + 1) as u8);
}
DateSection::Day => {
if delta >= 0 {
self.step_up();
} else {
self.step_down();
}
return;
}
}
self.set_date(next);
}
pub fn type_digit(&mut self, digit: char) -> bool {
if !digit.is_ascii_digit() {
return false;
}
let Some(section) = self.current_section else {
return false;
};
self.digit_buffer.push(digit);
while self.digit_buffer.len() > section.digit_capacity() {
self.digit_buffer.remove(0);
}
let Ok(value) = self.digit_buffer.parse::<i32>() else {
return false;
};
let mut next = self.date;
match section {
DateSection::Year => {
if value < 1000 {
return true;
}
next.set_year(value);
}
DateSection::Month => next.set_month(value as u8),
DateSection::Day => next.set_day(value as u8),
}
self.set_date(next);
true
}
pub fn spin_buttons(&self) -> bool {
self.spin_buttons
}
pub fn set_spin_buttons(&mut self, visible: bool) {
if self.spin_buttons == visible {
return;
}
self.spin_buttons = visible;
self.base.request_redraw();
}
pub fn spin_button_rects(&self) -> Option<(Rect, Rect)> {
if !self.spin_buttons {
return None;
}
let rect = self.geometry();
let width = 24u32.min(rect.width);
let x = rect.x + rect.width as i32
- width as i32
- if self.calendar_popup { CALENDAR_ARROW_W as i32 } else { 0 };
let half = rect.height / 2;
Some((
Rect::new(x, rect.y, width, half),
Rect::new(x, rect.y + half as i32, width, rect.height - half),
))
}
pub fn special_value_text(&self) -> Option<&str> {
self.special_value_text.as_deref()
}
pub fn set_special_value_text(&mut self, text: Option<String>) {
if self.special_value_text == text {
return;
}
self.special_value_text = text;
self.base.request_redraw();
}
pub fn step_up(&mut self) {
let mut d = self.date;
let next_day = d.day() as i32 + 1;
if next_day > d.days_in_month() as i32 {
d.day = 1;
let next_month = d.month() as i32 + 1;
if next_month > 12 {
d.month = 1;
d.year += 1;
} else {
d.month = next_month as u8;
}
} else {
d.day = next_day as u8;
}
self.set_date(d);
}
pub fn step_down(&mut self) {
let mut d = self.date;
if d.day() > 1 {
d.set_day(d.day() - 1);
} else {
if d.month() > 1 {
d.set_month(d.month() - 1);
} else {
d.set_month(12);
d.set_year(d.year() - 1);
}
d.set_day(d.days_in_month());
}
self.set_date(d);
}
pub fn undo(&mut self) -> bool {
if self.undo_stack.undo().is_err() {
return false;
}
self.restore_history_date();
true
}
pub fn redo(&mut self) -> bool {
if self.undo_stack.redo().is_err() {
return false;
}
self.restore_history_date();
true
}
pub fn can_undo(&self) -> bool {
self.undo_stack.can_undo()
}
pub fn can_redo(&self) -> bool {
self.undo_stack.can_redo()
}
fn restore_history_date(&mut self) {
self.restoring_history = true;
self.date = *self.history_target.borrow();
self.restoring_history = false;
self.date_changed.emit(self.date);
self.base.request_redraw();
}
}
impl Widget for DateEdit {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
crate::core::Size::new(
dimensions::DATE_EDIT_DEFAULT_WIDTH,
dimensions::DATE_EDIT_DEFAULT_HEIGHT,
)
}
fn tick(&mut self, delta_ms: u32) -> bool {
self.calendar_reveal.tick(delta_ms)
}
fn is_animating(&self) -> bool {
self.calendar_reveal.is_animating()
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"date_changed" => {
Some(EventSignalRef::mapped("date_changed", &self.date_changed, |v| {
CapabilityValue::String(format!("{v:?}"))
}))
}
"popup_visibility_changed" => Some(EventSignalRef::mapped(
"popup_visibility_changed",
&self.popup_visibility_changed,
|v| CapabilityValue::Bool(*v),
)),
"calendar_opened" => {
Some(EventSignalRef::unit("calendar_opened", &self.calendar_opened))
}
"calendar_closed" => {
Some(EventSignalRef::unit("calendar_closed", &self.calendar_closed))
}
"editing_finished" => {
Some(EventSignalRef::unit("editing_finished", &self.editing_finished))
}
_ => None,
}
}
}
impl WidgetProperties for DateEdit {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"date" => Ok(CapabilityValue::String(date_to_string(self.date()))),
"minimum_date" => Ok(CapabilityValue::String(date_to_string(self.minimum_date()))),
"maximum_date" => Ok(CapabilityValue::String(date_to_string(self.maximum_date()))),
"display_format" => Ok(CapabilityValue::String(self.display_format().to_string())),
"calendar_popup" => Ok(CapabilityValue::Bool(self.calendar_popup())),
"calendar_first_day" => {
Ok(CapabilityValue::String(weekday_to_str(self.calendar_first_day()).to_string()))
}
"alignment" => Ok(CapabilityValue::String(
horizontal_alignment_to_str(self.alignment()).to_string(),
)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"date" => {
self.set_date(expect_date(value)?);
Ok(())
}
"minimum_date" => {
self.set_minimum_date(expect_date(value)?);
Ok(())
}
"maximum_date" => {
self.set_maximum_date(expect_date(value)?);
Ok(())
}
"display_format" => {
self.set_display_format(expect_string(value)?);
Ok(())
}
"calendar_popup" => {
self.set_calendar_popup(expect_bool(value)?);
Ok(())
}
"calendar_first_day" => {
self.set_calendar_first_day(expect_weekday(value)?);
Ok(())
}
"alignment" => {
self.set_alignment(expect_horizontal_alignment(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"date",
"minimum_date",
"maximum_date",
"display_format",
"calendar_popup",
"calendar_first_day",
"alignment",
BASE_PROPERTY_NAMES
]
}
}
impl EventHandler for DateEdit {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
if self.calendar_open && matches!(event, Event::MouseMove { .. } | Event::MouseLeave { .. })
{
self.sync_calendar_page();
self.calendar.handle_event(event);
}
match event {
Event::MousePress { pos, button, .. } if *button == 1 => {
let field = self.geometry();
if let Some((up, down)) = self.spin_button_rects() {
if up.contains_point(*pos) {
self.step_current_section(1);
return;
}
if down.contains_point(*pos) {
self.step_current_section(-1);
return;
}
}
if self.calendar_popup && calendar_arrow_rect(field).contains_point(*pos) {
self.toggle_calendar_popup();
} else if self.calendar_open {
let popup = calendar_popup_rect(field);
if popup.contains_point(*pos) {
self.sync_calendar_page();
let before = self.calendar.selected_date();
self.calendar.handle_event(event);
let after = self.calendar.selected_date();
if after != before {
self.set_date(naive_to_date(after));
self.close_calendar();
}
} else {
self.close_calendar();
}
} else if let Some(section) = self.section_at(*pos) {
self.set_current_section(Some(section));
}
}
Event::MouseDoubleClick { pos, .. } if self.calendar_open => {
if calendar_popup_rect(self.geometry()).contains_point(*pos) {
self.sync_calendar_page();
self.calendar.handle_event(event);
self.commit_calendar_selection();
}
}
Event::Wheel { delta, .. } => {
if delta.y > 0 {
self.step_current_section(1);
} else if delta.y < 0 {
self.step_current_section(-1);
}
}
Event::KeyPress { key, .. } if self.calendar_open => {
if *key == 27 {
self.close_calendar();
} else {
self.sync_calendar_page();
self.calendar.handle_event(event);
if *key == 13 || *key == 32 {
self.commit_calendar_selection();
}
}
}
Event::KeyPress { key, modifiers } => match *key {
27 if self.calendar_open => self.set_calendar_open(false),
13 => self.editing_finished.emit(),
_ if primary_shortcut_pressed(*key, *modifiers, Key::Z) => {
let _ = self.undo();
}
_ if primary_shortcut_pressed(*key, *modifiers, Key::Y) => {
let _ = self.redo();
}
37 => {
if let Some(section) = self.current_section {
let previous = section.previous();
self.set_current_section(Some(previous));
}
}
39 => {
if let Some(section) = self.current_section {
let next = section.next();
self.set_current_section(Some(next));
}
}
38 => self.step_current_section(1), 40 => self.step_current_section(-1), 48..=57 if self.current_section.is_some() => {
if let Some(digit) = char::from_u32(*key) {
self.type_digit(digit);
}
}
_ => { }
},
Event::FocusLost => self.editing_finished.emit(),
_ => { }
}
}
}
impl Draw for DateEdit {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("date_edit");
let (window_fill, foreground) = match crate::style::palette_shared() {
Some(colors) => (colors.background, colors.foreground),
None => (Color::rgb(240, 240, 240), Color::BLACK),
};
let ink = style
.text_color
.or_else(|| theme.as_ref().and_then(|t| t.text_color))
.unwrap_or(foreground);
let field = window_fill.blend(&ink, 0.08);
let surface = match style.background_color {
Some(resolved) if resolved != window_fill => resolved,
_ => field,
};
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.filter(|resolved| *resolved != surface)
.unwrap_or_else(|| surface.blend(&ink, 0.35));
context.face_with_gradient(
rect,
surface,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
let mut text = String::new();
let special = self.special_value_text.as_deref().filter(|_| self.date == self.minimum);
if let Some(special) = special {
text.push_str(special);
} else if !super::date_edit::format_with_pattern_into(
&self.display_format,
super::date_edit::DateTimeComponents {
year: Some(self.date.year()),
month: Some(self.date.month()),
day: Some(self.date.day()),
..Default::default()
},
&mut text,
) {
text.clear();
let _ = write!(text, "{}", self.date);
}
let font = effective_font(style);
let line = context.text_line(rect, font);
let text_width = rect
.width
.saturating_sub(12)
.saturating_sub(if self.calendar_popup { CALENDAR_ARROW_W } else { 0 })
.saturating_sub(if self.spin_buttons { 24 } else { 0 });
context.draw_text_fitted_cow(
Rect { x: rect.x + 6, y: line.y, width: text_width, height: line.height },
text.as_str(),
font,
ink,
self.alignment.to_horizontal().unwrap_or(HorizontalAlignment::Left),
);
if let Some(section) = self.current_section {
let zone = self.section_zone(section);
let y = line.y + line.height as i32;
context.draw_line(
Point::new(zone.x + 2, y),
Point::new(zone.x + zone.width as i32 - 2, y),
ink,
);
}
if self.calendar_popup {
draw_calendar_arrow(context, rect, ink);
}
if let Some((up, down)) = self.spin_button_rects() {
let fill = surface.blend(&ink, 0.08);
context.fill_rect(up, fill);
context.fill_rect(down, fill);
context.draw_rect(up, border);
context.draw_rect(down, border);
let side = up.width.min(up.height).min(10);
crate::widget::draw_icon_centered(
context,
up,
side,
ink,
crate::widget::IconName::ChevronUp,
);
crate::widget::draw_icon_centered(
context,
down,
side,
ink,
crate::widget::IconName::ChevronDown,
);
}
if self.calendar_open || !self.calendar_reveal.is_closed() {
let popup = calendar_popup_rect(rect);
let revealed = self.calendar_reveal.revealed(
popup,
crate::widget::menu_toolbar::popup_reveal::RevealDirection::Down,
crate::widget::menu_toolbar::popup_reveal::PopupReveal::travel_cap_for(Size::new(
popup.width,
popup.height,
)),
);
let clipped = !self.calendar_reveal.is_closed();
if clipped {
context.push_clip(revealed.x, revealed.y, revealed.width, revealed.height);
}
self.sync_calendar_page();
self.calendar.draw(context);
if clipped {
context.pop_clip();
}
}
}
}
const CALENDAR_POPUP_W: u32 = 224;
const CALENDAR_POPUP_H: u32 = 210;
const POPUP_GAP: i32 = 4;
pub(crate) const CALENDAR_ARROW_W: u32 = 16;
pub(crate) fn calendar_arrow_rect(field: Rect) -> Rect {
Rect::new(field.right() - CALENDAR_ARROW_W as i32, field.y, CALENDAR_ARROW_W, field.height)
}
pub(crate) fn draw_calendar_arrow(context: &mut RenderContext, field: Rect, color: Color) {
let zone = calendar_arrow_rect(field);
if zone.width < 8 || zone.height < 8 {
return;
}
let cx = zone.x + zone.width as i32 / 2;
let cy = zone.y + zone.height as i32 / 2;
let half_w = 4;
let rise = 2;
context.draw_line(Point::new(cx - half_w, cy - rise), Point::new(cx, cy + rise), color);
context.draw_line(Point::new(cx, cy + rise), Point::new(cx + half_w, cy - rise), color);
}
pub(crate) fn calendar_popup_rect(field: Rect) -> Rect {
Rect::new(
field.x,
field.y + field.height as i32 + POPUP_GAP,
CALENDAR_POPUP_W.max(field.width),
CALENDAR_POPUP_H,
)
}
pub(crate) fn date_to_naive(date: Date) -> Option<NaiveDate> {
NaiveDate::from_ymd_opt(date.year(), date.month() as u32, date.day() as u32)
}
pub(crate) fn naive_to_date(date: NaiveDate) -> Date {
Date::new(date.year(), date.month() as u8, date.day() as u8)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use crate::widget::input_widgets::shortcut_editor::{
non_primary_modifier_event_bits, primary_modifier_event_bits,
};
use std::sync::{Arc, Mutex};
#[test]
fn date_creation_and_accessors() {
let d = Date::new(2026, 6, 8);
assert_eq!(d.year(), 2026);
assert_eq!(d.month(), 6);
assert_eq!(d.day(), 8);
}
#[test]
fn date_clamps_to_valid_ranges() {
let d = Date::new(2024, 13, 32);
assert_eq!(d.month(), 13);
assert_eq!(d.day(), 32);
assert!(!d.is_valid());
}
#[test]
fn date_is_leap_year() {
assert!(Date::new(2024, 1, 1).is_leap_year());
assert!(Date::new(2000, 1, 1).is_leap_year());
assert!(!Date::new(2023, 1, 1).is_leap_year());
assert!(!Date::new(1900, 1, 1).is_leap_year());
}
#[test]
fn date_days_in_month() {
let jan = Date::new(2026, 1, 1);
assert_eq!(jan.days_in_month(), 31);
let feb_normal = Date::new(2023, 2, 1); assert_eq!(feb_normal.days_in_month(), 28);
let feb_leap = Date::new(2024, 2, 1); assert_eq!(feb_leap.days_in_month(), 29);
let apr = Date::new(2026, 4, 1);
assert_eq!(apr.days_in_month(), 30);
}
#[test]
fn date_is_valid() {
assert!(Date::new(2026, 6, 8).is_valid());
assert!(Date::new(2024, 2, 29).is_valid()); assert!(!Date::new(2023, 2, 29).is_valid()); assert!(!Date::new(2026, 0, 1).is_valid()); assert!(!Date::new(2026, 13, 1).is_valid()); assert!(!Date::new(2026, 1, 0).is_valid()); assert!(!Date::new(2026, 4, 31).is_valid()); }
#[test]
fn date_setters_modify_correctly() {
let mut d = Date::new(2026, 6, 8);
d.set_year(2027);
assert_eq!(d.year(), 2027);
d.set_month(12);
assert_eq!(d.month(), 12);
d.set_day(25);
assert_eq!(d.day(), 25);
}
#[test]
fn date_setters_clamp_values() {
let mut d = Date::new(2026, 6, 8);
d.set_month(0);
assert_eq!(d.month(), 1);
d.set_month(13);
assert_eq!(d.month(), 12);
d.set_day(0);
assert_eq!(d.day(), 1);
d.set_day(32);
assert_eq!(d.day(), 31);
}
#[test]
fn date_display_format() {
let d = Date::new(2026, 6, 8);
assert_eq!(d.to_string(), "2026-06-08");
let d2 = Date::new(2024, 12, 25);
assert_eq!(d2.to_string(), "2024-12-25");
}
#[test]
fn date_ordering() {
let earlier = Date::new(2024, 1, 1);
let later = Date::new(2026, 6, 8);
assert!(earlier < later);
assert!(later > earlier);
assert_eq!(earlier, Date::new(2024, 1, 1));
}
#[test]
fn date_today() {
let today = Date::today();
assert_eq!(today.year(), 2024);
assert_eq!(today.month(), 1);
assert_eq!(today.day(), 1);
}
#[test]
fn date_edit_creation_defaults() {
let editor = DateEdit::new(Rect::new(0, 0, 200, 30));
assert_eq!(editor.date(), Date::today());
assert_eq!(editor.display_format(), "yyyy-MM-dd");
assert!(!editor.calendar_popup());
assert!(editor.is_visible());
assert!(editor.is_enabled());
}
#[test]
fn date_edit_set_date() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
let d = Date::new(2026, 12, 25);
editor.set_date(d);
assert_eq!(editor.date(), d);
}
#[test]
fn date_edit_set_date_clamps_to_range() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
let before_min = Date::new(1700, 1, 1);
editor.set_date(before_min);
assert_eq!(editor.date(), Date::today()); }
#[test]
fn date_edit_set_date_clamps_to_maximum() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
let after_max = Date::new(10000, 1, 1);
editor.set_date(after_max);
assert_eq!(editor.date(), Date::today()); }
#[test]
fn date_edit_set_date_range() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
let min = Date::new(2020, 1, 1);
let max = Date::new(2030, 12, 31);
editor.set_date_range(min, max);
assert_eq!(editor.minimum_date(), min);
assert_eq!(editor.maximum_date(), max);
}
#[test]
fn date_edit_set_display_format() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_display_format("dd/MM/yyyy".to_string());
assert_eq!(editor.display_format(), "dd/MM/yyyy");
}
#[test]
fn the_display_format_pattern_is_applied_to_the_painted_value() {
use crate::widget::svg::{render_to_svg, text_subpath_count};
let _theme_guard = crate::style::theme_test_guard();
let make = |format: &str| {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_display_format(format.to_string());
editor
};
let mut plain = make("yyyy-MM-dd");
let plain_svg = render_to_svg(&mut plain);
let mut explicit = make("yyyy-MM-dd");
assert_eq!(
render_to_svg(&mut explicit),
plain_svg,
"the default pattern must reproduce the pre-pattern spelling"
);
let mut reordered = make("dd/MM/yyyy");
let reordered_svg = render_to_svg(&mut reordered);
assert_ne!(
reordered_svg, plain_svg,
"a reordered pattern must move the ink, or the pattern was ignored"
);
let (a, b) = (text_subpath_count(&reordered_svg), text_subpath_count(&plain_svg));
assert!(
a.abs_diff(b) < b / 5,
"both spell the same date, so neither may gain or lose a field: reordered={a} plain={b}"
);
let mut nonsense = make("today");
assert_eq!(
render_to_svg(&mut nonsense),
plain_svg,
"an unrecognised pattern must fall back to the value, not print the pattern"
);
}
#[test]
fn the_pattern_substituter_prefers_the_longest_token() {
let c = DateTimeComponents {
year: Some(2026),
month: Some(6),
day: Some(8),
hour: Some(9),
minute: Some(5),
second: Some(7),
millisecond: Some(42),
};
let render = |pattern: &str| format_with_pattern(pattern, c);
assert_eq!(render("yyyy-MM-dd"), Some("2026-06-08".to_string()));
assert_eq!(render("dd/MM/yyyy"), Some("08/06/2026".to_string()));
assert_eq!(render("d/M/yyyy"), Some("8/6/2026".to_string()));
assert_eq!(render("HH:mm:ss"), Some("09:05:07".to_string()));
assert_eq!(render("H:mm"), Some("9:05".to_string()));
assert_eq!(render("SSS"), Some("042".to_string()));
assert_eq!(render("yy"), Some("26".to_string()));
assert_eq!(render("Today: yyyy"), Some("Today: 2026".to_string()));
let date_only = DateTimeComponents { year: Some(2026), ..Default::default() };
assert_eq!(format_with_pattern("yyyy HH", date_only), Some("2026 ".to_string()));
assert_eq!(render("nope"), None);
assert_eq!(render(""), None);
}
#[test]
fn date_edit_set_calendar_popup() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
assert!(!editor.calendar_popup());
editor.set_calendar_popup(true);
assert!(editor.calendar_popup());
}
#[test]
fn weekday_agrees_with_known_dates() {
assert_eq!(Date::new(2026, 6, 8).weekday(), 1, "2026-06-08 is a Monday");
assert_eq!(Date::new(2000, 1, 1).weekday(), 6, "2000-01-01 is a Saturday");
assert_eq!(Date::new(2024, 2, 29).weekday(), 4, "2024-02-29 is a Thursday");
assert_eq!(Date::new(1900, 1, 1).weekday(), 1, "1900-01-01 is a Monday");
assert_eq!(Date::new(2024, 3, 1).weekday(), 5, "2024-03-01 is a Friday");
assert_eq!(Date::new(1752, 9, 14).weekday(), 4, "the accepted floor is a Thursday");
let mut date = Date::new(2024, 2, 26);
let mut expected = 1; for _ in 0..8 {
assert_eq!(date.weekday(), expected, "{date}");
expected = (expected + 1) % 7;
date = next_day(date);
}
}
fn next_day(mut date: Date) -> Date {
if date.day < date.days_in_month() {
date.day += 1;
return date;
}
date.day = 1;
if date.month < 12 {
date.month += 1;
} else {
date.month = 1;
date.year += 1;
}
date
}
#[test]
fn the_calendar_popup_is_actually_painted() {
use crate::widget::svg::{render_to_svg, text_subpath_count};
let _theme_guard = crate::style::theme_test_guard();
let make = |popup: bool| {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_calendar_popup(popup);
if popup {
editor.set_calendar_open(true);
}
editor
};
let mut closed = make(false);
let closed_svg = render_to_svg(&mut closed);
let mut open = make(true);
let open_svg = render_to_svg(&mut open);
assert_ne!(closed_svg, open_svg, "opening the popup must change the picture");
let closed_ink = text_subpath_count(&closed_svg);
let open_ink = text_subpath_count(&open_svg);
assert!(
open_ink > closed_ink * 4,
"the popup must paint the grid and the month's days: closed={closed_ink} open={open_ink}"
);
assert!(
open_svg.matches("<rect").count() > closed_svg.matches("<rect").count(),
"the popup paints its own plate: {open_svg}"
);
}
#[test]
fn calendar_popup_alone_does_not_paint_the_grid() {
use crate::widget::svg::{render_to_svg, text_subpath_count};
let _theme_guard = crate::style::theme_test_guard();
let mut plain = DateEdit::new(Rect::new(0, 0, 200, 30));
plain.set_date(Date::new(2026, 6, 8));
let mut affordance = DateEdit::new(Rect::new(0, 0, 200, 30));
affordance.set_date(Date::new(2026, 6, 8));
affordance.set_calendar_popup(true);
assert!(!affordance.calendar_open(), "the flag alone must not open the grid");
let plain_svg = render_to_svg(&mut plain);
let affordance_svg = render_to_svg(&mut affordance);
assert_ne!(plain_svg, affordance_svg, "the arrow affordance itself must be painted");
assert_eq!(
text_subpath_count(&affordance_svg),
text_subpath_count(&plain_svg),
"no weekday/day runs may appear until the grid is open"
);
}
#[test]
fn arrow_click_emits_popup_visibility_changed_and_paints_the_grid() {
use crate::widget::svg::{render_to_svg, text_subpath_count};
let _theme_guard = crate::style::theme_test_guard();
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_calendar_popup(true);
let seen = Arc::new(Mutex::new(Vec::<bool>::new()));
let sink = Arc::clone(&seen);
editor.popup_visibility_changed.connect(move |open| {
if let Ok(mut v) = sink.lock() {
v.push(*open);
}
});
let closed_svg = render_to_svg(&mut editor);
let arrow = calendar_arrow_rect(editor.geometry()).center();
editor.handle_event(&Event::mouse_press(arrow.x, arrow.y, 1));
assert!(editor.calendar_open(), "the arrow press must open the grid");
assert_eq!(*seen.lock().expect("visibility lock"), vec![true]);
let open_svg = render_to_svg(&mut editor);
assert!(
text_subpath_count(&open_svg) > text_subpath_count(&closed_svg) * 4,
"the open grid must add the month's day runs"
);
}
fn calendar_day_center(editor: &DateEdit, day: u32) -> Point {
let popup = calendar_popup_rect(editor.geometry());
let nav_h = 30_i32;
let header_h = 24_i32;
let grid_y = popup.y + nav_h + header_h;
let grid_h = popup.height as i32 - nav_h - header_h;
let cell_w = (popup.width / 7).max(1) as i32;
let cell_h = (grid_h / 6).max(1);
let date = editor.date();
let first = NaiveDate::from_ymd_opt(date.year(), date.month() as u32, 1)
.expect("the field's month always has a first day");
let slot = first.weekday().num_days_from_sunday() + day - 1;
Point::new(
popup.x + (slot % 7) as i32 * cell_w + cell_w / 2,
grid_y + (slot / 7) as i32 * cell_h + cell_h / 2,
)
}
#[test]
fn selecting_a_day_updates_the_date_and_closes_the_popup() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_calendar_popup(true);
editor.set_calendar_open(true);
let cell = calendar_day_center(&editor, 15);
editor.handle_event(&Event::mouse_press(cell.x, cell.y, 1));
assert_eq!(editor.date(), Date::new(2026, 6, 15));
assert!(!editor.calendar_open(), "picking a day finishes the interaction");
}
#[test]
fn calendar_open_and_close_signals_bracket_the_embedded_calendar() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_calendar_popup(true);
let opened = Arc::new(Mutex::new(0_usize));
let closed = Arc::new(Mutex::new(0_usize));
let opened_sink = Arc::clone(&opened);
let closed_sink = Arc::clone(&closed);
editor.calendar_opened.connect(move || {
if let Ok(mut count) = opened_sink.lock() {
*count += 1;
}
});
editor.calendar_closed.connect(move || {
if let Ok(mut count) = closed_sink.lock() {
*count += 1;
}
});
editor.open_calendar();
assert!(editor.calendar_open());
assert_eq!(editor.calendar().selected_date(), NaiveDate::from_ymd_opt(2026, 6, 8).unwrap());
assert_eq!(
editor.calendar().first_day_of_week(),
Weekday::Sun,
"the popup keeps the grid's historical Sunday-first columns"
);
editor.close_calendar();
assert!(!editor.calendar_open());
assert_eq!(*opened.lock().expect("opened lock"), 1);
assert_eq!(*closed.lock().expect("closed lock"), 1);
}
#[test]
fn calendar_first_day_property_round_trips_and_reaches_the_calendar() {
use crate::widget::capability::types::CapabilityValue;
use crate::widget::capability::WidgetProperties;
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor
.set("calendar_first_day", CapabilityValue::String("mon".to_string()))
.expect("writable");
assert_eq!(editor.calendar_first_day(), Weekday::Mon);
assert_eq!(editor.calendar().first_day_of_week(), Weekday::Mon);
assert_eq!(
editor.get("calendar_first_day").expect("readable"),
CapabilityValue::String("mon".to_string())
);
}
#[test]
fn escape_closes_the_calendar_first() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_calendar_popup(true);
editor.set_calendar_open(true);
editor.handle_event(&Event::KeyPress { key: 27, modifiers: 0 });
assert!(!editor.calendar_open());
assert_eq!(editor.date(), Date::new(2026, 6, 8), "closing must not change the value");
}
#[test]
fn date_edit_step_up() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.step_up();
assert_eq!(editor.date(), Date::new(2026, 6, 9));
}
#[test]
fn date_edit_step_up_rolls_month() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 30));
editor.step_up();
assert_eq!(editor.date(), Date::new(2026, 7, 1));
}
#[test]
fn date_edit_step_up_rolls_year() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 12, 31));
editor.step_up();
assert_eq!(editor.date(), Date::new(2027, 1, 1));
}
#[test]
fn date_edit_step_down() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.step_down();
assert_eq!(editor.date(), Date::new(2026, 6, 7));
}
#[test]
fn date_edit_step_down_rolls_month() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 1));
editor.step_down();
assert_eq!(editor.date(), Date::new(2026, 5, 31));
}
#[test]
fn date_edit_step_down_rolls_year() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 1, 1));
editor.step_down();
assert_eq!(editor.date(), Date::new(2025, 12, 31));
}
#[test]
fn date_edit_undo_redo_restores_date() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.step_up();
assert!(editor.can_undo());
assert!(editor.undo());
assert_eq!(editor.date(), Date::new(2026, 6, 8));
assert!(editor.can_redo());
assert!(editor.redo());
assert_eq!(editor.date(), Date::new(2026, 6, 9));
}
#[test]
fn date_edit_keyboard_shortcuts_drive_history() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_date(Date::new(2026, 6, 9));
editor.handle_event(&Event::KeyPress { key: 90, modifiers: primary_modifier_event_bits() });
assert_eq!(editor.date(), Date::new(2026, 6, 8));
editor.handle_event(&Event::KeyPress { key: 89, modifiers: primary_modifier_event_bits() });
assert_eq!(editor.date(), Date::new(2026, 6, 9));
}
#[test]
fn date_edit_primary_shortcut_and_shifted_chord() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.set_date(Date::new(2026, 6, 9));
let primary = primary_modifier_event_bits();
editor.handle_event(&Event::key_press(90, primary | 0b0001));
assert_eq!(editor.date(), Date::new(2026, 6, 9));
editor.handle_event(&Event::key_press(90, primary));
assert_eq!(editor.date(), Date::new(2026, 6, 8));
editor.handle_event(&Event::key_press(89, primary));
assert_eq!(editor.date(), Date::new(2026, 6, 9));
editor.handle_event(&Event::key_press(90, non_primary_modifier_event_bits()));
assert_eq!(editor.date(), Date::new(2026, 6, 9));
}
#[test]
fn date_edit_date_changed_signal_emits() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
let captured = Arc::new(Mutex::new(Date::new(0, 1, 1)));
let sink = captured.clone();
editor.date_changed.connect(move |d| {
if let Ok(mut c) = sink.lock() {
*c = *d;
}
});
let expected = Date::new(2027, 4, 15);
editor.set_date(expected);
let got = *captured.lock().unwrap();
assert_eq!(got, expected);
}
#[test]
fn date_edit_keyboard_navigation() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
editor.set_date(Date::new(2026, 6, 8));
editor.handle_event(&Event::KeyPress { key: 38, modifiers: 0 });
assert_eq!(editor.date(), Date::new(2026, 6, 9));
editor.handle_event(&Event::KeyPress { key: 40, modifiers: 0 });
assert_eq!(editor.date(), Date::new(2026, 6, 8));
}
#[test]
fn date_edit_geometry_delegation() {
let rect = Rect::new(10, 20, 200, 30);
let editor = DateEdit::new(rect);
assert_eq!(editor.geometry(), rect);
assert_eq!(editor.position(), Point::new(10, 20));
assert_eq!(editor.size(), crate::core::Size::new(200, 30));
}
#[test]
fn date_edit_visibility() {
let mut editor = DateEdit::new(Rect::new(0, 0, 200, 30));
assert!(editor.is_visible());
editor.set_visible(false);
assert!(!editor.is_visible());
editor.set_visible(true);
assert!(editor.is_visible());
}
#[test]
fn date_edit_kind() {
let editor = DateEdit::new(Rect::new(0, 0, 200, 30));
assert_eq!(editor.kind(), WidgetKind::DatePicker);
}
#[test]
fn date_edit_ids_are_unique() {
let a = DateEdit::new(Rect::new(0, 0, 200, 30));
let b = DateEdit::new(Rect::new(0, 0, 200, 30));
assert_ne!(a.id(), b.id());
}
#[test]
fn lowering_the_maximum_clamps_the_current_date() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date_range(Date::new(2024, 1, 1), Date::new(2024, 12, 31));
edit.set_date(Date::new(2024, 6, 15));
edit.set_maximum_date(Date::new(2024, 3, 31));
assert_eq!(
edit.date(),
Date::new(2024, 3, 31),
"the value must be clamped to the new maximum rather than left out of range"
);
edit.set_date(Date::new(2024, 2, 1));
}
#[test]
fn raising_the_minimum_clamps_the_current_date() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date_range(Date::new(2024, 1, 1), Date::new(2024, 12, 31));
edit.set_date(Date::new(2024, 6, 15));
edit.set_minimum_date(Date::new(2024, 9, 1));
assert_eq!(edit.date(), Date::new(2024, 9, 1), "clamped up to the new minimum");
edit.set_date(Date::new(2024, 10, 1));
}
#[test]
fn an_inverted_range_pins_to_the_minimum_and_stays_usable() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date_range(Date::new(2024, 1, 1), Date::new(2024, 12, 31));
edit.set_date(Date::new(2024, 6, 15));
edit.set_date_range(Date::new(2024, 10, 1), Date::new(2024, 3, 1));
assert_eq!(edit.date(), Date::new(2024, 10, 1), "the minimum is authoritative");
edit.set_date(Date::new(2024, 10, 1));
}
#[test]
fn setting_a_range_that_contains_the_value_does_not_move_it() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date_range(Date::new(2024, 1, 1), Date::new(2024, 12, 31));
edit.set_date(Date::new(2024, 6, 15));
edit.set_minimum_date(Date::new(2024, 2, 1));
edit.set_maximum_date(Date::new(2024, 11, 1));
assert_eq!(edit.date(), Date::new(2024, 6, 15), "an in-range value must not be nudged");
}
#[test]
fn typing_edits_the_active_section() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date(Date::new(2024, 6, 15));
assert!(!edit.type_digit('2'), "no section, no typing");
edit.set_current_section(Some(DateSection::Year));
for digit in ['2', '0', '2', '6'] {
assert!(edit.type_digit(digit));
}
assert_eq!(edit.date(), Date::new(2026, 6, 15));
edit.set_current_section(Some(DateSection::Month));
assert!(edit.type_digit('1'));
assert!(edit.type_digit('2'));
assert_eq!(edit.date(), Date::new(2026, 12, 15));
edit.set_current_section(Some(DateSection::Day));
assert!(edit.type_digit('3'));
assert_eq!(edit.date(), Date::new(2026, 12, 3));
assert!(!edit.type_digit('x'), "a non-digit is refused");
}
#[test]
fn stepping_a_section_wraps_the_month() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date(Date::new(2026, 12, 15));
edit.set_current_section(Some(DateSection::Month));
edit.step_current_section(1);
assert_eq!(edit.date(), Date::new(2027, 1, 15));
edit.step_current_section(-1);
assert_eq!(edit.date(), Date::new(2026, 12, 15));
edit.set_current_section(Some(DateSection::Year));
edit.step_current_section(-1);
assert_eq!(edit.date(), Date::new(2025, 12, 15));
}
#[test]
fn spin_buttons_are_opt_in_and_step_the_value() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date(Date::new(2026, 6, 15));
assert!(edit.spin_button_rects().is_none(), "off by default");
edit.set_spin_buttons(true);
let (up, _down) = edit.spin_button_rects().expect("the pair exists once enabled");
edit.handle_event(&Event::mouse_press(up.x + 2, up.y + 2, 1));
assert_eq!(edit.date(), Date::new(2026, 6, 16), "the button steps a whole day");
edit.set_current_section(Some(DateSection::Month));
edit.handle_event(&Event::mouse_press(up.x + 2, up.y + 2, 1));
assert_eq!(edit.date(), Date::new(2026, 7, 16), "and the section once one is active");
}
#[test]
fn the_wheel_steps_the_value() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date(Date::new(2026, 6, 15));
edit.handle_event(&Event::wheel(0, 1, 0));
assert_eq!(edit.date(), Date::new(2026, 6, 16), "wheel up increases");
edit.handle_event(&Event::wheel(0, -1, 0));
assert_eq!(edit.date(), Date::new(2026, 6, 15), "wheel down decreases");
}
#[test]
fn special_value_text_is_shown_at_the_minimum() {
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_date_range(Date::new(2026, 1, 1), Date::new(2026, 12, 31));
edit.set_date(Date::new(2026, 1, 1));
let plain = crate::widget::svg::render_to_svg(&mut edit);
edit.set_special_value_text(Some("Not set".to_string()));
let special = crate::widget::svg::render_to_svg(&mut edit);
assert_ne!(plain, special, "the special text must reach the painter");
assert_eq!(edit.special_value_text(), Some("Not set"));
edit.set_date(Date::new(2026, 2, 1));
assert_eq!(edit.date(), Date::new(2026, 2, 1));
}
#[test]
fn the_calendar_popup_reveals() {
use crate::widget::Widget;
let mut edit = DateEdit::new(Rect::new(0, 0, 200, 30));
edit.set_calendar_popup(true);
edit.set_calendar_open(true);
assert!(edit.is_animating(), "opening the grid starts a reveal");
for _ in 0..120 {
edit.tick(16);
}
assert!(!edit.is_animating());
assert!(edit.calendar_open());
}
#[test]
fn clicking_a_value_zone_selects_its_section() {
let mut edit = DateEdit::new(Rect::new(0, 0, 240, 30));
assert_eq!(edit.current_section(), None);
let month = edit.section_zone(DateSection::Month);
edit.handle_event(&Event::mouse_press(month.x + 2, month.y + 2, 1));
assert_eq!(edit.current_section(), Some(DateSection::Month));
let day = edit.section_zone(DateSection::Day);
edit.handle_event(&Event::mouse_press(day.x + 2, day.y + 2, 1));
assert_eq!(edit.current_section(), Some(DateSection::Day));
}
#[test]
fn the_edit_finishes_on_enter_and_on_focus_loss() {
use std::sync::{Arc, Mutex};
let mut edit = DateEdit::new(Rect::new(0, 0, 160, 28));
let finished = Arc::new(Mutex::new(0u32));
let sink = Arc::clone(&finished);
edit.editing_finished.connect(move || *sink.lock().unwrap() += 1);
edit.handle_event(&Event::key_press(crate::event::key_codes::ENTER, 0));
assert_eq!(*finished.lock().unwrap(), 1);
edit.handle_event(&Event::FocusLost);
assert_eq!(*finished.lock().unwrap(), 2);
}
}