Files
wordcraft/crates/ui-egui/src/window_ui.rs
T
batistalucasvandfelipegavilanes-source bf426c1804 Insert > Date & Time: Word-style dialog with regional formats and field support (#5093) (#6022)
* feat(engine): format Brazilian date fields and insert localized dates

* feat(ui): persist preferred date and time format

* feat(ui): implement Word-style Insert > Date & Time modal

* feat(ribbon): open Date & Time dialog from Insert tab

* test: Brazilian Word TIME fields respect document defaults after DOCX round-trip

* fix(docx): refresh Word date/time fields using their run language

* ci: add temporary date/time regression validation workflow

* style: apply rustfmt output to insert.rs

* style: apply rustfmt output to date_fields.rs

* style: apply rustfmt output to dialogs_insert.rs

* i18n: reuse translated date-format and language labels

* i18n(pt-BR): translate Update automatically in date/time dialog

* fix(i18n): translate date dialog options and audit generated date previews

* chore: remove temporary date/time regression workflow after focused validation

* fix(date): resolve inherited locale and preserve it in fixed text

* fix(ui): display date insertion errors without closing the dialog and document state

* test: cover inherited language, static date text, and escaped date pictures

* test(ui): exercise date language, picture, checkbox, OK, and invalid-value feedback via egui

* style: apply rustfmt to date insertion and regression tests

* fix(engine): borrow caret position while resolving inherited date language

* fix(date): honor selected locale when the date picture is omitted

* test(ui): target date dialog language combo instead of ribbon menu

* test(date): use raw field instruction literal in locale regression

* test(date): match actual single-backslash DATE field instruction

* test(ui): select actual date dialog language option by accessible label

* fix(i18n): add missing automatic date update translation (Romanian)

* fix(i18n): add missing automatic date update translation (Slovenian)

* fix(ui): open date_time dialog from keytip, normalize pt tags, and document items

* fix(ui): default date_time_update preference to false

---------

Co-authored-by: felipegavilanes-source <felipegavilanes@gmail.com>
2026-11-11 14:21:31 +01:00

421 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! What one window shows and is doing, apart from the rest of the app. [`WordApp`] holds the
//! state of the window being drawn in its own fields, so drawing code reads `app.canvas`,
//! `app.dialog`, `app.ui.tab` whatever window it is; every other window keeps its state in a
//! [`WindowUi`]. [`WordApp::swap_window`] exchanges the two by moving owned fields, so it is
//! cheap enough to run for every window on every frame.
//!
//! Every field of [`WordApp`] and [`UiState`] is one of:
//! - per window: listed in [`WindowUi`] and swapped;
//! - per document: kept for the documents not shown in [`WordApp`]'s `parked` map
//! ([`WordApp::sync_document`]) or in the engine's session;
//! - app-wide: the session, preferences, fonts, theme, the control channel, timers and workers.
//!
//! `swap_window` names every field, so a new field doesn't compile until it is classified.
use crate::{AwaitingSave, UiState, WordApp, canvas, colors, content_controls, dialogs, dialogs_review, file_dialogs, keytips, previews, print_page};
/// A window's state while another window is drawn. Fields mirror their namesakes in [`WordApp`]
/// and, for the ribbon tab, Backstage, printing and keytips, in [`UiState`].
pub(crate) struct WindowUi {
pub(crate) canvas: canvas::CanvasState,
pub(crate) controls: content_controls::ControlUi,
pub(crate) dialog: Option<dialogs::Dialog>,
pub(crate) colors: Option<colors::ColorsDialog>,
pub(crate) restrict: dialogs_review::RestrictPane,
pub(crate) spelling_review: Option<crate::proofing_pane::SpellingForm>,
pub(crate) status_msg: Option<(String, f64)>,
pub(crate) previews: previews::Previews,
pub(crate) change_picture_target: Option<String>,
pub(crate) recipient_list_pending: bool,
pub(crate) dialog_pick: Option<String>,
pub(crate) focus_ribbon: bool,
pub(crate) control_click: bool,
pub(crate) keytip_rects: Vec<(egui::Rect, String)>,
pub(crate) nav_search_focus: bool,
pub(crate) comment_fonts_for: Option<(u64, u64)>,
pub(crate) find_fonts_for: Option<(String, usize, u64)>,
pub(crate) sent_title: String,
pub(crate) sent_full_screen: bool,
pub(crate) seen_full_screen: Option<bool>,
pub(crate) escape_free: bool,
pub(crate) status_counts: (u64, crate::StatusCounts),
/// Per window, as each window shows its own layout: one shared list would be read again
/// on every frame of every window.
pub(crate) missing_fonts: crate::chrome::MissingFontsMemo,
pub(crate) last_autosave: f64,
pub(crate) equation_prev_tab: Option<String>,
pub(crate) smart_art_item: Option<usize>,
pub(crate) smart_art_row_focused: bool,
pub(crate) smart_art_typing: Option<usize>,
pub(crate) outline_prev_tab: Option<String>,
pub(crate) read_aloud_at: Option<wordcraft_engine::doc::Pos>,
pub(crate) awaiting_save: Option<AwaitingSave>,
pub(crate) file_dialog: Option<file_dialogs::PendingDialog>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) file_browser: crate::file_browser::State,
pub(crate) info_editing: Option<(&'static str, u64)>,
pub(crate) shown_document: u64,
// From UiState.
pub(crate) tab: String,
pub(crate) backstage: bool,
pub(crate) backstage_page: String,
pub(crate) print: print_page::PrintSettings,
pub(crate) nav_collapsed: std::collections::HashSet<wordcraft_doc::Path>,
pub(crate) keytip_prefix: String,
pub(crate) keytips: keytips::Phase,
pub(crate) alt_pressed: bool,
pub(crate) alt_chord_used: bool,
pub(crate) alt_key_down: bool,
pub(crate) alt_modifier_seen: bool,
}
/// Status-bar counts that match no document revision, so the next frame counts.
pub(crate) const NOT_COUNTED: (u64, crate::StatusCounts) = (u64::MAX, crate::StatusCounts { words: 0, chars: 0, chars_no_spaces: 0 });
impl WindowUi {
/// A new window showing open document `document`: nothing open over the page, Home tab.
pub(crate) fn new(document: u64) -> WindowUi {
let UiState {
tab,
backstage,
backstage_page,
print,
nav_collapsed,
keytip_prefix,
keytips,
alt_pressed,
alt_chord_used,
alt_key_down,
alt_modifier_seen,
..
} = UiState::default();
WindowUi {
canvas: Default::default(),
controls: Default::default(),
dialog: None,
colors: None,
restrict: Default::default(),
spelling_review: None,
status_msg: None,
previews: Default::default(),
change_picture_target: None,
recipient_list_pending: false,
dialog_pick: None,
focus_ribbon: false,
control_click: false,
keytip_rects: Vec::new(),
nav_search_focus: false,
comment_fonts_for: None,
find_fonts_for: None,
sent_title: String::new(),
sent_full_screen: false,
seen_full_screen: None,
escape_free: false,
status_counts: NOT_COUNTED,
missing_fonts: Default::default(),
last_autosave: 0.0,
equation_prev_tab: None,
smart_art_item: None,
smart_art_row_focused: false,
smart_art_typing: None,
outline_prev_tab: None,
read_aloud_at: None,
awaiting_save: None,
file_dialog: None,
#[cfg(not(target_arch = "wasm32"))]
file_browser: Default::default(),
info_editing: None,
shown_document: document,
tab,
backstage,
backstage_page,
print,
nav_collapsed,
keytip_prefix,
keytips,
alt_pressed,
alt_chord_used,
alt_key_down,
alt_modifier_seen,
}
}
}
/// Swap each named field of the drawn window (bound by the caller's destructuring) with its
/// namesake in the [`WindowUi`]. The `WindowUi` pattern is exhaustive, so a field added there
/// and left out here doesn't compile.
macro_rules! swap_fields {
($w:ident: $($(#[$attr:meta])* $field:ident),* $(,)?) => {
let WindowUi { $($(#[$attr])* $field: _),* } = &*$w;
$($(#[$attr])* std::mem::swap($field, &mut $w.$field);)*
};
}
impl WordApp {
/// Exchange the drawn window's state with `w`'s: afterwards the app draws the window `w`
/// held, and `w` holds the one that was drawn. App-wide and per-document state stays. The
/// session's active document is not changed: the caller activates `w`'s document
/// (`shown_document`) with it, or [`WordApp::sync_document`] treats the change as a switch
/// within the window.
pub(crate) fn swap_window(&mut self, w: &mut WindowUi) {
let WordApp {
// App-wide.
session: _,
services: _,
printing: _,
print_feedback_seen: _,
font_generation: _,
integrated_titlebar: _,
mac_titlebar: _,
control_rx: _,
updates_rt: _,
packs_rt: _,
// The control channel's screenshots and input go to the active window, whichever it is.
shots: _,
capture_epoch: _,
surface_geometry: _,
capture_geometry: _,
synthetic: _,
synthetic_pending: _,
synthetic_injected: _,
// The current command's origin, set and cleared around it.
ime_origin: _,
styled: _,
fonts_hans: _,
fonts_korean: _,
fonts_hant: _,
fonts_system_cjk: _,
fonts_system_arabic: _,
want_system_cjk: _,
want_system_arabic: _,
recent_fonts_for: _,
// File › Recover lists the copies of every window's documents.
recover_fonts_for: _,
fonts_frames: _,
applied_theme: _,
native_scale: _,
legacy_interface_size: _,
desktop_theme: _,
desktop_dark: _,
applied_editing: _,
system_animations: _,
applied_animations: _,
frame_ms: _,
quit_requested: _,
autosave: _,
zotero: _,
ctx: _,
read_aloud_error: _,
saves_in_flight: _,
recovery_seen: _,
no_document: _,
windows: _,
print_document: _,
// Per document: the shown document's, swapped with `parked` on a document switch.
autosave_path: _,
comment_drafts: _,
parked: _,
// Per window.
ui,
canvas,
controls,
dialog,
colors,
restrict,
spelling_review,
status_msg,
previews,
change_picture_target,
recipient_list_pending,
dialog_pick,
focus_ribbon,
control_click,
keytip_rects,
nav_search_focus,
comment_fonts_for,
find_fonts_for,
sent_title,
sent_full_screen,
seen_full_screen,
escape_free,
status_counts,
missing_fonts,
last_autosave,
equation_prev_tab,
smart_art_item,
smart_art_row_focused,
smart_art_typing,
outline_prev_tab,
read_aloud_at,
awaiting_save,
file_dialog,
#[cfg(not(target_arch = "wasm32"))]
file_browser,
info_editing,
shown_document,
} = self;
let UiState {
// App-wide: preferences, recent lists, and the settings the session owns while
// running.
ribbon_collapsed: _,
quick_access_toolbar: _,
recent: _,
recent_symbols: _,
date_time_format: _,
date_time_language: _,
date_time_update: _,
recent_colors: _,
recent_fonts: _,
theme: _,
interface_size: _,
legacy_interface_size: _,
animations: _,
legacy_dark: _,
nav_tab: _,
display_server: _,
status_count: _,
nav_pane: _,
show_discord: _,
author: _,
window: _,
language: _,
editing: _,
read_aloud_rate: _,
read_aloud_skip_citations: _,
keytips_enabled: _,
dark_page: _,
check_spelling: _,
caret_animation: _,
keyboard: _,
ribbon: _,
caption_labels: _,
last_caption_label: _,
ink_pens: _,
initials: _,
options_pane: _,
recent_count: _,
mini_toolbar: _,
alignment_guides: _,
start_screen: _,
print_backgrounds: _,
update_fields_before_print: _,
dictionary: _,
// Where each task pane sits, and their order.
styles_pane_dock: _,
styles_pane_float: _,
styles_pane_width: _,
pane_places: _,
pane_order: _,
// Per window.
tab,
backstage,
backstage_page,
print,
nav_collapsed,
keytip_prefix,
keytips,
alt_pressed,
alt_chord_used,
alt_key_down,
alt_modifier_seen,
} = ui;
swap_fields!(w:
canvas,
controls,
dialog,
colors,
restrict,
spelling_review,
status_msg,
previews,
change_picture_target,
recipient_list_pending,
dialog_pick,
focus_ribbon,
control_click,
keytip_rects,
nav_search_focus,
comment_fonts_for,
find_fonts_for,
sent_title,
sent_full_screen,
seen_full_screen,
escape_free,
status_counts,
missing_fonts,
last_autosave,
equation_prev_tab,
smart_art_item,
smart_art_row_focused,
smart_art_typing,
outline_prev_tab,
read_aloud_at,
awaiting_save,
file_dialog,
#[cfg(not(target_arch = "wasm32"))]
file_browser,
info_editing,
shown_document,
tab,
backstage,
backstage_page,
print,
nav_collapsed,
keytip_prefix,
keytips,
alt_pressed,
alt_chord_used,
alt_key_down,
alt_modifier_seen,
);
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use wordcraft_engine::Session;
use super::WindowUi;
use crate::{Services, WordApp, dialogs::Dialog};
/// Swapping in a new window shows its fresh state; swapping back restores the first window's
/// dialog, status message, composition, scroll request and tab. The session and the
/// preferences stay put both ways.
#[test]
fn swap_window_round_trip() {
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default());
a.run("text.insert", json!({"text": "Kept"})).unwrap();
let doc = a.session.document_id();
let rev = a.session.rev();
a.ui.recent_count = 7;
a.ui.tab = "Insert".into();
a.dialog =
Some(Dialog::SaveChanges { name: "Report".into(), then: "file.close".into(), params: json!({}), document: doc, message: String::new() });
a.status_msg = Some(("Saved".into(), 1.0));
a.canvas.ime_preedit = "ni".into();
a.canvas.ime_preedit_range = Some(0..2);
a.canvas.scroll_to_caret = false;
a.canvas.nav_to_caret = true;
let mut w = WindowUi::new(doc);
a.swap_window(&mut w);
assert!(a.dialog.is_none() && a.status_msg.is_none());
assert!(a.canvas.ime_preedit.is_empty() && a.canvas.ime_preedit_range.is_none());
assert!(a.canvas.scroll_to_caret && !a.canvas.nav_to_caret, "a new window goes to the caret");
assert_eq!(a.ui.tab, crate::DEFAULT_TAB);
assert_eq!(w.tab, "Insert");
assert_eq!((a.session.document_id(), a.session.rev(), a.ui.recent_count), (doc, rev, 7), "app-wide state stays");
a.swap_window(&mut w);
assert_eq!(a.dialog.as_ref().map(Dialog::name), Some("saveChanges"));
assert_eq!(a.status_msg.as_ref().map(|(m, _)| m.as_str()), Some("Saved"));
assert_eq!((a.canvas.ime_preedit.as_str(), a.canvas.ime_preedit_range.clone()), ("ni", Some(0..2)));
assert!(!a.canvas.scroll_to_caret && a.canvas.nav_to_caret);
assert_eq!(a.ui.tab, "Insert");
assert!(w.dialog.is_none() && w.tab == crate::DEFAULT_TAB);
assert_eq!((a.session.document_id(), a.session.rev(), a.ui.recent_count), (doc, rev, 7));
assert_eq!(a.session.doc.plain_text(wordcraft_doc::StoryRef::Body).trim(), "Kept");
}
}