use std::cell::Cell;
use std::sync::Arc;
use rich::{Segment, Style};
use crate::component::{Component, Context, Flow, View};
use crate::components::{Theme, fit, plain, question, text};
use crate::compose::{FOCUS_NEXT, FOCUS_PREVIOUS};
use crate::datetime::{Date, DateTime, Time};
use crate::event::{Button, Event, Key, KeyCode, MouseKind};
use crate::keymap::{Keymap, keys};
use crate::policy::{LineIo, NotInteractive};
pub fn date_keymap() -> Keymap {
Keymap::new("date")
.bind("pick", keys("enter"), "pick")
.bind("cancel", keys("escape"), "cancel")
.bind("left", keys("left h"), "a day earlier; the field before")
.bind("right", keys("right l"), "a day later; the next field")
.bind("up", keys("up k"), "a week earlier; the field up")
.bind("down", keys("down j"), "a week later; the field down")
.bind("previous-month", keys("pageup ["), "a month earlier")
.bind("next-month", keys("pagedown ]"), "a month later")
.bind("previous-year", keys("shift+pageup {"), "a year earlier")
.bind("next-year", keys("shift+pagedown }"), "a year later")
.bind("first-day", keys("home"), "the month's first day")
.bind("last-day", keys("end"), "the month's last day")
.bind("today", keys("t"), "today")
.bind(FOCUS_NEXT, keys("tab"), "the next part")
.bind(FOCUS_PREVIOUS, keys("shift+tab"), "the part before")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Mode {
Date,
Time,
DateTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Part {
Grid,
Hour,
Minute,
Second,
Half,
}
type Check = Arc<dyn Fn(DateTime) -> Result<(), String> + Send + Sync>;
const WEEKS: usize = 6;
const GRID_WIDTH: usize = 23;
const GRID_TOP: usize = 3;
const FIRST_DAY: Date = Date::new(1, 1, 1);
const LAST_DAY: Date = Date::new(9999, 12, 31);
struct Chooser {
prompt: String,
mode: Mode,
value: DateTime,
value_given: bool,
min: Option<DateTime>,
max: Option<DateTime>,
seconds: bool,
twelve_hour: bool,
today: Date,
part: Part,
typed: String,
message: Option<String>,
check: Option<Check>,
default: Option<DateTime>,
theme: Theme,
mouse: bool,
keymap: Keymap,
clicked: Option<Date>,
answer: Option<Option<String>>,
width: Cell<usize>,
}
impl Chooser {
fn new(prompt: String, mode: Mode) -> Chooser {
let now = DateTime::now_utc();
let value = match mode {
Mode::Date => DateTime::new(now.date, Time::MIDNIGHT),
Mode::Time | Mode::DateTime => {
DateTime::new(now.date, Time::new(now.time.hour, now.time.minute, 0))
}
};
Chooser {
prompt,
mode,
value,
value_given: false,
min: None,
max: None,
seconds: false,
twelve_hour: false,
today: now.date,
part: if mode == Mode::Time {
Part::Hour
} else {
Part::Grid
},
typed: String::new(),
message: None,
check: None,
default: None,
theme: Theme::default(),
mouse: false,
keymap: date_keymap(),
clicked: None,
answer: None,
width: Cell::new(80),
}
}
fn has_date(&self) -> bool {
self.mode != Mode::Time
}
fn has_time(&self) -> bool {
self.mode != Mode::Date
}
fn parts(&self) -> Vec<Part> {
let mut parts = Vec::new();
if self.has_date() {
parts.push(Part::Grid);
}
if self.has_time() {
parts.extend([Part::Hour, Part::Minute]);
if self.seconds {
parts.push(Part::Second);
}
if self.twelve_hour {
parts.push(Part::Half);
}
}
parts
}
fn to_the_minute(&self) -> bool {
self.has_time() && !self.seconds
}
fn shown(&self, at: DateTime) -> DateTime {
if !self.to_the_minute() {
return at;
}
DateTime::new(
at.date,
Time {
second: 0,
..at.time
},
)
}
fn earliest(&self) -> Option<DateTime> {
let min = self.min?;
if !self.to_the_minute() || min.time.second == 0 {
return Some(min);
}
let minute = i64::from(min.time.seconds() - min.time.second);
let time = Time::from_seconds(minute + 60);
let date = if time == Time::MIDNIGHT {
min.date.add_days(1)
} else {
min.date
};
Some(DateTime::new(date, time))
}
fn latest(&self) -> Option<DateTime> {
self.max.map(|max| self.shown(max))
}
fn earliest_text(&self) -> Option<String> {
let (min, earliest) = (self.min?, self.earliest()?);
Some(if self.mode == Mode::Time && earliest.date != min.date {
min.time.format(true, self.twelve_hour)
} else {
self.format(earliest)
})
}
fn day_allowed(&self, date: Date) -> bool {
(FIRST_DAY..=LAST_DAY).contains(&date)
&& self.earliest().is_none_or(|min| date >= min.date)
&& self.latest().is_none_or(|max| date <= max.date)
}
fn clamp_day(&self, date: Date) -> Date {
let mut date = date;
if let Some(min) = self.earliest() {
date = date.max(min.date);
}
if let Some(max) = self.latest() {
date = date.min(max.date);
}
date.clamp(FIRST_DAY, LAST_DAY)
}
fn key_of(&self, at: DateTime) -> (Option<Date>, Option<Time>) {
match self.mode {
Mode::Date => (Some(at.date), None),
Mode::Time => (None, Some(at.time)),
Mode::DateTime => (Some(at.date), Some(at.time)),
}
}
fn format(&self, at: DateTime) -> String {
match self.mode {
Mode::Date => at.date.to_string(),
Mode::Time => at.time.format(self.seconds, self.twelve_hour),
Mode::DateTime => at.format(self.seconds, self.twelve_hour),
}
}
fn refusal(&self, at: DateTime) -> Option<String> {
let at = self.shown(at);
if let (Some(earliest), Some(latest)) = (self.earliest(), self.latest())
&& self.key_of(earliest) > self.key_of(latest)
{
return Some(format!(
"the range is empty: the earliest, {}, is after the latest, {}",
self.earliest_text().unwrap_or_default(),
self.format(latest)
));
}
let at_key = self.key_of(at);
if let Some(min) = self.min
&& at_key < self.key_of(min)
{
let earliest = self.earliest_text().unwrap_or_default();
return Some(format!("the earliest is {earliest}"));
}
if let Some(max) = self.latest()
&& at_key > self.key_of(max)
{
return Some(format!("the latest is {}", self.format(max)));
}
self.check.as_ref().and_then(|check| check(at).err())
}
fn set_day(&mut self, date: Date) {
self.value.date = self.clamp_day(date);
}
fn step_field(&mut self, delta: i64) {
let time = self.value.time;
let wrap =
|value: u32, by: i64, modulus: i64| (value as i64 + by).rem_euclid(modulus) as u32;
self.value.time = match self.part {
Part::Hour => Time {
hour: wrap(time.hour, delta, 24),
..time
},
Part::Minute => Time {
minute: wrap(time.minute, delta, 60),
..time
},
Part::Second => Time {
second: wrap(time.second, delta, 60),
..time
},
Part::Half => Time {
hour: (time.hour + 12) % 24,
..time
},
Part::Grid => time,
};
}
fn type_digit(&mut self, digit: char) {
self.typed.push(digit);
let number: u32 = self.typed.parse().unwrap_or(0);
let time = self.value.time;
let (most, full) = match self.part {
Part::Hour if self.twelve_hour => (12, number >= 2 || self.typed.len() == 2),
Part::Hour => (23, number >= 3 || self.typed.len() == 2),
_ => (59, number >= 6 || self.typed.len() == 2),
};
let number = number.min(most);
self.value.time = match self.part {
Part::Hour if self.twelve_hour => {
let pm = time.hour >= 12;
let hour = if self.typed.len() == 2 || full {
number.max(1) % 12
} else {
number % 12
};
Time {
hour: hour + if pm { 12 } else { 0 },
..time
}
}
Part::Hour => Time {
hour: number,
..time
},
Part::Minute => Time {
minute: number,
..time
},
Part::Second => Time {
second: number,
..time
},
_ => time,
};
if full {
self.typed.clear();
self.move_part(1);
}
}
fn part_index(&self, parts: &[Part]) -> usize {
parts
.iter()
.position(|part| *part == self.part)
.unwrap_or(0)
}
fn move_part(&mut self, by: isize) {
let parts = self.parts();
let count = parts.len() as isize;
let first = if self.has_date() { 1 } else { 0 };
let next = (self.part_index(&parts) as isize + by).clamp(first.min(count - 1), count - 1);
self.part = parts[next as usize];
self.typed.clear();
}
fn step_part(&mut self, forward: bool) -> bool {
let parts = self.parts();
let at = self.part_index(&parts);
let next = if forward {
Some(at + 1)
} else {
at.checked_sub(1)
};
match next.and_then(|next| parts.get(next)) {
Some(part) => {
self.part = *part;
self.typed.clear();
true
}
None => false,
}
}
fn enter(&mut self, forward: bool) {
let parts = self.parts();
if let Some(part) = if forward { parts.first() } else { parts.last() } {
self.part = *part;
}
self.typed.clear();
}
fn pick(&mut self) -> Flow<DateTime> {
let value = self.shown(self.value);
if let Some(why) = self.refusal(value) {
self.message = Some(why);
return Flow::Continue;
}
self.value = value;
self.answer = Some(Some(self.format(value)));
Flow::Done(value)
}
fn act(&mut self, action: &str) -> Flow<DateTime> {
let on_grid = self.part == Part::Grid;
let day = self.value.date;
match action {
"pick" => return self.pick(),
"cancel" => {
self.answer = Some(None);
return Flow::Cancel;
}
FOCUS_NEXT | FOCUS_PREVIOUS => {
if !self.step_part(action == FOCUS_NEXT) {
return Flow::Ignored;
}
}
"left" if on_grid => self.set_day(day.add_days(-1)),
"right" if on_grid => self.set_day(day.add_days(1)),
"up" if on_grid => self.set_day(day.add_days(-7)),
"down" if on_grid => self.set_day(day.add_days(7)),
"left" => self.move_part(-1),
"right" => self.move_part(1),
"up" => self.step_field(1),
"down" => self.step_field(-1),
"previous-month" if self.has_date() => self.set_day(day.add_months(-1)),
"next-month" if self.has_date() => self.set_day(day.add_months(1)),
"previous-year" if self.has_date() => self.set_day(day.add_months(-12)),
"next-year" if self.has_date() => self.set_day(day.add_months(12)),
"first-day" if on_grid => self.set_day(day.first_of_month()),
"last-day" if on_grid => self.set_day(day.last_of_month()),
"today" if self.has_date() => self.set_day(self.today),
_ => return Flow::Ignored,
}
Flow::Continue
}
fn key(&mut self, key: Key) -> Flow<DateTime> {
let plain_key = !key.modifiers.ctrl && !key.modifiers.alt;
if plain_key && self.part != Part::Grid {
match key.code {
KeyCode::Char(c @ '0'..='9') if self.part != Part::Half => {
self.type_digit(c);
return Flow::Continue;
}
KeyCode::Char(c @ ('a' | 'A' | 'p' | 'P')) if self.twelve_hour => {
let pm = c.eq_ignore_ascii_case(&'p');
let hour = self.value.time.hour % 12 + if pm { 12 } else { 0 };
self.value.time.hour = hour;
return Flow::Continue;
}
_ => {}
}
}
let Some(action) = self.keymap.action(key).map(str::to_string) else {
return Flow::Ignored;
};
self.typed.clear();
self.act(&action)
}
fn time_row(&self) -> usize {
if self.has_date() { GRID_TOP + WEEKS } else { 1 }
}
fn day_at(&self, column: usize, row: usize) -> Option<Date> {
if !self.has_date() || !(GRID_TOP..GRID_TOP + WEEKS).contains(&row) || column < 2 {
return None;
}
let cell = (column - 1) / 3;
if cell >= 7 {
return None;
}
let first = self.value.date.first_of_month();
let at = (row - GRID_TOP) * 7 + cell;
let day = (at + 1).checked_sub(first.weekday() as usize)?;
(1..=Date::days_in_month(first.year, first.month) as usize)
.contains(&day)
.then_some(Date {
day: day as u32,
..first
})
}
fn field_at(&self, column: usize) -> Option<Part> {
let mut at = self.time_label().len();
for (part, width) in self.time_fields() {
if (at..at + width).contains(&column) {
return Some(part);
}
at += width + 1;
}
None
}
fn mouse_event(&mut self, kind: MouseKind, column: usize, row: usize) -> Flow<DateTime> {
let over_grid = self.has_date() && row < GRID_TOP + WEEKS;
match kind {
MouseKind::Down(Button::Left) => {
if let Some(day) = self.day_at(column, row) {
if !self.day_allowed(day) {
return Flow::Continue;
}
self.part = Part::Grid;
if self.clicked == Some(day) && self.value.date == day {
return self.pick();
}
self.value.date = day;
self.clicked = Some(day);
} else if self.has_time()
&& row == self.time_row()
&& let Some(part) = self.field_at(column)
{
self.part = part;
self.typed.clear();
}
}
MouseKind::ScrollUp | MouseKind::ScrollDown => {
let by = if kind == MouseKind::ScrollUp { -1 } else { 1 };
if over_grid {
self.set_day(self.value.date.add_months(by));
} else if self.has_time() && self.part != Part::Grid {
self.step_field(-(by as i64));
}
}
_ => return Flow::Ignored,
}
Flow::Continue
}
fn event(&mut self, event: &Event) -> Flow<DateTime> {
if let Some(mouse) = event.mouse() {
self.message = None;
return self.mouse_event(mouse.kind, mouse.column as usize, mouse.row as usize);
}
match event.key() {
Some(key) => {
self.message = None;
self.key(key)
}
None => Flow::Ignored,
}
}
fn time_label(&self) -> &'static str {
if self.has_date() { " Time " } else { " " }
}
fn time_fields(&self) -> Vec<(Part, usize)> {
let mut fields = vec![(Part::Hour, 2), (Part::Minute, 2)];
if self.seconds {
fields.push((Part::Second, 2));
}
if self.twelve_hour {
fields.push((Part::Half, 2));
}
fields
.into_iter()
.map(|(part, width)| (part, width + if part == self.part { 2 } else { 0 }))
.collect()
}
fn time_line(&self, width: usize) -> Vec<Segment> {
let theme = &self.theme;
let time = self.value.time;
let out = self
.refusal(self.value)
.is_some_and(|_| self.min.is_some() || self.max.is_some());
let reverse = Style::parse("reverse").expect("a built-in style");
let mut line = vec![plain(self.time_label())];
let (hour, pm) = time.twelve_hour();
for (index, (part, _)) in self.time_fields().into_iter().enumerate() {
if index > 0 {
line.push(plain(if part == Part::Half { " " } else { ":" }));
}
let shown = match part {
Part::Hour if self.twelve_hour => format!("{hour:02}"),
Part::Hour => format!("{:02}", time.hour),
Part::Minute => format!("{:02}", time.minute),
Part::Second => format!("{:02}", time.second),
_ => (if pm { "PM" } else { "AM" }).to_string(),
};
let shown = if part == self.part && !self.typed.is_empty() {
format!("{:_<2}", self.typed)
} else {
shown
};
if part == self.part {
line.push(text(format!("[{shown}]"), &theme.focused.combine(&reverse)));
} else if out {
line.push(text(shown, &theme.error));
} else {
line.push(plain(shown));
}
}
fit(line, width)
}
fn grid_lines(&self, width: usize) -> Vec<Vec<Segment>> {
let theme = &self.theme;
let selected = self.value.date;
let first = selected.first_of_month();
let title = format!("{} {}", selected.month_name(), selected.year);
let pad = GRID_WIDTH.saturating_sub(title.len()) / 2;
let title_style = Style::parse("bold").expect("a built-in style");
let mut lines = vec![
fit(
vec![plain(" ".repeat(pad)), text(title, &title_style)],
width,
),
fit(vec![text(" Mo Tu We Th Fr Sa Su", &theme.hint)], width),
];
let reverse = Style::parse("reverse").expect("a built-in style");
let underline = Style::parse("underline").expect("a built-in style");
let today_style = Style::parse("bold").expect("a built-in style");
let chosen = if self.part == Part::Grid {
theme.focused.combine(&reverse)
} else {
underline
};
let offset = first.weekday() as usize;
let days = Date::days_in_month(first.year, first.month) as usize;
for week in 0..WEEKS {
let mut line = vec![plain(" ")];
for cell in 0..7 {
let at = week * 7 + cell;
let day = (at + 1)
.checked_sub(offset)
.filter(|day| (1..=days).contains(day));
let is = |cell: usize| {
(week * 7 + cell + 1)
.checked_sub(offset)
.is_some_and(|day| day == selected.day as usize)
};
line.push(plain(if is(cell) {
"["
} else if cell > 0 && is(cell - 1) {
"]"
} else {
" "
}));
let Some(day) = day else {
line.push(plain(" "));
continue;
};
let date = Date {
day: day as u32,
..first
};
let label = format!("{day:>2}");
line.push(if date == selected {
text(label, &chosen)
} else if !self.day_allowed(date) {
text(label, &theme.hint)
} else if date == self.today {
text(label, &today_style)
} else {
plain(label)
});
}
if is_last_selected(week, offset, selected.day) {
line.push(plain("]"));
}
lines.push(fit(line, width));
}
lines
}
fn hint(&self) -> String {
let mut hint = String::from(" ");
hint.push_str(match self.part {
Part::Grid => "←→↑↓ day · pgup/pgdn month · t today",
_ => "←→ field · ↑↓ change · type digits",
});
if self.parts().len() > 1 {
hint.push_str(match (self.part, self.mode) {
(Part::Grid, Mode::DateTime) => " · tab time",
(_, Mode::DateTime) => " · tab date",
_ => " · tab next",
});
}
hint.push_str(" · enter pick · esc cancel");
hint
}
fn range_line(&self) -> Option<String> {
let latest = self.latest().map(|at| self.format(at));
match (self.earliest_text(), latest) {
(None, None) => None,
(Some(min), Some(max)) => Some(format!(" from {min} to {max}")),
(Some(min), None) => Some(format!(" from {min}")),
(None, Some(max)) => Some(format!(" until {max}")),
}
}
fn render(&self, context: &Context<'_>) -> View {
let width = context.width;
self.width.set(width);
let theme = &self.theme;
let mut header = question(theme, &self.prompt);
if let Some(answer) = &self.answer {
header.push(match answer {
Some(answer) => text(answer.clone(), &theme.answer),
None => text("cancelled", &theme.hint),
});
return View::new(vec![fit(header, width)]);
}
header.push(plain(self.format(self.value)));
let mut lines = vec![fit(header, width)];
if self.has_date() {
lines.extend(self.grid_lines(width));
}
if self.has_time() {
lines.push(self.time_line(width));
}
if let Some(range) = self.range_line() {
lines.push(fit(vec![text(range, &theme.hint)], width));
}
lines.push(fit(vec![text(self.hint(), &theme.hint)], width));
if let Some(message) = &self.message {
lines.push(fit(
vec![text(format!(" ✗ {message}"), &theme.error)],
width,
));
}
View::new(lines)
}
fn shape(&self) -> &'static str {
match (self.mode, self.seconds) {
(Mode::Date, _) => "YYYY-MM-DD",
(Mode::Time, false) => "HH:MM",
(Mode::Time, true) => "HH:MM:SS",
(Mode::DateTime, false) => "YYYY-MM-DD HH:MM",
(Mode::DateTime, true) => "YYYY-MM-DD HH:MM:SS",
}
}
fn parse(&self, line: &str) -> Result<DateTime, String> {
match self.mode {
Mode::Date => line.parse::<Date>().map(DateTime::from),
Mode::Time => line
.parse::<Time>()
.map(|time| DateTime::new(self.value.date, time)),
Mode::DateTime => line.parse::<DateTime>(),
}
}
fn ask(&mut self, io: &mut dyn LineIo) -> Result<Option<DateTime>, NotInteractive> {
let mut prompt = format!("{} ({}", self.prompt, self.shape());
if let Some(range) = self.range_line() {
prompt.push_str(&format!(", {}", range.trim()));
}
prompt.push(')');
if let Some(default) = self.default {
prompt.push_str(&format!(" [{}]", self.format(default)));
}
io.write(&format!("{prompt}: "));
let line = match io.read_line() {
Some(line) if !line.trim().is_empty() => line,
ended => {
return match self.default {
Some(default) => self.checked(default).map(Some),
None if ended.is_none() => Err(NotInteractive::Ended),
None => Err(NotInteractive::Invalid(format!(
"no answer given ({})",
self.shape()
))),
};
}
};
let at = self.parse(&line).map_err(NotInteractive::Invalid)?;
self.checked(at).map(Some)
}
fn checked(&self, at: DateTime) -> Result<DateTime, NotInteractive> {
let at = self.shown(at);
match self.refusal(at) {
Some(why) => Err(NotInteractive::Invalid(format!(
"{}: {why}",
self.format(at)
))),
None => Ok(at),
}
}
}
fn is_last_selected(week: usize, offset: usize, day: u32) -> bool {
(week * 7 + 7).checked_sub(offset) == Some(day as usize)
}
macro_rules! shared_builders {
() => {
pub fn theme(mut self, theme: Theme) -> Self {
self.0.theme = theme;
self
}
pub fn with_mouse(mut self, on: bool) -> Self {
self.0.mouse = on;
self
}
pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
self.0.keymap.rebind(action, keys);
self
}
};
}
macro_rules! time_builders {
() => {
pub fn seconds(mut self, on: bool) -> Self {
self.0.seconds = on;
self
}
pub fn twelve_hour(mut self, on: bool) -> Self {
self.0.twelve_hour = on;
self
}
};
}
macro_rules! date_builders {
() => {
pub fn today(mut self, today: Date) -> Self {
self.0.today = today;
self
}
};
}
#[must_use = "a component does nothing until it is run or mounted"]
pub struct DatePicker(Chooser);
impl DatePicker {
pub fn new(prompt: impl Into<String>) -> DatePicker {
DatePicker(Chooser::new(prompt.into(), Mode::Date))
}
shared_builders!();
date_builders!();
pub fn value(mut self, date: Date) -> Self {
self.0.value.date = date;
self.0.value_given = true;
self
}
pub fn default(mut self, date: Date) -> Self {
if !self.0.value_given {
self.0.value.date = date;
}
self.0.default = Some(date.into());
self
}
pub fn min(mut self, date: Date) -> Self {
self.0.min = Some(date.at(Time::MIDNIGHT));
self
}
pub fn max(mut self, date: Date) -> Self {
self.0.max = Some(date.at(Time::LAST));
self
}
pub fn validate(
mut self,
check: impl Fn(Date) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
self.0.check = Some(Arc::new(move |at: DateTime| check(at.date)));
self
}
pub fn selected(&self) -> Date {
self.0.value.date
}
}
#[must_use = "a component does nothing until it is run or mounted"]
pub struct TimePicker(Chooser);
impl TimePicker {
pub fn new(prompt: impl Into<String>) -> TimePicker {
TimePicker(Chooser::new(prompt.into(), Mode::Time))
}
shared_builders!();
time_builders!();
pub fn value(mut self, time: Time) -> Self {
self.0.value.time = time;
self.0.value_given = true;
self
}
pub fn default(mut self, time: Time) -> Self {
if !self.0.value_given {
self.0.value.time = time;
}
self.0.default = Some(self.0.value.date.at(time));
self
}
pub fn min(mut self, time: Time) -> Self {
self.0.min = Some(self.0.value.date.at(time));
self
}
pub fn max(mut self, time: Time) -> Self {
self.0.max = Some(self.0.value.date.at(time));
self
}
pub fn validate(
mut self,
check: impl Fn(Time) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
self.0.check = Some(Arc::new(move |at: DateTime| check(at.time)));
self
}
pub fn selected(&self) -> Time {
self.0.shown(self.0.value).time
}
}
#[must_use = "a component does nothing until it is run or mounted"]
pub struct DateTimePicker(Chooser);
impl DateTimePicker {
pub fn new(prompt: impl Into<String>) -> DateTimePicker {
DateTimePicker(Chooser::new(prompt.into(), Mode::DateTime))
}
shared_builders!();
date_builders!();
time_builders!();
pub fn value(mut self, at: DateTime) -> Self {
self.0.value = at;
self.0.value_given = true;
self
}
pub fn default(mut self, at: DateTime) -> Self {
if !self.0.value_given {
self.0.value = at;
}
self.0.default = Some(at);
self
}
pub fn min(mut self, at: DateTime) -> Self {
self.0.min = Some(at);
self
}
pub fn max(mut self, at: DateTime) -> Self {
self.0.max = Some(at);
self
}
pub fn validate(
mut self,
check: impl Fn(DateTime) -> Result<(), String> + Send + Sync + 'static,
) -> Self {
self.0.check = Some(Arc::new(check));
self
}
pub fn selected(&self) -> DateTime {
self.0.shown(self.0.value)
}
}
macro_rules! picker_component {
($picker:ty, $output:ty, $from:expr) => {
impl Component for $picker {
type Output = $output;
fn start(&mut self, _context: &Context<'_>) -> Flow<$output> {
let mut value = self.0.shown(self.0.value);
value.date = self.0.clamp_day(value.date);
self.0.value = value;
Flow::Continue
}
fn handle(&mut self, event: &Event, _context: &Context<'_>) -> Flow<$output> {
match self.0.event(event) {
Flow::Done(at) => Flow::Done($from(at)),
Flow::Cancel => Flow::Cancel,
Flow::Ignored => Flow::Ignored,
_ => Flow::Continue,
}
}
fn render(&self, context: &Context<'_>) -> View {
self.0.render(context)
}
fn mouse(&self) -> bool {
self.0.mouse
}
fn keymap(&self) -> Keymap {
self.0.keymap.clone()
}
fn focus_step(&mut self, forward: bool) -> bool {
self.0.step_part(forward)
}
fn focus_enter(&mut self, forward: bool) -> bool {
self.0.enter(forward);
true
}
fn default_value(&self) -> Option<$output> {
self.0
.default
.and_then(|default| self.0.checked(default).ok())
.map($from)
}
fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<$output>, NotInteractive> {
Ok(self.0.ask(io)?.map($from))
}
}
};
}
picker_component!(DatePicker, Date, |at: DateTime| at.date);
picker_component!(TimePicker, Time, |at: DateTime| at.time);
picker_component!(DateTimePicker, DateTime, |at: DateTime| at);