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::advanced_widgets::{date_edit::Date, time_edit::Time};
use crate::widget::capability::coercion::{expect_bool, expect_datetime, 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_TIME_EDIT_COMMAND_ID: AtomicU64 = AtomicU64::new(1);
struct DateTimeEditCommand {
id: CommandId,
target: Rc<RefCell<DateTime>>,
before: DateTime,
after: DateTime,
}
impl DateTimeEditCommand {
fn new(target: Rc<RefCell<DateTime>>, before: DateTime, after: DateTime) -> Self {
Self {
id: CommandId(NEXT_DATE_TIME_EDIT_COMMAND_ID.fetch_add(1, Ordering::Relaxed)),
target,
before,
after,
}
}
}
impl UndoCommand for DateTimeEditCommand {
fn id(&self) -> CommandId {
self.id
}
fn description(&self) -> CommandDescription {
CommandDescription {
text: "Edit date-time".to_string(),
timestamp_ms: 0,
command_type: "date_time_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 DateTime {
pub date: Date,
pub time: Time,
}
impl DateTime {
pub fn new(date: Date, time: Time) -> Self {
Self { date, time }
}
pub fn is_valid(&self) -> bool {
self.date.is_valid() && self.time.is_valid()
}
}
impl std::fmt::Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.date, self.time)
}
}
pub struct DateTimeEdit {
base: BaseWidget,
datetime: DateTime,
minimum: DateTime,
maximum: DateTime,
display_format: String,
calendar_popup: bool,
pub datetime_changed: Signal1<DateTime>,
undo_stack: UndoStack,
history_target: Rc<RefCell<DateTime>>,
restoring_history: bool,
}
impl DateTimeEdit {
pub fn new(geometry: Rect) -> Self {
let min_dt = DateTime::new(Date::new(1752, 9, 14), Time::new(0, 0, 0, 0));
let max_dt = DateTime::new(Date::new(9999, 12, 31), Time::new(23, 59, 59, 999));
let now = DateTime::new(Date::today(), Time::new(0, 0, 0, 0));
Self {
base: BaseWidget::new(WidgetKind::DateTimePicker, geometry, "DateTimeEdit"),
datetime: now,
minimum: min_dt,
maximum: max_dt,
display_format: "yyyy-MM-dd HH:mm:ss".to_string(),
calendar_popup: false,
datetime_changed: Signal1::new(),
undo_stack: UndoStack::new(),
history_target: Rc::new(RefCell::new(now)),
restoring_history: false,
}
}
pub fn datetime(&self) -> DateTime {
self.datetime
}
pub fn date(&self) -> Date {
self.datetime.date
}
pub fn time(&self) -> Time {
self.datetime.time
}
pub fn minimum_datetime(&self) -> DateTime {
self.minimum
}
pub fn maximum_datetime(&self) -> DateTime {
self.maximum
}
pub fn display_format(&self) -> &str {
&self.display_format
}
pub fn calendar_popup(&self) -> bool {
self.calendar_popup
}
pub fn set_datetime(&mut self, dt: DateTime) {
if dt.is_valid() && dt >= self.minimum && dt <= self.maximum && self.datetime != dt {
let before = self.datetime;
self.datetime = dt;
if !self.restoring_history {
*self.history_target.borrow_mut() = self.datetime;
self.undo_stack.push(Box::new(DateTimeEditCommand::new(
self.history_target.clone(),
before,
self.datetime,
)));
}
self.datetime_changed.emit(dt);
self.base.request_redraw();
}
}
pub fn set_date(&mut self, date: Date) {
self.set_datetime(DateTime::new(date, self.datetime.time));
}
pub fn set_time(&mut self, time: Time) {
self.set_datetime(DateTime::new(self.datetime.date, time));
}
pub fn set_minimum_datetime(&mut self, dt: DateTime) {
self.minimum = dt;
self.clamp_to_range();
self.base.request_redraw();
}
pub fn set_maximum_datetime(&mut self, dt: DateTime) {
self.maximum = dt;
self.clamp_to_range();
self.base.request_redraw();
}
fn clamp_to_range(&mut self) {
self.datetime = super::clamp_ordered_range(self.datetime, 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 t = self.datetime.time;
let new_sec = t.second() + 1;
if new_sec >= 60 {
t.set_second(0);
let new_min = t.minute() + 1;
if new_min >= 60 {
t.set_minute(0);
let new_hour = t.hour() + 1;
if new_hour >= 24 {
t.set_hour(0);
let mut d = self.datetime.date;
let next_day = d.day() as u16 + 1;
if next_day > d.days_in_month() as u16 {
d.set_day(1);
let next_month = d.month() + 1;
if next_month > 12 {
d.set_month(1);
d.set_year(d.year() + 1);
} else {
d.set_month(next_month);
}
} else {
d.set_day(next_day as u8);
}
self.set_datetime(DateTime::new(d, t));
return;
} else {
t.set_hour(new_hour);
}
} else {
t.set_minute(new_min);
}
} else {
t.set_second(new_sec);
}
self.set_time(t);
}
pub fn step_down(&mut self) {
let mut t = self.datetime.time;
if t.second() > 0 {
t.set_second(t.second() - 1);
} else {
t.set_second(59);
if t.minute() > 0 {
t.set_minute(t.minute() - 1);
} else {
t.set_minute(59);
if t.hour() > 0 {
t.set_hour(t.hour() - 1);
} else {
t.set_hour(23);
let mut d = self.datetime.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_datetime(DateTime::new(d, t));
return;
}
}
}
self.set_time(t);
}
pub fn undo(&mut self) -> bool {
if self.undo_stack.undo().is_err() {
return false;
}
self.restore_history_datetime();
true
}
pub fn redo(&mut self) -> bool {
if self.undo_stack.redo().is_err() {
return false;
}
self.restore_history_datetime();
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_datetime(&mut self) {
self.restoring_history = true;
self.datetime = *self.history_target.borrow();
self.restoring_history = false;
self.datetime_changed.emit(self.datetime);
self.base.request_redraw();
}
}
impl Widget for DateTimeEdit {
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(160, 28)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for DateTimeEdit {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"datetime" => Ok(CapabilityValue::String(self.datetime().to_string())),
"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 {
"datetime" => {
self.set_datetime(expect_datetime(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!["datetime", "display_format", "calendar_popup", BASE_PROPERTY_NAMES]
}
}
impl EventHandler for DateTimeEdit {
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 DateTimeEdit {
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_time_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.datetime.date.year()),
month: Some(self.datetime.date.month()),
day: Some(self.datetime.date.day()),
hour: Some(self.datetime.time.hour()),
minute: Some(self.datetime.time.minute()),
second: Some(self.datetime.time.second()),
millisecond: Some(self.datetime.time.msec()),
},
)
.unwrap_or_else(|| self.datetime.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 DateTimeEdit {
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 date = self.datetime.date;
let first = Date::new(date.year(), 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 this = Date::new(date.year(), date.month(), day as u8);
let in_range = this >= self.minimum.date && this <= self.maximum.date;
if day == 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 crate::widget::svg::render_to_svg;
use std::sync::{Arc, Mutex};
#[test]
fn date_time_creation_and_accessors() {
let d = Date::new(2026, 6, 8);
let t = Time::new(14, 30, 15, 0);
let dt = DateTime::new(d, t);
assert_eq!(dt.date, d);
assert_eq!(dt.time, t);
assert!(dt.is_valid());
}
#[test]
fn date_time_is_valid() {
let valid = DateTime::new(Date::new(2026, 6, 8), Time::new(12, 0, 0, 0));
assert!(valid.is_valid());
let bad_date = DateTime::new(Date::new(2026, 13, 1), Time::new(12, 0, 0, 0));
assert!(!bad_date.is_valid());
let bad_time = DateTime::new(Date::new(2026, 6, 8), Time::new(25, 0, 0, 0));
assert!(bad_time.is_valid());
}
#[test]
fn date_time_display() {
let dt = DateTime::new(Date::new(2026, 6, 8), Time::new(9, 5, 3, 0));
assert_eq!(dt.to_string(), "2026-06-08 09:05:03");
}
#[test]
fn date_time_ordering() {
let early = DateTime::new(Date::new(2026, 6, 8), Time::new(8, 0, 0, 0));
let late = DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0));
assert!(early < late);
assert!(late > early);
assert_eq!(early, DateTime::new(Date::new(2026, 6, 8), Time::new(8, 0, 0, 0)));
}
#[test]
fn date_time_edit_creation_defaults() {
let editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
assert_eq!(editor.date(), Date::today());
assert_eq!(editor.time(), Time::new(0, 0, 0, 0));
assert_eq!(editor.display_format(), "yyyy-MM-dd HH:mm:ss");
assert!(!editor.calendar_popup());
assert!(editor.is_visible());
assert!(editor.is_enabled());
}
#[test]
fn date_time_edit_set_and_get_datetime() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let dt = DateTime::new(Date::new(2026, 12, 25), Time::new(10, 30, 0, 0));
editor.set_datetime(dt);
assert_eq!(editor.datetime(), dt);
assert_eq!(editor.date(), Date::new(2026, 12, 25));
assert_eq!(editor.time(), Time::new(10, 30, 0, 0));
}
#[test]
fn date_time_edit_set_date_part() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let new_date = Date::new(2027, 4, 15);
editor.set_date(new_date);
assert_eq!(editor.date(), new_date);
assert_eq!(editor.time(), Time::new(0, 0, 0, 0));
}
#[test]
fn date_time_edit_set_time_part() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_date(Date::new(2026, 6, 8));
let new_time = Time::new(18, 45, 30, 0);
editor.set_time(new_time);
assert_eq!(editor.time(), new_time);
assert_eq!(editor.date(), Date::new(2026, 6, 8));
}
#[test]
fn date_time_edit_set_display_format() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_display_format("dd/MM/yyyy HH:mm".to_string());
assert_eq!(editor.display_format(), "dd/MM/yyyy HH:mm");
}
#[test]
fn date_time_edit_set_minimum_maximum() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let min = DateTime::new(Date::new(2025, 1, 1), Time::new(0, 0, 0, 0));
let max = DateTime::new(Date::new(2030, 12, 31), Time::new(23, 59, 59, 999));
editor.set_minimum_datetime(min);
editor.set_maximum_datetime(max);
assert_eq!(editor.minimum_datetime(), min);
assert_eq!(editor.maximum_datetime(), max);
}
#[test]
fn date_time_edit_set_datetime_clamps_to_range() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let min = DateTime::new(Date::new(2025, 6, 1), Time::new(0, 0, 0, 0));
let max = DateTime::new(Date::new(2025, 6, 30), Time::new(23, 59, 59, 999));
editor.set_minimum_datetime(min);
editor.set_maximum_datetime(max);
let within = DateTime::new(Date::new(2025, 6, 15), Time::new(12, 0, 0, 0));
editor.set_datetime(within);
assert_eq!(editor.datetime(), within);
let before = DateTime::new(Date::new(2025, 5, 1), Time::new(0, 0, 0, 0));
editor.set_datetime(before);
assert_eq!(editor.datetime(), within);
let after = DateTime::new(Date::new(2025, 7, 1), Time::new(0, 0, 0, 0));
editor.set_datetime(after);
assert_eq!(editor.datetime(), within);
}
#[test]
fn date_time_edit_date_time_changed_signal_emits() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let captured =
Arc::new(Mutex::new(DateTime::new(Date::new(2000, 1, 1), Time::new(0, 0, 0, 0))));
let sink = captured.clone();
editor.datetime_changed.connect(move |dt| {
if let Ok(mut c) = sink.lock() {
*c = *dt;
}
});
let expected = DateTime::new(Date::new(2026, 12, 25), Time::new(10, 30, 0, 0));
editor.set_datetime(expected);
let got = *captured.lock().unwrap();
assert_eq!(got, expected);
}
#[test]
fn date_time_edit_date_time_changed_not_emitted_for_unchanged() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let hits = Arc::new(Mutex::new(0usize));
let hits_clone = hits.clone();
editor.datetime_changed.connect(move |_| {
if let Ok(mut h) = hits_clone.lock() {
*h += 1;
}
});
let default_dt = DateTime::new(Date::today(), Time::new(0, 0, 0, 0));
editor.set_datetime(default_dt);
assert_eq!(*hits.lock().unwrap(), 0);
}
#[test]
fn date_time_edit_calendar_popup_visibility() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
assert!(!editor.calendar_popup());
editor.set_calendar_popup(true);
assert!(editor.calendar_popup());
editor.set_calendar_popup(false);
assert!(!editor.calendar_popup());
}
#[test]
fn date_time_edit_geometry_delegation() {
let rect = Rect::new(10, 20, 280, 30);
let editor = DateTimeEdit::new(rect);
assert_eq!(editor.geometry(), rect);
assert_eq!(editor.position(), Point::new(10, 20));
assert_eq!(editor.size(), crate::core::Size::new(280, 30));
}
#[test]
fn date_time_edit_widget_kind() {
let editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
assert_eq!(editor.kind(), WidgetKind::DateTimePicker);
}
#[test]
fn date_time_edit_ids_are_unique() {
let a = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let b = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
assert_ne!(a.id(), b.id());
}
#[test]
fn date_time_edit_svg_output() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(12, 30, 45, 0)));
let svg = render_to_svg(&mut editor);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
assert!(svg.contains("width=\"280\""));
assert!(svg.contains("height=\"30\""));
assert!(svg.contains("2026-06-08 12:30:45") || svg.contains("fill="));
}
#[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 = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(12, 30, 45, 0)));
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,
"the popup must add ink: closed={closed_ink} open={open_ink}"
);
assert!(
open_ink >= 30,
"the popup must paint the month's days, not a stub: 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_time_edit_disabled_state_blocks_changes() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0)));
editor.set_enabled(false);
assert!(!editor.is_enabled());
editor.handle_event(&Event::KeyPress { key: 38, modifiers: 0 });
assert_eq!(editor.datetime(), DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0)));
}
#[test]
fn date_time_edit_date_selection_updates_datetime() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let new_date = Date::new(2027, 12, 25);
editor.set_date(new_date);
assert_eq!(editor.date(), new_date);
assert_eq!(editor.time(), Time::new(0, 0, 0, 0));
}
#[test]
fn date_time_edit_time_part_updates_independently() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(14, 30, 0, 0)));
editor.set_time(Time::new(20, 15, 45, 0));
assert_eq!(editor.time(), Time::new(20, 15, 45, 0));
assert_eq!(editor.date(), Date::new(2026, 6, 8));
}
#[test]
fn date_time_edit_step_up_increments_time() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(10, 30, 15, 0)));
editor.step_up();
assert_eq!(editor.time(), Time::new(10, 30, 16, 0));
assert_eq!(editor.date(), Date::new(2026, 6, 8));
}
#[test]
fn date_time_edit_step_down_decrements_time() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(10, 30, 15, 0)));
editor.step_down();
assert_eq!(editor.time(), Time::new(10, 30, 14, 0));
assert_eq!(editor.date(), Date::new(2026, 6, 8));
}
#[test]
fn date_time_edit_step_up_rolls_to_next_day() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(23, 59, 59, 0)));
editor.step_up();
assert_eq!(editor.time(), Time::new(0, 0, 0, 0));
assert_eq!(editor.date(), Date::new(2026, 6, 9));
}
#[test]
fn date_time_edit_step_down_rolls_to_prev_day() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(0, 0, 0, 0)));
editor.step_down();
assert_eq!(editor.time(), Time::new(23, 59, 59, 0));
assert_eq!(editor.date(), Date::new(2026, 6, 7));
}
#[test]
fn date_time_edit_undo_redo_restores_datetime() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let first = DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0));
let second = DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 1, 0));
editor.set_datetime(first);
editor.set_datetime(second);
assert!(editor.can_undo());
assert!(editor.undo());
assert_eq!(editor.datetime(), first);
assert!(editor.can_redo());
assert!(editor.redo());
assert_eq!(editor.datetime(), second);
}
#[test]
fn date_time_edit_keyboard_shortcuts_drive_history() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
let first = DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0));
let second = DateTime::new(Date::new(2026, 6, 9), Time::new(11, 0, 0, 0));
editor.set_datetime(first);
editor.set_datetime(second);
editor.handle_event(&Event::KeyPress { key: 90, modifiers: 2 });
assert_eq!(editor.datetime(), first);
editor.handle_event(&Event::KeyPress { key: 89, modifiers: 2 });
assert_eq!(editor.datetime(), second);
}
#[test]
fn date_time_edit_keyboard_navigation() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
editor.set_datetime(DateTime::new(Date::new(2026, 6, 8), Time::new(10, 0, 0, 0)));
editor.handle_event(&Event::KeyPress { key: 38, modifiers: 0 });
assert_eq!(editor.time(), Time::new(10, 0, 1, 0));
editor.handle_event(&Event::KeyPress { key: 40, modifiers: 0 });
assert_eq!(editor.time(), Time::new(10, 0, 0, 0));
}
#[test]
fn date_time_edit_visibility_toggle() {
let mut editor = DateTimeEdit::new(Rect::new(0, 0, 280, 30));
assert!(editor.is_visible());
editor.set_visible(false);
assert!(!editor.is_visible());
editor.set_visible(true);
assert!(editor.is_visible());
}
}