use crate::core::{Color, Font, HorizontalAlignment, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::undo::{CommandDescription, CommandId, UndoCommand, UndoStack};
use crate::widget::capability::access::date_to_string;
use crate::widget::capability::coercion::{expect_bool, expect_date, expect_string};
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::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_DATE_EDIT_COMMAND_ID: AtomicU64 = AtomicU64::new(1);
struct DateEditCommand {
id: CommandId,
target: Rc<RefCell<Date>>,
before: Date,
after: Date,
}
impl DateEditCommand {
fn new(target: Rc<RefCell<Date>>, before: Date, after: Date) -> Self {
Self {
id: CommandId(NEXT_DATE_EDIT_COMMAND_ID.fetch_add(1, Ordering::Relaxed)),
target,
before,
after,
}
}
}
impl UndoCommand for DateEditCommand {
fn id(&self) -> CommandId {
self.id
}
fn description(&self) -> CommandDescription {
CommandDescription {
text: "Edit date".to_string(),
timestamp_ms: 0,
command_type: "date_edit",
}
}
fn execute(&mut self) -> Result<(), String> {
*self.target.borrow_mut() = self.after;
Ok(())
}
fn undo(&mut self) -> Result<(), String> {
*self.target.borrow_mut() = self.before;
Ok(())
}
}
#[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(pattern: &str, components: DateTimeComponents) -> Option<String> {
type Token = (&'static str, fn(&DateTimeComponents) -> Option<String>);
const TOKENS: [Token; 11] = [
("yyyy", |c| c.year.map(|y| format!("{y:04}"))),
("SSS", |c| c.millisecond.map(|ms| format!("{ms:03}"))),
("yy", |c| c.year.map(|y| format!("{:02}", y.rem_euclid(100)))),
("MM", |c| c.month.map(|m| format!("{m:02}"))),
("dd", |c| c.day.map(|d| format!("{d:02}"))),
("HH", |c| c.hour.map(|h| format!("{h:02}"))),
("mm", |c| c.minute.map(|m| format!("{m:02}"))),
("ss", |c| c.second.map(|s| format!("{s:02}"))),
("M", |c| c.month.map(|m| m.to_string())),
("d", |c| c.day.map(|d| d.to_string())),
("H", |c| c.hour.map(|h| h.to_string())),
];
let mut out = crate::compat::String::new();
let mut rest = pattern;
let mut substituted = false;
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 let Some(text) = render(&components) {
out.push_str(&text);
substituted = true;
}
rest = after;
at_token_start = true;
continue 'scan;
}
}
}
let ch = rest.chars().next().expect("rest is non-empty, so there is a next char");
out.push(ch);
rest = &rest[ch.len_utf8()..];
at_token_start = !ch.is_alphabetic();
}
if substituted {
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>,
}
pub struct DateEdit {
base: BaseWidget,
date: Date,
minimum: Date,
maximum: Date,
display_format: String,
calendar_popup: bool,
pub date_changed: Signal1<Date>,
undo_stack: UndoStack,
history_target: Rc<RefCell<Date>>,
restoring_history: bool,
}
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,
date_changed: Signal1::new(),
undo_stack: UndoStack::new(),
history_target: Rc::new(RefCell::new(Date::today())),
restoring_history: false,
}
}
pub fn date(&self) -> Date {
self.date
}
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 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(DateEditCommand::new(
self.history_target.clone(),
before,
self.date,
)));
}
self.date_changed.emit(date);
self.base.request_redraw();
}
}
pub fn set_minimum_date(&mut self, date: Date) {
self.minimum = date;
self.clamp_to_range();
self.base.request_redraw();
}
pub fn set_maximum_date(&mut self, date: Date) {
self.maximum = date;
self.clamp_to_range();
self.base.request_redraw();
}
pub fn set_date_range(&mut self, min: Date, max: Date) {
self.minimum = min;
self.maximum = max;
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) {
self.display_format = fmt;
self.base.request_redraw();
}
pub fn set_calendar_popup(&mut self, popup: bool) {
self.calendar_popup = popup;
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 base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
crate::core::Size::new(120, 28)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
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())),
_ => 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(())
}
_ => 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",
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 let Event::KeyPress { key, modifiers } = event {
match *key {
90 if *modifiers == 2 => {
let _ = self.undo();
}
89 if *modifiers == 2 => {
let _ = self.redo();
}
38 => self.step_up(), 40 => self.step_down(), _ => { }
}
}
}
}
impl Draw for DateEdit {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let style = self.base.style().clone();
let theme = crate::style::resolved_theme_style("date_edit");
let (window_fill, foreground) = {
let manager = crate::style::theme_manager();
match manager.current_theme() {
Some(active) => (active.colors.background, active.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.fill_rect(rect, surface);
context.draw_rect(rect, border);
let text = super::date_edit::format_with_pattern(
&self.display_format,
super::date_edit::DateTimeComponents {
year: Some(self.date.year()),
month: Some(self.date.month()),
day: Some(self.date.day()),
..Default::default()
},
)
.unwrap_or_else(|| self.date.to_string());
let font = Font::default();
let line = context.text_line(rect, &font);
context.draw_text_fitted(
Rect {
x: rect.x + 6,
y: line.y,
width: rect.width.saturating_sub(12),
height: line.height,
},
&text,
&font,
ink,
HorizontalAlignment::Left,
);
if self.calendar_popup {
self.draw_calendar_popup(context, rect, surface, border, ink);
}
}
}
const POPUP_CELL_W: u32 = 20;
const POPUP_CELL_H: u32 = 16;
const POPUP_GAP: i32 = 4;
const WEEKDAY_INITIALS: [&str; 7] = ["S", "M", "T", "W", "T", "F", "S"];
impl DateEdit {
fn draw_calendar_popup(
&self,
context: &mut RenderContext,
field: Rect,
surface: Color,
border: Color,
ink: Color,
) {
let grid_w = POPUP_CELL_W * 7;
let grid_h = POPUP_CELL_H * 7;
let popup = Rect::new(
field.x,
field.y + field.height as i32 + POPUP_GAP,
grid_w.max(field.width),
grid_h,
);
let plate = surface.blend(&ink, 0.10);
context.fill_rect(popup, plate);
context.draw_rect(popup, border);
let font = Font::default();
let muted = plate.blend(&ink, 0.45);
for (col, initial) in WEEKDAY_INITIALS.iter().enumerate() {
let cell = Rect::new(
popup.x + (col as u32 * POPUP_CELL_W) as i32,
popup.y,
POPUP_CELL_W,
POPUP_CELL_H,
);
context.draw_text_line(cell, initial, &font, muted, HorizontalAlignment::Center);
}
let first = Date::new(self.date.year(), self.date.month(), 1);
let leading = first.weekday() as u32;
let days = first.days_in_month() as u32;
for day in 1..=days {
let slot = leading + day - 1;
let col = slot % 7;
let row = slot / 7;
if row > 5 {
break;
}
let cell = Rect::new(
popup.x + (col * POPUP_CELL_W) as i32,
popup.y + POPUP_CELL_H as i32 + (row * POPUP_CELL_H) as i32,
POPUP_CELL_W,
POPUP_CELL_H,
);
let date = Date::new(self.date.year(), self.date.month(), day as u8);
let in_range = date >= self.minimum && date <= self.maximum;
if day == self.date.day() as u32 {
let accent = crate::style::resolved_theme_style("slider")
.and_then(|style| style.background_color)
.unwrap_or_else(|| plate.blend(&ink, 0.55));
context.fill_rect(cell, accent);
context.draw_text_line(
cell,
&day.to_string(),
&font,
accent.contrast_color(),
HorizontalAlignment::Center,
);
} else {
let day_ink = if in_range { ink } else { muted };
context.draw_text_line(
cell,
&day.to_string(),
&font,
day_ink,
HorizontalAlignment::Center,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
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);
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 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: 2 });
assert_eq!(editor.date(), Date::new(2026, 6, 8));
editor.handle_event(&Event::KeyPress { key: 89, modifiers: 2 });
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");
}
}