M6: Date picker: date fields show a month calendar while focused; the picked day is written in the field's own format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0d954f10a
commit
cce71c7607
|
|
@ -1373,7 +1373,7 @@ pub fn document_area(app: &mut PrintCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
if let Some(e) = comments::composer(ui.ctx(), view, info, prefs) {
|
||||
view.pending_edit = Some(e);
|
||||
}
|
||||
if let Some(e) = crate::forms_ui::overlay(ui.ctx(), view, info, &form) {
|
||||
if let Some(e) = crate::forms_ui::overlay(ui.ctx(), view, info, &form, today) {
|
||||
view.pending_edit = Some(e);
|
||||
}
|
||||
if let Some(e) = crate::fill_sign::type_box(ui.ctx(), view, info, &author) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ pub struct Focus {
|
|||
pub request_focus: bool,
|
||||
/// Select the whole value when the editor opens (entered with Tab), so typing replaces it.
|
||||
pub select_all: bool,
|
||||
/// Date fields: the month the calendar shows (year, month 1–12), and where it was drawn.
|
||||
pub calendar: Option<(i32, u32)>,
|
||||
pub calendar_rect: Option<egui::Rect>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
|
@ -53,6 +56,8 @@ fn open_focus(f: &FormField, widget: usize) -> Focus {
|
|||
picked: f.value.clone(),
|
||||
request_focus: true,
|
||||
select_all: false,
|
||||
calendar: None,
|
||||
calendar_rect: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +135,77 @@ fn commit(view: &mut DocView, form: &[FormField]) {
|
|||
view.pending_edit = Some(Edit::SetFieldValue { name: f.name.clone(), value });
|
||||
}
|
||||
|
||||
const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
|
||||
/// The date picker under a focused date field. Returns the picked date, formatted with the
|
||||
/// field's own format.
|
||||
fn calendar(ctx: &egui::Context, view: &mut DocView, field: egui::Rect, fmt: &str, today: (i64, u32, u32)) -> Option<String> {
|
||||
use printcraft_engine::form_scripts::{DateTime, format_date, parse_date};
|
||||
let fx = view.forms.focus.as_mut()?;
|
||||
let (mut y, mut m) = fx.calendar.unwrap_or_else(|| match parse_date(&fx.text, fmt) {
|
||||
Some(d) => (d.y, d.m),
|
||||
None => (today.0 as i32, today.1),
|
||||
});
|
||||
let current = parse_date(&fx.text, fmt);
|
||||
let mut picked = None;
|
||||
let area = egui::Area::new(egui::Id::new(("date-picker", view.id.0)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(field.left_bottom() + vec2(0.0, 2.0))
|
||||
.show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("‹").on_hover_text("Previous month").clicked() {
|
||||
(y, m) = if m == 1 { (y - 1, 12) } else { (y, m - 1) };
|
||||
}
|
||||
ui.label(egui::RichText::new(format!("{} {y}", MONTHS[(m.clamp(1, 12) - 1) as usize])).strong());
|
||||
if ui.small_button("›").on_hover_text("Next month").clicked() {
|
||||
(y, m) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
|
||||
}
|
||||
});
|
||||
// Weekday of the 1st (0 = Sunday), Zeller-style via days since 1970-01-01 (a Thursday).
|
||||
let days_from_civil = |y: i32, m: u32, d: u32| -> i64 {
|
||||
let (y, m) = if m <= 2 { (y as i64 - 1, m as i64 + 9) } else { (y as i64, m as i64 - 3) };
|
||||
let era = y.div_euclid(400);
|
||||
let yoe = y - era * 400;
|
||||
let doy = (153 * m + 2) / 5 + d as i64 - 1;
|
||||
era * 146_097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719_468
|
||||
};
|
||||
let first = (days_from_civil(y, m, 1) + 4).rem_euclid(7) as usize;
|
||||
let len = (days_from_civil(if m == 12 { y + 1 } else { y }, if m == 12 { 1 } else { m + 1 }, 1) - days_from_civil(y, m, 1)) as usize;
|
||||
egui::Grid::new(("date-grid", view.id.0)).spacing([4.0, 2.0]).show(ui, |ui| {
|
||||
for d in ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] {
|
||||
ui.label(egui::RichText::new(d).small());
|
||||
}
|
||||
ui.end_row();
|
||||
for cell in 0..(first + len).div_ceil(7) * 7 {
|
||||
if cell >= first && cell < first + len {
|
||||
let day = (cell - first + 1) as u32;
|
||||
let selected = current.is_some_and(|c| (c.y, c.m, c.d) == (y, m, day));
|
||||
let is_today = (today.0 as i32, today.1, today.2) == (y, m, day);
|
||||
let mut text = egui::RichText::new(day.to_string());
|
||||
if is_today {
|
||||
text = text.strong();
|
||||
}
|
||||
if ui.selectable_label(selected, text).on_hover_text(format!("{} {day}, {y}", MONTHS[(m - 1) as usize])).clicked() {
|
||||
picked = Some(format_date(DateTime { y, m, d: day, hh: 0, mm: 0, ss: 0 }, fmt));
|
||||
}
|
||||
} else {
|
||||
ui.label("");
|
||||
}
|
||||
if cell % 7 == 6 {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if let Some(fx) = view.forms.focus.as_mut() {
|
||||
fx.calendar = Some((y, m));
|
||||
fx.calendar_rect = Some(area.response.rect);
|
||||
}
|
||||
picked
|
||||
}
|
||||
|
||||
/// Paint focus and hover frames on a page.
|
||||
pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, xf: &PageXform, page: usize, info: &DocInfo, form: &[FormField], view: &DocView) {
|
||||
let pointer = ui.input(|i| i.pointer.hover_pos());
|
||||
|
|
@ -147,7 +223,7 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, xf: &PageXform,
|
|||
}
|
||||
|
||||
/// The in-place editor or option list for the focused field. Returns an edit to apply.
|
||||
pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, form: &[FormField]) -> Option<Edit> {
|
||||
pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, form: &[FormField], today: (i64, u32, u32)) -> Option<Edit> {
|
||||
let focus = view.forms.focus.clone()?;
|
||||
let Some(f) = form.iter().find(|f| f.name == focus.name) else {
|
||||
view.forms.focus = None;
|
||||
|
|
@ -205,11 +281,24 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
|
|||
close = true;
|
||||
} else if tab {
|
||||
next = Some(!shift);
|
||||
} else if r.lost_focus() && (!multiline || !enter) {
|
||||
} else if r.lost_focus()
|
||||
&& (!multiline || !enter)
|
||||
&& !fx.calendar_rect.is_some_and(|c| ui.input(|i| i.pointer.interact_pos().is_some_and(|p| c.contains(p))))
|
||||
{
|
||||
// Enter in a single-line field, or clicking elsewhere, commits.
|
||||
commit(view, form);
|
||||
}
|
||||
});
|
||||
// Date fields: a calendar under the field (Acrobat's date picker).
|
||||
if let printcraft_engine::form_scripts::Format::Date(fmt) = &f.actions.format
|
||||
&& let Some(picked) = calendar(ctx, view, rect, fmt, today)
|
||||
{
|
||||
if let Some(fx) = view.forms.focus.as_mut() {
|
||||
fx.text = picked;
|
||||
}
|
||||
commit(view, form);
|
||||
return view.pending_edit.take();
|
||||
}
|
||||
}
|
||||
FormFieldKind::Combo | FormFieldKind::List => {
|
||||
let multi = f.kind == FormFieldKind::List && f.has(field_flags::MULTI_SELECT);
|
||||
|
|
|
|||
|
|
@ -120,3 +120,27 @@ fn tabbing_into_a_filled_field_selects_it_so_typing_replaces() {
|
|||
h.run_steps(3);
|
||||
assert_eq!(value(&h, "city"), ["Lyon"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_fields_offer_a_calendar() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut h = harness();
|
||||
h.state_mut().apply_edit(printcraft_engine::Edit::AddField {
|
||||
page: 0,
|
||||
rect: [50.0, 40.0, 200.0, 60.0],
|
||||
kind: printcraft_engine::NewField::Date,
|
||||
name: Some("due".into()),
|
||||
});
|
||||
h.run_steps(4);
|
||||
click_field(&mut h, "due", 0);
|
||||
let (y, m, _) = h.state().session.today();
|
||||
let month =
|
||||
["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][(m - 1) as usize];
|
||||
h.get_by_label(&format!("{month} {y}"));
|
||||
h.get_by_label("›").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("15").click();
|
||||
h.run_steps(4);
|
||||
let (ny, nm) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
|
||||
assert_eq!(value(&h, "due"), vec![format!("{nm:02}/15/{ny}")], "picked in the field's format");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4503,11 +4503,11 @@ title = "Date picker for date fields"
|
|||
area = "F"
|
||||
tier = "P1"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
tools = ["form_fill"]
|
||||
evidence = ["crates/ui-egui/tests/forms.rs::date_fields_offer_a_calendar"]
|
||||
notes = "a month calendar under date fields; the pick is written in the field's own date format"
|
||||
|
||||
[[feature]]
|
||||
id = "form.auto-complete"
|
||||
|
|
|
|||
Loading…
Reference in New Issue