From 297faa83189fc90f3b78ae95772a374597b67e05 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 25 Oct 2026 15:14:28 +0100 Subject: [PATCH] egui037: document migrated UI and regression contracts --- apps/gridcraft-web/src/web.rs | 2 + apps/gridcraft/src/main.rs | 1 + apps/gridcraft/src/print.rs | 1 + crates/ui-egui/build.rs | 1 + crates/ui-egui/src/backstage.rs | 9 ++++ crates/ui-egui/src/cell_pictures.rs | 1 + crates/ui-egui/src/code_editor.rs | 5 ++ crates/ui-egui/src/control.rs | 51 +++++++++++++++++++ crates/ui-egui/src/craft_fonts_policy.rs | 8 +++ crates/ui-egui/src/dialogs.rs | 6 +++ crates/ui-egui/src/floating_pictures.rs | 1 + crates/ui-egui/src/formula_bar.rs | 4 ++ crates/ui-egui/src/grid.rs | 6 +++ crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/de.rs | 1 + crates/ui-egui/src/i18n/el.rs | 1 + crates/ui-egui/src/i18n/es.rs | 1 + crates/ui-egui/src/i18n/fr.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ko.rs | 1 + crates/ui-egui/src/i18n/nl.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt.rs | 1 + crates/ui-egui/src/i18n/ro.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/sr.rs | 1 + crates/ui-egui/src/i18n/tr.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + crates/ui-egui/src/i18n/vi.rs | 1 + crates/ui-egui/src/i18n/zh.rs | 1 + crates/ui-egui/src/lib.rs | 18 +++++++ crates/ui-egui/src/panes.rs | 4 ++ crates/ui-egui/src/paper_view.rs | 1 + crates/ui-egui/src/pivot_pane.rs | 1 + crates/ui-egui/src/ribbon.rs | 7 +++ crates/ui-egui/src/tabs.rs | 2 + crates/ui-egui/src/text_box.rs | 2 + crates/ui-egui/src/theme.rs | 13 +++++ crates/ui-egui/src/widgets.rs | 14 +++++ crates/ui-egui/tests/it/advanced_filter.rs | 1 + crates/ui-egui/tests/it/autosave_persist.rs | 1 + crates/ui-egui/tests/it/chart_resize.rs | 1 + .../ui-egui/tests/it/committed_text_fields.rs | 1 + .../ui-egui/tests/it/contextual_ribbon_tab.rs | 1 + crates/ui-egui/tests/it/dialog_button_row.rs | 1 + crates/ui-egui/tests/it/dialog_move.rs | 1 + .../ui-egui/tests/it/error_checking_menu.rs | 2 + crates/ui-egui/tests/it/file_menu.rs | 1 + crates/ui-egui/tests/it/filter_list_scroll.rs | 1 + crates/ui-egui/tests/it/font_size_box.rs | 1 + crates/ui-egui/tests/it/formula_bar.rs | 3 ++ crates/ui-egui/tests/it/formula_locale.rs | 2 + crates/ui-egui/tests/it/formula_popup.rs | 7 +++ crates/ui-egui/tests/it/interaction.rs | 8 +++ crates/ui-egui/tests/it/interface_scale.rs | 1 + crates/ui-egui/tests/it/keyboard_shortcuts.rs | 1 + crates/ui-egui/tests/it/more_colors.rs | 1 + crates/ui-egui/tests/it/native_print.rs | 4 ++ crates/ui-egui/tests/it/page_setup_margins.rs | 1 + crates/ui-egui/tests/it/pivot_pane.rs | 1 + crates/ui-egui/tests/it/ribbon_layout.rs | 1 + .../ui-egui/tests/it/scrollbar_column_fill.rs | 1 + crates/ui-egui/tests/it/sheet_tabs.rs | 2 + crates/ui-egui/tests/it/symbol_glyphs.rs | 9 ++++ crates/ui-egui/tests/it/table_headings.rs | 1 + crates/ui-egui/tests/it/text_box_editing.rs | 1 + crates/ui-egui/tests/it/wheel_zoom.rs | 6 +++ 67 files changed, 237 insertions(+) diff --git a/apps/gridcraft-web/src/web.rs b/apps/gridcraft-web/src/web.rs index 7a953e580..e8d9ab181 100644 --- a/apps/gridcraft-web/src/web.rs +++ b/apps/gridcraft-web/src/web.rs @@ -8,6 +8,7 @@ use wasm_bindgen::JsValue; const CANVAS_ID: &str = "gridcraft_canvas"; const LOADING_ID: &str = "gridcraft_loading"; +/// Starts the eframe web runner for the GridCraft canvas. It adjusts the WebGPU device limits to the adapter's, optionally selects the WebGL backend from the query string, and builds the shell with the requested sample or host bridge, browser language rules, stored preferences and the loading/error page state. pub fn start() { eframe::WebLogger::init(log::LevelFilter::Info).ok(); wasm_bindgen_futures::spawn_local(async { @@ -158,6 +159,7 @@ impl eframe::App for WebShell { } } + /// Forwards a browser frame's raw input to the shared app before eframe handles it. fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { self.app.raw_input_hook(ctx, raw); } diff --git a/apps/gridcraft/src/main.rs b/apps/gridcraft/src/main.rs index b1c5ddd63..ad39edb90 100644 --- a/apps/gridcraft/src/main.rs +++ b/apps/gridcraft/src/main.rs @@ -44,6 +44,7 @@ impl eframe::App for App { ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose); } } + /// Forwards raw input to the shared app, then opens every non-empty dropped file path as a workbook. fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { self.0.raw_input_hook(ctx, raw); // Files dropped on the window open as workbooks. diff --git a/apps/gridcraft/src/print.rs b/apps/gridcraft/src/print.rs index dea386cf2..ec05ed08a 100644 --- a/apps/gridcraft/src/print.rs +++ b/apps/gridcraft/src/print.rs @@ -1,6 +1,7 @@ //! Desktop spooler adapter. Discovery and submission never run on the UI thread. use gridcraft_ui_egui::printing::{Discover, Duplex, Printer, Settings, Submit}; +/// Returns a printer-discovery closure that enumerates `craft_print::printers()` on a named background thread and sends the mapped list through a channel. pub fn discover() -> Discover { Box::new(|| { let (sender, receiver) = std::sync::mpsc::channel(); diff --git a/crates/ui-egui/build.rs b/crates/ui-egui/build.rs index d2711ae55..07ab18a23 100644 --- a/crates/ui-egui/build.rs +++ b/crates/ui-egui/build.rs @@ -15,6 +15,7 @@ use serde_json::Value; #[path = "src/craft_fonts_policy.rs"] mod craft_fonts_policy; +/// Build script: reads the contributor JSON/TOML and writes generated `credits.rs`, then emits `craft_fonts.rs`; missing or malformed optional inputs warn, while required mode reports build errors. fn main() { // Read when the script runs: `env!` would keep the path from when the script was compiled, // which goes stale if the checkout moves (and a missing file reruns the script every build). diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index 5dfc2b694..2a20726c1 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -258,6 +258,7 @@ fn save(app: &mut SheetApp, out: &mut Outcome) { // ---------------------------------------------------------------- navigation +/// Allocates and paints one Backstage navigation row: selected or hover fill, icon, label and button semantics. Returns the row's click response. fn nav_item(ui: &mut Ui, icon: Icon, label: &str, selected: bool, text: Color32, sel: Color32) -> egui::Response { let (rect, resp) = ui.allocate_exact_size(vec2(NAV_W, 38.0), Sense::click()); resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, selected, label)); @@ -589,6 +590,7 @@ pub fn protect_workbook(app: &mut SheetApp) { } } +/// Shows the workbook title, path, sheet count and structure protection, plus editable document properties. Committing a changed field after it loses focus runs `file.properties`. fn info(app: &mut SheetApp, ui: &mut Ui, bs: &mut Backstage) { let t = Tokens::get(ui.ctx()); let lang = app.ui.language; @@ -660,6 +662,7 @@ fn info(app: &mut SheetApp, ui: &mut Ui, bs: &mut Backstage) { }); } +/// Draws the Save As type picker and action: xlsx opens the `saveAs` dialog, other types export, and the page closes when the save/export succeeds (or a web download handles it). fn save_as(app: &mut SheetApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome) { let lang = app.ui.language; dim(ui, lang.tr("Save the workbook under another name or in another folder.")); @@ -701,6 +704,7 @@ fn save_as(app: &mut SheetApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcom dim(ui, lang.tr("A CSV file, web page or PDF is a copy: the workbook keeps its own file.")); } +/// Builds the print scope and fit-to-page parameters, caches `file.printPreview` per document revision/options, and draws page thumbnails with navigation plus the print settings and Print action. fn print(app: &mut SheetApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome) { let t = Tokens::get(ui.ctx()); let lang = app.ui.language; @@ -945,6 +949,11 @@ pub fn export_as(app: &mut SheetApp, fmt: &str) -> bool { } } +/// Excel's File › Options page: General (display theme, interface language and Show formula bar), +/// Editing (AutoComplete) and Save (the autosave checkbox toggling `app.ui.autosave`), plus Formulas +/// (calculation mode and iterative calculation) when a document is active. Theme, language, formula +/// bar, AutoComplete and calculation changes run their commands through `run_or_alert`; calculation +/// settings are saved with the workbook. fn options(app: &mut SheetApp, ui: &mut Ui) { let lang = app.ui.language; section(ui, lang, "Options|General"); diff --git a/crates/ui-egui/src/cell_pictures.rs b/crates/ui-egui/src/cell_pictures.rs index 2a8d4ff22..524faf87d 100644 --- a/crates/ui-egui/src/cell_pictures.rs +++ b/crates/ui-egui/src/cell_pictures.rs @@ -116,6 +116,7 @@ fn fitted_rect(cell: Rect, size: [usize; 2]) -> Option { Some(Rect::from_center_size(cell.center(), source * scale)) } +/// Draws a cached cell picture fitted inside the cell and clipped to it, or paints the picture fallback icon when no texture is available. pub(crate) fn paint(painter: &Painter, cell: Rect, picture: &Arc) { if cell.width() <= 0.0 || cell.height() <= 0.0 || !cell.intersects(painter.clip_rect()) { return; diff --git a/crates/ui-egui/src/code_editor.rs b/crates/ui-egui/src/code_editor.rs index 17c24c628..6f02b4044 100644 --- a/crates/ui-egui/src/code_editor.rs +++ b/crates/ui-egui/src/code_editor.rs @@ -670,6 +670,7 @@ mod tests { } #[test] + /// Shift+Tab removes four leading spaces, while a plain Tab still inserts four spaces at the caret. fn shift_tab_in_the_editor_dedents_rather_than_indents() { let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state( |ui, text: &mut String| { @@ -699,6 +700,7 @@ mod tests { ctx.set_fonts(theme::pinned_font_definitions()); } + /// Builds a 400x300 code-editor harness with pinned mono fonts, focus, two passes and an accessibility check. fn editor_harness(text: &str) -> egui_kittest::Harness<'static, String> { let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state( |ui, text: &mut String| { @@ -867,6 +869,7 @@ mod tests { } #[test] + /// Events held by the previous pass are applied once in the next pass; older held events are dropped and cleared. fn held_back_events_land_in_the_next_pass_only() { let mut h = editor_harness(""); let id = egui::Id::unique("code"); @@ -884,6 +887,7 @@ mod tests { } #[test] + /// Held keyboard input does not land in the next focused text field once the code editor has lost focus. fn held_back_events_go_nowhere_once_the_editor_lost_the_focus() { // Typed into the editor, they must not land in whatever has the focus next. let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(400.0, 300.0)).build_ui_state( @@ -909,6 +913,7 @@ mod tests { } #[test] + /// A discard-triggered repeat pass receives held keys with the selection made by the first pass. fn a_repeat_pass_of_the_frame_counts_as_the_next_pass() { // When a widget asks egui to discard and repeat the pass, the repeat pass gets the // held-back keys, with the selection the field made in the first pass. diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index b7fe784d4..9d093e913 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -141,6 +141,7 @@ impl CaptureGeometry { Self::build(raw.viewport_id, &info, raw.screen_rect, raw.max_texture_side, ctx.zoom_factor(), ctx.pixels_per_point()) } + /// Builds a `CaptureGeometry` from fresh viewport information. A minimized window has no size; `inner_rect` falls back to the raw screen rect; native scale is multiplied by interface zoom, and a zero surface limit means uncapped. fn build( viewport_id: egui::ViewportId, info: &egui::ViewportInfo, @@ -229,6 +230,7 @@ impl CaptureGeometry { } } +/// True only when both width and height are finite and positive. fn positive_size(size: &egui::Vec2) -> bool { size.x.is_finite() && size.y.is_finite() && size.x > 0.0 && size.y > 0.0 } @@ -477,6 +479,7 @@ pub const MAX_QUEUED_SYNTHETIC_BYTES: usize = 8 * 1024 * 1024; /// string is plenty for a spreadsheet cell and keeps a burst from exhausting memory). pub const MAX_SYNTHETIC_TEXT_BYTES: usize = 1024 * 1024; +/// Bytes charged to one synthetic event: `size_of::()` plus the payload of a `Text` event. fn event_bytes(event: &egui::Event) -> usize { std::mem::size_of::() + match event { @@ -485,6 +488,7 @@ fn event_bytes(event: &egui::Event) -> usize { } } +/// Saturating totals of queued synthetic events and their charged bytes across every pending batch. fn synthetic_usage(app: &SheetApp) -> (usize, usize) { app.synthetic .iter() @@ -505,6 +509,7 @@ fn synthetic_budget(app: &SheetApp, extra_events: usize, extra_bytes: usize) -> Ok(()) } +/// Checks the synthetic-event budget, then queues one pointer move followed by `count` press/release pairs (`count` is at least one). fn push_click(app: &mut SheetApp, pos: egui::Pos2, button: egui::PointerButton, count: u32, m: egui::Modifiers) -> Result<(), String> { let count = count.max(1) as usize; let events = count.saturating_mul(2).saturating_add(1); @@ -519,6 +524,7 @@ fn push_click(app: &mut SheetApp, pos: egui::Pos2, button: egui::PointerButton, Ok(()) } +/// Checks the synthetic-event budget, then queues a move, a press, `steps` interpolated moves and a release. fn push_drag(app: &mut SheetApp, a: egui::Pos2, b: egui::Pos2, steps: usize, m: egui::Modifiers) -> Result<(), String> { let steps = steps.max(1); let events = steps.saturating_add(3); @@ -533,6 +539,7 @@ fn push_drag(app: &mut SheetApp, a: egui::Pos2, b: egui::Pos2, steps: usize, m: Ok(()) } +/// Checks the synthetic-event budget and queues one key press/release pair. fn push_key(app: &mut SheetApp, key: egui::Key, m: egui::Modifiers) -> Result<(), String> { let events = 2; synthetic_budget(app, events, events * std::mem::size_of::())?; @@ -543,6 +550,7 @@ fn push_key(app: &mut SheetApp, key: egui::Key, m: egui::Modifiers) -> Result<() Ok(()) } +/// Rejects text longer than `MAX_SYNTHETIC_TEXT_BYTES`, then queues one `Text` event under the shared queue budget. fn push_text(app: &mut SheetApp, text: String) -> Result<(), String> { if text.len() > MAX_SYNTHETIC_TEXT_BYTES { return Err(format!("text input is {} bytes, over the {MAX_SYNTHETIC_TEXT_BYTES}-byte limit for one event", text.len())); @@ -553,6 +561,7 @@ fn push_text(app: &mut SheetApp, text: String) -> Result<(), String> { Ok(()) } +/// Queues one pointer-move event under the shared synthetic queue budget. fn push_move(app: &mut SheetApp, pos: egui::Pos2) -> Result<(), String> { let event = egui::Event::PointerMoved(pos); synthetic_budget(app, 1, event_bytes(&event))?; @@ -571,6 +580,7 @@ fn filter_commands(commands: Vec, search: Option<&str>) -> Vec { commands.into_iter().filter(|c| has(c, "id") || has(c, "label") || in_menu(c)).collect() } +/// Dispatches one control request: commands, inspection, UI state, dialogs, editing, synthetic input, open/save/quit and screenshots. Unknown methods (and unknown command ids) return an error. Immediate answers request a repaint; `ui.screenshot` is admitted asynchronously instead of being answered here. pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -> Outcome { let s = |k: &str| p.get(k).and_then(Json::as_str); let f = |k: &str| p.get(k).and_then(Json::as_f64).unwrap_or(0.0) as f32; @@ -810,6 +820,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - out } +/// Drains queued control requests, drops requests whose client stopped waiting, handles immediate outcomes and admits screenshots, then requests a repaint while synthetic input remains. pub fn poll(app: &mut SheetApp, ctx: &egui::Context) { let Some(rx) = app.control_rx.take() else { return }; while let Ok(req) = rx.try_recv() { @@ -832,6 +843,7 @@ pub fn poll(app: &mut SheetApp, ctx: &egui::Context) { } } +/// Sends one screenshot rejection carrying the request token. fn reject_shot(reply: Sender, token: u64, message: impl std::fmt::Display) { let _ = reply.send(json!({"ok": false, "error": message.to_string(), "request": token})); } @@ -868,6 +880,7 @@ pub(crate) fn note_capture_geometry(app: &mut SheetApp, geometry: CaptureGeometr app.capture_epoch } +/// Returns the next screenshot token, wrapping and skipping zero. fn next_token(app: &mut SheetApp) -> u64 { let mut token = app.shots.token.wrapping_add(1); if token == 0 { @@ -965,6 +978,7 @@ fn admit(app: &mut SheetApp, ctx: &egui::Context, path: Option, reply: S ctx.request_repaint(); } +/// Builds the standard token-carrying timeout reply for a screenshot request. fn timeout_reply(message: &str, token: u64) -> Json { json!({"ok": false, "error": message, "request": token}) } @@ -1089,6 +1103,7 @@ pub fn issue_screenshots(app: &mut SheetApp, ctx: &egui::Context) { } } +/// Checks that the image has the pixel count its dimensions imply, is within one pixel of the expected size on each axis, and keeps the expected aspect ratio within that rounding. fn validate_image(image: &egui::ColorImage, expected: [usize; 2]) -> Result<(), String> { let [w, h] = image.size; let [ew, eh] = expected; @@ -1248,6 +1263,7 @@ fn retry_allowed(r: &Shot, now: f64) -> bool { r.retries < SCREENSHOT_MAX_RETRIES && now < r.deadline } +/// Takes and cancels the request's capture slot, if it has one. fn cancel_shot(r: &mut Shot) { if let Some(slot) = r.slot.take() { slot.cancel(); @@ -1387,6 +1403,7 @@ mod tests { } #[test] + /// Checks that oversized click/drag counts are refused without changing already queued batches, that queued input counts toward the budget, and that normal gestures still queue fully. fn oversized_pointer_requests_are_refused_and_leave_the_queue_alone() { let mut app = test_app(); let p = egui::pos2(20.0, 20.0); @@ -1454,6 +1471,7 @@ mod tests { out } + /// Returns an egui context initialized by one blank pass at `size` with an 8192-pixel texture limit. fn sized_context(size: egui::Vec2) -> egui::Context { let ctx = egui::Context::default(); let raw = egui::RawInput { @@ -1465,10 +1483,12 @@ mod tests { ctx } + /// Returns blank raw input with a screen rect at `size` and an 8192-pixel texture limit. fn raw_of(size: egui::Vec2) -> egui::RawInput { egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size)), max_texture_side: Some(8192), ..Default::default() } } + /// Extracts cloned `ViewportCommand::Screenshot` callbacks from a `FullOutput` for the context's viewport. fn screenshot_callbacks(out: &egui::FullOutput, ctx: &egui::Context) -> Vec { out.viewport_output .get(&ctx.viewport_id()) @@ -1495,6 +1515,7 @@ mod tests { callbacks } + /// Returns a per-process temporary PNG path after removing any stale file at that path. fn temp_png(name: &str) -> std::path::PathBuf { let path = std::env::temp_dir().join(format!("gridcraft-shot-{name}-{}.png", std::process::id())); let _ = std::fs::remove_file(&path); @@ -1502,6 +1523,7 @@ mod tests { } #[test] + /// Checks rounded physical pixels, expected/retained byte counts, the texture-axis cap, the no-size case and overflow refusal. fn capture_geometry_reports_checked_capped_pixels() { let geometry = CaptureGeometry { viewport_id: egui::ViewportId::ROOT, @@ -1526,6 +1548,7 @@ mod tests { } #[test] + /// Checks that one request beyond `SCREENSHOT_MAX_REQUESTS` is rejected with its token while earlier requests stay queued and do not reply early. fn screenshot_admission_stops_on_the_count_budget_without_evicting() { let ctx = sized_context(egui::vec2(800.0, 600.0)); let mut app = test_app(); @@ -1547,6 +1570,7 @@ mod tests { } #[test] + /// Three roughly 100 MB path captures hit the aggregate byte limit; the third is rejected and only two requests remain. fn screenshot_admission_stops_on_aggregate_bytes_before_the_count() { let ctx = sized_context(egui::vec2(5000.0, 5000.0)); let mut app = test_app(); @@ -1566,6 +1590,7 @@ mod tests { } #[test] + /// An unknown-size request reserves the placeholder, issues nothing while settling, then replies once on timeout and releases the reservation. fn requests_without_a_size_stay_queued_and_time_out() { let ctx = sized_context(egui::Vec2::ZERO); let mut app = test_app(); @@ -1585,11 +1610,13 @@ mod tests { assert_eq!(app.shots.reserved(), 0, "the placeholder reservation is released"); } + /// Counts `RequestPaintWhileHidden` commands in a logic output. fn hidden_paints(out: &egui::LogicOutput) -> usize { out.viewport_commands.values().flatten().filter(|c| matches!(c, egui::ViewportCommand::RequestPaintWhileHidden)).count() } #[test] + /// A queued capture requests a hidden paint; once issued, the screenshot command supplies the paint itself. fn a_queued_capture_requests_hidden_paint_until_it_is_issued() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1606,6 +1633,7 @@ mod tests { } #[test] + /// A minimized request stays queued without requesting hidden paints, because it cannot issue until restored. fn a_minimized_capture_does_not_force_hidden_frames_while_it_waits() { let ctx = egui::Context::default(); let mut app = test_app(); @@ -1624,6 +1652,7 @@ mod tests { } #[test] + /// The real app harness issues exactly one callback, replies once with the token and dimensions, and releases the reservation. fn one_admitted_capture_issues_one_callback_and_replies_once() { use egui_kittest::kittest::Queryable as _; @@ -1674,6 +1703,7 @@ mod tests { } #[test] + /// Completing callbacks in reverse order replies on each matching token/path and writes each request's own pixels. fn out_of_order_completions_reply_on_their_own_request() { let ctx = sized_context(egui::vec2(8.0, 8.0)); let mut app = test_app(); @@ -1710,6 +1740,7 @@ mod tests { } #[test] + /// An expired request replies once with a timeout, writes no file and releases its reservation. fn a_timed_out_capture_replies_once_and_never_writes() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1728,6 +1759,7 @@ mod tests { } #[test] + /// A valid image that completes after its deadline gets one timeout reply and is not written. fn a_capture_completed_after_its_deadline_writes_nothing() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1752,6 +1784,7 @@ mod tests { } #[test] + /// Cancelling a request does not reply; a later callback for it is ignored and writes nothing. fn a_callback_after_the_request_is_removed_is_inert() { let ctx = sized_context(egui::vec2(32.0, 24.0)); let mut app = test_app(); @@ -1771,6 +1804,7 @@ mod tests { } #[test] + /// The first issuance adopts a new geometry, updates the epoch and reservation, spends no retry, and the capture then succeeds. fn a_resize_between_admission_and_issuance_does_not_cost_a_retry() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1795,6 +1829,7 @@ mod tests { } #[test] + /// The reservation is the largest accepted rounded image (11×9×4 bytes for a 10×8 viewport), and that image is accepted. fn the_retained_reservation_is_the_slot_bound() { let ctx = sized_context(egui::vec2(10.0, 8.0)); let mut app = test_app(); @@ -1814,6 +1849,7 @@ mod tests { } #[test] + /// Oversized images retry within the bounded budget, never write, then report the per-image limit, while the valid peer is written. fn an_image_beyond_the_reservation_is_released_without_writing() { let ctx = sized_context(egui::vec2(10.0, 8.0)); let mut app = test_app(); @@ -1856,6 +1892,7 @@ mod tests { } #[test] + /// Two `ui` calls in one pass count as one settling frame, and a pass that requests a discard is not counted; only its rerun counts. fn settling_counts_one_real_pass_and_skips_a_discarded_one() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1880,6 +1917,7 @@ mod tests { } #[test] + /// Issuance survives a discard requested after it, the image is written once, and a duplicate callback completion is inert. fn a_discard_requested_after_issuance_still_issues_and_writes_once() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -1913,6 +1951,7 @@ mod tests { } #[test] + /// A grow-resize spends one retry, reissues at the new size and then accepts the new image. fn an_image_larger_than_the_reservation_retries_like_a_smaller_one() { let ctx = sized_context(egui::vec2(10.0, 8.0)); let mut app = test_app(); @@ -1945,6 +1984,7 @@ mod tests { } #[test] + /// Minimize retries reserve unknown-size bytes; only two fit the aggregate budget and stay queued (the rest are reported), and the survivors write after restore. fn a_minimize_retry_without_a_size_still_checks_the_aggregate_budget() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -2026,6 +2066,7 @@ mod tests { } #[test] + /// Fresh hidden viewport geometry invalidates the old image, spends one retry, and the reissued image succeeds. fn a_hidden_resize_rejects_an_image_issued_before_it_and_reissues() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -2065,6 +2106,7 @@ mod tests { } #[test] + /// Draining four completed captures replies to all of them and permits a new admission. fn a_drained_capture_frees_its_slot_for_the_next_admission() { let ctx = sized_context(egui::vec2(8.0, 8.0)); let mut app = test_app(); @@ -2093,6 +2135,7 @@ mod tests { } #[test] + /// Uses `screen_rect` when `inner_rect` is absent, applies native scale times zoom, and advances the epoch on a hidden resize. fn raw_input_geometry_uses_screen_rect_when_inner_rect_is_absent() { // Wayland always omits `inner_rect`; the fresh `RawInput::screen_rect` is the only size. let ctx = sized_context(egui::vec2(800.0, 600.0)); @@ -2126,6 +2169,7 @@ mod tests { } #[test] + /// Minimized geometry has no size and reserves unknown bytes; restoring at the same size still changes the epoch. fn minimized_geometry_has_no_size_and_restore_bumps_the_epoch() { let ctx = egui::Context::default(); let mut app = test_app(); @@ -2160,6 +2204,7 @@ mod tests { } #[test] + /// Switching documents before issuance cancels the request once with a document-changed error and writes nothing. fn a_document_switch_before_issuance_cancels_without_writing() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -2180,6 +2225,7 @@ mod tests { } #[test] + /// Switching documents after issuance rejects the completed image once and writes nothing. fn a_document_switch_after_issuance_rejects_the_completed_image() { let ctx = sized_context(egui::vec2(64.0, 48.0)); let mut app = test_app(); @@ -2199,6 +2245,7 @@ mod tests { } #[test] + /// A request bound to another viewport is rejected once with a viewport error and writes nothing. fn a_completion_for_another_viewport_is_rejected() { let ctx = sized_context(egui::vec2(32.0, 24.0)); let mut app = test_app(); @@ -2219,6 +2266,7 @@ mod tests { } #[test] + /// Accepts one-pixel rounding on a single axis, but rejects opposite-axis deviation, aspect distortion and more than one pixel of error. fn validate_image_rejects_distortion_but_allows_one_pixel_rounding() { let image = |w: usize, h: usize| egui::ColorImage::filled([w, h], egui::Color32::RED); assert!(validate_image(&image(100, 100), [100, 100]).is_ok()); @@ -2230,6 +2278,7 @@ mod tests { } #[test] + /// Checks the encoding working-set threshold, the base64 reply cap, and the retained-plus-encoding peak budget. fn encoding_working_set_is_bounded_for_admitted_captures() { assert!(encoding_working_bytes([8191, 2048], false).expect("checked") <= SCREENSHOT_MAX_ENCODING_BYTES); assert!(encoding_working_bytes([8192, 8192], false).expect("checked") > SCREENSHOT_MAX_ENCODING_BYTES); @@ -2244,6 +2293,7 @@ mod tests { } #[test] + /// An oversized base64 capture is refused with nothing reserved, while the same capture as a path is admitted. fn base64_capture_over_the_encoding_budget_is_rejected_before_admission() { let ctx = sized_context(egui::vec2(5000.0, 4000.0)); let mut app = test_app(); @@ -2267,6 +2317,7 @@ mod tests { } #[test] + /// Oversized text, repeated maximum texts, keys and moves are refused without changing the queue, and the byte/event limits are both enforced. fn synthetic_text_key_and_move_producers_honour_the_queue_budget() { let ctx = egui::Context::default(); let mut app = test_app(); diff --git a/crates/ui-egui/src/craft_fonts_policy.rs b/crates/ui-egui/src/craft_fonts_policy.rs index 01caed7b0..d88fa6a75 100644 --- a/crates/ui-egui/src/craft_fonts_policy.rs +++ b/crates/ui-egui/src/craft_fonts_policy.rs @@ -162,6 +162,7 @@ mod tests { const GOOD: &str = "NanumGothic | Regular | fonts/nanum-gothic/NanumGothic-Regular.ttf | Hang,Kore | OFL-1.1 | fonts/nanum-gothic/OFL.txt | 76f45ef4a6bcff344c837c95a7dcc26e017e38b5846d5ae0cdcb5b86be2e2d31 | https://example.invalid/nanum"; #[test] + /// Asserts the exact web allowlist and that every entry is unbarred, required and not a CJK collection face. fn the_web_allowlist_is_exact_and_never_barred() { assert_eq!( WEB_ALLOWED, @@ -181,6 +182,7 @@ mod tests { } #[test] + /// Adobe/Source and Noto CJK families are barred, while the permitted Noto Symbols, BIZ and Nanum faces are not. fn the_barred_prefixes_keep_the_permitted_noto_symbol_faces() { for family in ["Adobe Source Sans", "Source Han Sans", "Source Sans 3", "Source Serif 4", "Noto Sans CJK SC", "Noto Serif CJK JP"] { assert!(barred(family), "{family} must be barred"); @@ -191,6 +193,7 @@ mod tests { } #[test] + /// Only the exact string `"1"` enables required mode; absent and all other values leave it off. fn only_the_value_one_enables_required_mode() { assert!(!required_mode(None)); for value in ["", "0", "00", "01", "false", "true", "yes"] { @@ -200,6 +203,7 @@ mod tests { } #[test] + /// A complete eight-field row yields the expected family, style and script list. fn a_complete_manifest_row_parses() { let rows = parse_manifest(GOOD).unwrap_or_default(); assert_eq!(rows.len(), 1); @@ -208,6 +212,7 @@ mod tests { } #[test] + /// Missing fields, an empty file field, a bad hash and a non-ISO script tag are all rejected. fn malformed_rows_are_errors() { for bad in [ "NanumGothic | Regular | fonts/x.ttf | Hang,Kore", @@ -220,6 +225,7 @@ mod tests { } #[test] + /// One row cannot satisfy every required face; script order and exact tags are checked, and a required family in an unlisted style is rejected. fn required_faces_and_script_tags_are_checked() { let rows = parse_manifest(GOOD).unwrap_or_default(); assert!(check_required(&rows).is_err(), "one row cannot satisfy every required face"); @@ -231,6 +237,7 @@ mod tests { } #[test] + /// LXGW is permitted but native-only; its script order and style are checked, and a required native build must include it. fn the_native_chinese_face_is_permitted_and_checked() { let chinese = "LXGW WenKai Lite | Regular | fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Regular.ttf | Hani,Hans | OFL-1.1 | fonts/lxgw-wenkai-lite/OFL.txt | f799a538a3355877e597a0ae04050e897932c9ae7689eb9227c2953c5dd549ea | https://example.invalid/lxgw"; let rows = parse_manifest(chinese).unwrap_or_default(); @@ -246,6 +253,7 @@ mod tests { } #[test] + /// Native embedding excludes the barred CJK face; web embedding keeps only allowlisted faces. fn web_embedding_keeps_only_the_allowlist() { let manifest = format!( "{GOOD}\nNoto Sans CJK SC | Regular | fonts/cjk.otf | Hans,Latn | OFL-1.1 | fonts/OFL.txt | 76f45ef4a6bcff344c837c95a7dcc26e017e38b5846d5ae0cdcb5b86be2e2d31 | https://example.invalid/cjk" diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7a685c2af..109d0a880 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1251,6 +1251,7 @@ pub(crate) fn build_params(d: &Dialog) -> Json { v } +/// Takes and draws the active dialog, dispatches its body by name, consumes picture-inbox results, runs OK/confirm commands, handles Esc/close and restores the back-dialog stack. pub fn show(app: &mut SheetApp, ctx: &egui::Context) { let Some(mut d) = app.dialog.take() else { return }; if d.name == "pasteSpecial" { @@ -1930,6 +1931,7 @@ fn macros(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog) { }); } +/// Draws the six-tab Format Cells editor (number, alignment, font, border, fill, protection); the Number tab shows a sample preview, and the style draft updates. fn format_cells(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { // `d.tab` stays the English name (it is what `open_dialog` and tests pass); only the drawn // labels are translated. The Fill tab is keyed by context: the Alignment tab's "Fill" is a @@ -2224,6 +2226,7 @@ fn function_category_label(id: &str) -> &str { } } +/// Draws the Find/Replace form with case, whole-cell and workbook options, requests field focus once, and runs find/replace with the current parameters. fn find(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog) { ui.horizontal(|ui| { for tab in ["Find", "Replace"] { @@ -2369,6 +2372,7 @@ fn go_to_entries(app: &SheetApp) -> Vec { .unwrap_or_default() } +/// Draws a virtualized name/reference list and editable reference field, links to Go To Special, and confirms with `edit.goTo`. fn go_to(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { let lang = app.ui.language; let entries: Vec = @@ -2408,6 +2412,7 @@ fn go_to(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bo } } +/// Draws the sort-level editor: header toggle, column/by/order controls, add/remove level, and `data.sort` on confirmation. fn sort(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool) { let lang = app.ui.language; let mut header = d.values.get("header").and_then(Json::as_bool).unwrap_or(true); @@ -2754,6 +2759,7 @@ fn recommended_charts(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, con } } +/// Draws chart range, series keep/remove and category-range controls and runs `chart.selectData` when something changed. fn select_data(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) { let text = |d: &Dialog, k: &str| d.values.get(k).and_then(Json::as_str).unwrap_or("").to_string(); let edit = |ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str| { diff --git a/crates/ui-egui/src/floating_pictures.rs b/crates/ui-egui/src/floating_pictures.rs index 62c167014..7e30a5bdb 100644 --- a/crates/ui-egui/src/floating_pictures.rs +++ b/crates/ui-egui/src/floating_pictures.rs @@ -142,6 +142,7 @@ fn decode(data: &[u8], side: u32) -> Option<(egui::ColorImage, bool)> { Some((egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw()), complete)) } +/// Returns the context-local shared cache of floating-picture textures. fn cache(ctx: &egui::Context) -> Arc> { // Context-local, like cell pictures: texture handles never leak between offscreen, native // and browser contexts. diff --git a/crates/ui-egui/src/formula_bar.rs b/crates/ui-egui/src/formula_bar.rs index 37a6d5ac3..81a668700 100644 --- a/crates/ui-egui/src/formula_bar.rs +++ b/crates/ui-egui/src/formula_bar.rs @@ -47,6 +47,7 @@ impl Default for BarLayout { } impl BarLayout { + /// Returns the stable widget id used for the formula-bar layout state. pub fn id() -> egui::Id { egui::Id::unique("formula_bar.layout") } @@ -64,6 +65,7 @@ pub struct EditorInfo { pub caret: Option, } +/// Draws the formula bar: line-count-driven height, Name Box, cancel/enter/Fx buttons, the editable field, muted read-only spill text and the expansion chevron. pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let font = theme::ui_font(13.0); @@ -185,6 +187,7 @@ fn active_input(app: &SheetApp) -> String { String::new() } +/// Draws the Name Box and its name dropdown; Enter jumps to a reference or known name (or defines a new name) and is consumed so the grid does not move down. fn name_box(app: &mut SheetApp, ui: &mut egui::Ui, t: &Tokens) -> Rect { let id = egui::Id::unique("gridcraft.name_box"); let shown = app.name_box_text(); @@ -778,6 +781,7 @@ mod spill_tests { check(&mut app, "A2", "=FILTER(D1:D3,D1:D3>0)", true); } #[test] + /// A disabled spill formula paints disabled and does not edit; clicking an active formula starts formula-bar editing in both light and dark themes, with accessibility checks. fn formula_bar_click_and_theme_presentation() { use egui_kittest::kittest::Queryable as _; for dark in [false, true] { diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index ed68653be..3b9a6f921 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -3159,6 +3159,7 @@ fn editor_width(text_w: f32, cell_w: f32, size: f32) -> f32 { if text_w + 14.0 <= cell_w { cell_w } else { text_w + editor_slack(size) } } +/// Draws the in-cell editor, growing and wrapping with its text, mirroring for right-to-left sheets, choosing contrasting caret ink and clipping cells hidden under frozen panes. fn in_cell_editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, sh: &Sheet, wb: &Workbook) { let Some(ed) = app.editor.as_ref() else { return }; if ed.sheet != wb.active_sheet { @@ -3243,6 +3244,7 @@ fn filter_table_range(sh: &Sheet, col: u32, row: u32) -> Option { sh.tables.iter().find(|t| t.range.start.row == row && col >= t.range.start.col && col <= t.range.end.col).map(|t| t.range) } +/// Draws the filter-header popup: sort, search/select-all, apply/clear, a lazily installed table autofilter, new-open search focus and dismissal. fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool) { let Some((col, row, at)) = app.grid.filter_menu else { return }; let Some(d) = app.session.active() else { return }; @@ -3360,6 +3362,7 @@ fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool) } } +/// Draws and dispatches the cell context menu, including Show Details when offered, clipboard and insert/format actions; closes on Esc or an outside press. fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) { let Some(at) = app.grid.context_menu else { return }; let mut close = false; @@ -3734,6 +3737,7 @@ pub fn validation_arrow(sh: &Sheet, geo: &Geo, c: CellRef) -> Option { } #[allow(clippy::too_many_arguments)] +/// Paints the validation arrow and input message, notes, the hyperlink tooltip and trace precedent/dependent arrows. pub(crate) fn paint_overlays( app: &mut SheetApp, ui: &egui::Ui, @@ -3856,6 +3860,7 @@ fn list_picker_height(n: usize, row_h: f32, gap: f32) -> f32 { rows * row_h + (rows - 1.0) * gap } +/// Draws the validation list popup, choosing above/below and left/right placement for RTL, and applies the clicked item with `cell.set`. fn list_picker(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool) { let Some(c) = app.grid.list_picker else { return }; let Some(d) = app.session.active() else { return }; @@ -3901,6 +3906,7 @@ fn list_picker(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool) } } +/// Draws row/column header context menus with insert/delete, format, size, hide/unhide and group actions, plus Insert Copied Cells when available. fn header_menu(app: &mut SheetApp, ui: &mut egui::Ui, was_open: bool) { let Some((at, rows)) = app.grid.header_menu else { return }; let mut close = false; diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 174a1f6ee..b7ce6b11e 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -1,5 +1,6 @@ //! Czech translations for the interface strings in i18n.rs. +/// Returns the Czech translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Vložit", diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 557e13ffd..d15e211a0 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -1,6 +1,7 @@ //! German translations for the interface strings in i18n.rs. Labels follow the wording German //! spreadsheet users know; anything absent falls back to English. +/// Returns the German translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Einfügen", diff --git a/crates/ui-egui/src/i18n/el.rs b/crates/ui-egui/src/i18n/el.rs index 4cced5da9..d826b1eb8 100644 --- a/crates/ui-egui/src/i18n/el.rs +++ b/crates/ui-egui/src/i18n/el.rs @@ -2,6 +2,7 @@ //! Command ids, cell references and formula identifiers are not translated. //! Source labels are kept verbatim and unmatched labels fall back to English. +/// Returns the Greek translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Επικόλληση", diff --git a/crates/ui-egui/src/i18n/es.rs b/crates/ui-egui/src/i18n/es.rs index 3437432fe..011434a1d 100644 --- a/crates/ui-egui/src/i18n/es.rs +++ b/crates/ui-egui/src/i18n/es.rs @@ -2,6 +2,7 @@ //! wording (sentence case, «Inmovilizar paneles», «Combinar y centrar»); anything absent falls back //! to English. Formula names and separators are localized separately (`formula_locale`). +/// Returns the Spanish translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Pegar", diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 7a7ec2b9a..65af8830e 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -1,5 +1,6 @@ //! French translations for the interface strings in i18n.rs. +/// Returns the French translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Different First Page" => "Première page différente", diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 0d168ae49..7f79b4076 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -2,6 +2,7 @@ //! wording (sentence case, «Blocca riquadri», «Unisci e allinea al centro»); anything absent falls //! back to English. +/// Returns the Italian translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Incolla", diff --git a/crates/ui-egui/src/i18n/ko.rs b/crates/ui-egui/src/i18n/ko.rs index fd4a3b307..c6c6086ae 100644 --- a/crates/ui-egui/src/i18n/ko.rs +++ b/crates/ui-egui/src/i18n/ko.rs @@ -1,5 +1,6 @@ //! Korean translations for the interface strings in i18n.rs. +/// Returns the Korean translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "붙여넣기", diff --git a/crates/ui-egui/src/i18n/nl.rs b/crates/ui-egui/src/i18n/nl.rs index 3ed2e8ef1..9cedcce74 100644 --- a/crates/ui-egui/src/i18n/nl.rs +++ b/crates/ui-egui/src/i18n/nl.rs @@ -1,5 +1,6 @@ //! Dutch translations for the interface strings in i18n.rs. +/// Returns the Dutch translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Plakken", diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index ed68e823e..1a229f90d 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -1,3 +1,4 @@ +/// Returns the Polish translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Wklej", diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs index 706f19510..fa840c35f 100644 --- a/crates/ui-egui/src/i18n/pt.rs +++ b/crates/ui-egui/src/i18n/pt.rs @@ -1,6 +1,7 @@ //! Brazilian Portuguese translations for the interface strings in i18n.rs. Labels follow Excel's //! pt-BR wording; anything absent falls back to English. +/// Returns the Portuguese translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Colar", diff --git a/crates/ui-egui/src/i18n/ro.rs b/crates/ui-egui/src/i18n/ro.rs index 480c802e8..e71576848 100644 --- a/crates/ui-egui/src/i18n/ro.rs +++ b/crates/ui-egui/src/i18n/ro.rs @@ -5,6 +5,7 @@ //! usual in Romanian software ("Salvare", "Inserare…") and messages address the user formally. //! An AI-assisted draft (Claude) for a Romanian-speaking contributor, pending native-speaker review. +/// Returns the Romanian translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Lipire", diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 7e547eff4..6c88b4f66 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -1,5 +1,6 @@ //! Russian translations for the interface strings in i18n.rs. +/// Returns the Russian translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Вставить", diff --git a/crates/ui-egui/src/i18n/sr.rs b/crates/ui-egui/src/i18n/sr.rs index d1b6d7d2c..0d3f71a40 100644 --- a/crates/ui-egui/src/i18n/sr.rs +++ b/crates/ui-egui/src/i18n/sr.rs @@ -1,5 +1,6 @@ //! Serbian (Latin script, ekavian) translations for the interface strings in i18n.rs. +/// Returns the Serbian translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Nalepi", diff --git a/crates/ui-egui/src/i18n/tr.rs b/crates/ui-egui/src/i18n/tr.rs index a97245eaa..ea1a16380 100644 --- a/crates/ui-egui/src/i18n/tr.rs +++ b/crates/ui-egui/src/i18n/tr.rs @@ -1,6 +1,7 @@ //! Turkish translations for the interface strings in i18n.rs. Labels follow the wording Turkish //! spreadsheet users know; anything absent falls back to English. +/// Returns the Turkish translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Yapıştır", diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 4a89c227c..8055f77fc 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1,5 +1,6 @@ //! Ukrainian translations for the interface strings in i18n.rs. +/// Returns the Ukrainian translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Вставити", diff --git a/crates/ui-egui/src/i18n/vi.rs b/crates/ui-egui/src/i18n/vi.rs index 68ca4d2b4..874c7caff 100644 --- a/crates/ui-egui/src/i18n/vi.rs +++ b/crates/ui-egui/src/i18n/vi.rs @@ -1,6 +1,7 @@ //! Vietnamese translations for the interface strings in i18n.rs. Labels follow the wording //! Vietnamese spreadsheet users know; anything absent falls back to English. +/// Returns the Vietnamese translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "Dán", diff --git a/crates/ui-egui/src/i18n/zh.rs b/crates/ui-egui/src/i18n/zh.rs index 9d22d98b4..d9f3c485a 100644 --- a/crates/ui-egui/src/i18n/zh.rs +++ b/crates/ui-egui/src/i18n/zh.rs @@ -1,5 +1,6 @@ //! Simplified Chinese translations for the interface strings in i18n.rs. +/// Returns the Chinese translation for `text`, or `None` so callers fall back to English. pub(super) fn translate(text: &str) -> Option<&'static str> { Some(match text { "Paste" => "粘贴", diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 70b5027cb..33837022a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -329,6 +329,7 @@ pub struct SheetApp { } impl SheetApp { + /// Constructs a `SheetApp` with default UI, grid, printing, control and screenshot state and the caller's session and services. pub fn new(session: Session, services: Services) -> SheetApp { SheetApp { session, @@ -1425,6 +1426,7 @@ impl SheetApp { ctx.request_repaint(); } + /// Records capture geometry from the fresh raw frame, then offers exactly one pending synthetic batch and tracks whether it was applied. pub fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) { // Capture geometry from this frame's real input before any synthetic event is added. This // runs on every native frame, including hidden/minimized logic-only ticks, so a resize, @@ -1649,6 +1651,7 @@ pub fn monotonic_ms() -> f64 { } #[cfg(target_arch = "wasm32")] +/// Web monotonic clock: the browser's `performance.now()`, or 0 when it is unavailable. pub fn monotonic_ms() -> f64 { web_sys::window().and_then(|window| window.performance()).map_or(0.0, |performance| performance.now()) } @@ -1661,10 +1664,12 @@ pub fn cell(r: u32, c: u32) -> CellRef { mod lifecycle_tests { use super::*; + /// Builds a key-A press or release event without modifiers. fn key_event(pressed: bool) -> egui::Event { egui::Event::Key { key: egui::Key::A, physical_key: None, pressed, repeat: false, modifiers: egui::Modifiers::NONE } } + /// Returns a test app with a new workbook and default services. fn app() -> SheetApp { let mut session = Session::new(); session.new_workbook(); @@ -1680,6 +1685,7 @@ mod lifecycle_tests { } #[test] + /// Only one synthetic step is offered, is not duplicated by a hidden logic-only tick, and press/release remain distinct batches. fn a_hidden_logic_tick_keeps_one_unapplied_synthetic_step() { let ctx = egui::Context::default(); let mut app = app(); @@ -1700,11 +1706,13 @@ mod lifecycle_tests { assert_eq!(app.synthetic.len(), 2, "press and release stay distinct batches"); } + /// Returns raw input with a screen rect at the given size. fn raw_with_size(size: egui::Vec2) -> egui::RawInput { egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size)), ..Default::default() } } #[test] + /// A font-gated early-returning pass does not lose the queued step; the hook offers it again and a later real pass applies it. fn a_font_gated_pass_offers_the_synthetic_step_again() { let ctx = egui::Context::default(); ctx.set_fonts(theme::font_definitions()); @@ -1735,6 +1743,7 @@ mod lifecycle_tests { } #[test] + /// A language-driven font rebuild waits while a synthetic step is pending and proceeds once it is clear. fn a_pending_step_delays_a_font_change() { let ctx = egui::Context::default(); let mut app = app(); @@ -1752,7 +1761,9 @@ mod lifecycle_tests { } #[test] + /// Logic ticks see fresh viewport and DPI information; resizing back to the original size still advances the capture epoch. fn a_hidden_logic_tick_sees_fresh_viewport_info_and_keeps_the_epoch_monotonic() { + /// Runs one logic tick with the requested width and native scale. fn tick(ctx: &egui::Context, app: &mut SheetApp, width: f32, native_ppp: f32) { let mut raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, 400.0))), ..Default::default() }; @@ -1778,7 +1789,9 @@ mod lifecycle_tests { } #[test] + /// A resize and a resize back both advance the capture epoch. fn the_capture_epoch_changes_even_when_the_geometry_returns() { + /// Runs one UI pass at the requested width. fn tick(ctx: &egui::Context, app: &mut SheetApp, width: f32) { let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, 400.0))), ..Default::default() }; @@ -1795,6 +1808,7 @@ mod lifecycle_tests { assert!(app.capture_epoch > changed, "resizing back is still a new epoch: old pixels must not be saved"); } + /// Extracts title viewport commands from a logic output. fn titles(out: &egui::LogicOutput) -> Vec { out.viewport_commands .values() @@ -1807,6 +1821,7 @@ mod lifecycle_tests { } #[test] + /// A hidden logic tick saves a dirty file-backed workbook once and clears its dirty state. fn a_logic_only_tick_autosaves_a_dirty_workbook_once() { let ctx = egui::Context::default(); let mut app = app(); @@ -1830,6 +1845,7 @@ mod lifecycle_tests { } #[test] + /// The title is sent on the first tick and after a rename, but not on every unchanged frame. fn a_logic_only_tick_updates_the_title_once_per_change() { let ctx = egui::Context::default(); let mut app = app(); @@ -1847,6 +1863,7 @@ mod lifecycle_tests { } #[test] + /// A hidden tick surfaces a failed print job in the app message. fn a_logic_only_tick_polls_printing_feedback() { let ctx = egui::Context::default(); let mut app = app(); @@ -1864,6 +1881,7 @@ mod lifecycle_tests { } #[test] + /// Pending synthetic input makes logic request a real hidden paint. fn a_pending_synthetic_step_requests_a_hidden_paint() { let ctx = egui::Context::default(); let mut app = app(); diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index 81ccd3b96..65d169c87 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -50,6 +50,7 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { }); } +/// Lists comments and replies, links each to its cell, and runs delete/resolve/reopen/reply commands. fn comments(app: &mut SheetApp, ui: &mut Ui) { let Some(d) = app.session.active() else { return }; let Some(sh) = d.wb.active() else { return }; @@ -106,6 +107,7 @@ fn comments(app: &mut SheetApp, ui: &mut Ui) { } } +/// Adds the active cell, lists watch values and formulas, and removes watched entries. fn watch(app: &mut SheetApp, ui: &mut Ui) { if ui.button("Add Watch (active cell)").clicked() && let Some(d) = app.session.active() @@ -134,6 +136,7 @@ fn watch(app: &mut SheetApp, ui: &mut Ui) { }); } +/// Lists the sheet's objects and dispatches select, bring-forward, send-backward and delete. fn selection(app: &mut SheetApp, ui: &mut Ui) { let list = app.session.run("arrange.selectionPane", json!({})).unwrap_or(Json::Null); let items = list.as_array().cloned().unwrap_or_default(); @@ -161,6 +164,7 @@ fn selection(app: &mut SheetApp, ui: &mut Ui) { } } +/// Draws the chart format pane: title, type, legend, elements/axis titles, series colors, palettes/layouts and size. fn format_chart(app: &mut SheetApp, ui: &mut Ui) { let Some(id) = app.selected_chart else { ui.label(egui::RichText::new("Select a chart to format it.").italics()); diff --git a/crates/ui-egui/src/paper_view.rs b/crates/ui-egui/src/paper_view.rs index cdf56bddb..fa2313d67 100644 --- a/crates/ui-egui/src/paper_view.rs +++ b/crates/ui-egui/src/paper_view.rs @@ -438,6 +438,7 @@ pub fn go_to(app: &mut SheetApp, footer: bool) { begin(app, sheet, &sh, page, HfZone { footer, section }); } +/// Edits a header/footer section across left/center/right, cancels on Esc, switches sections and preserves focus/caret while the page scrolls. fn editor(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, pg: &PageGrid) { let Some(mut e) = app.grid.hf_edit.take() else { return }; if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape)) { diff --git a/crates/ui-egui/src/pivot_pane.rs b/crates/ui-egui/src/pivot_pane.rs index c29c71b9d..a6bffce4e 100644 --- a/crates/ui-egui/src/pivot_pane.rs +++ b/crates/ui-egui/src/pivot_pane.rs @@ -17,6 +17,7 @@ pub fn active_pivot(app: &SheetApp) -> Option { sh.pivots.iter().find(|p| p.last_range.is_some_and(|r| r.contains(a)) || p.anchor == a).map(|p| p.name.clone()) } +/// Draws the PivotTable Fields pane: add/remove fields, move them between areas, sort/group/value settings, refresh, calculated field and report layout. pub fn show(app: &mut SheetApp, ui: &mut Ui) { let Some(name) = active_pivot(app) else { return }; let t = Tokens::get(ui.ctx()); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 4958ecd59..aa1d2035d 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -35,6 +35,7 @@ const TITLE_BAR_H: f32 = if MAC { MAC_TITLE_BAR } else { 38.0 }; const TITLE_CONTROL_H: f32 = if MAC { 22.0 } else { 26.0 }; const _: () = assert!(TITLE_CONTROL_H < TITLE_BAR_H, "title-bar controls keep a margin above and below"); +/// Draws the title bar: drag/maximize, AutoSave toggle, Home/Save/Undo/Redo/More, search, share, comments and theme controls, with a centered truncated title. pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let ch = TITLE_CONTROL_H; @@ -210,6 +211,7 @@ fn paint_title(ui: &Ui, center: egui::Pos2, title: &str, dirty: bool, max_w: f32 } } +/// Draws the ribbon tabs and the active tab's horizontally scrollable card, resetting an unavailable contextual tab to Home. pub fn show(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let collapsed = app.ui.ribbon_collapsed; @@ -345,6 +347,7 @@ pub struct RibbonOverflow { } impl RibbonOverflow { + /// Returns the stable id for the ribbon overflow state. fn id() -> egui::Id { egui::Id::unique("ribbon_overflow") } @@ -736,6 +739,7 @@ fn menu_items(app: &mut SheetApp, ui: &mut Ui, items: &[(&str, &str, serde_json: } } +/// Draws the Home tab: clipboard/paste menu, font, alignment, number, style, cell and editing commands with keytips. fn home(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let st = active_style(app); @@ -1642,6 +1646,7 @@ fn cell_style_gallery(app: &mut SheetApp, ui: &mut Ui) { } } +/// Draws the Insert tab: PivotTable, Table, Pictures, Shapes, Icons, charts, slicer, sparklines, checkbox, links, comments, text box, header/footer and symbols. fn insert(app: &mut SheetApp, ui: &mut Ui) { let __k = big_button(ui, Icon::PivotTable, "PivotTable", "PivotTable", false); control_keytip(app, ui, &__k, "insert.pivotTable"); @@ -2439,6 +2444,7 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { } } +/// Draws the View tab: view modes, ruler/gridlines/formula-bar/headings toggles, zoom, freeze panes, theme, animations, interface size and language. fn view(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let _ = t; @@ -2615,6 +2621,7 @@ fn automate(app: &mut SheetApp, ui: &mut Ui) { } } +/// Draws the Table Design tab: rename, resize, remove duplicates, convert to range, slicer, table options and the style strip. fn table_design(app: &mut SheetApp, ui: &mut Ui) { let Some((tname, header, totals, banded, banded_c, first, last, filter, style)) = app.session.active().and_then(|d| { let sh = d.wb.active()?; diff --git a/crates/ui-egui/src/tabs.rs b/crates/ui-egui/src/tabs.rs index a56219eef..b2216ca93 100644 --- a/crates/ui-egui/src/tabs.rs +++ b/crates/ui-egui/src/tabs.rs @@ -97,6 +97,7 @@ fn tab_split_handle(app: &mut SheetApp, ui: &mut Ui, split: Rect, bar: (f32, f32 } } +/// Draws the worksheet tab strip: navigation, scroll/reveal, rename/drag/move with optional copy, context menu, new-sheet button, split handle and grid scrollbar. pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::bottom("sheet_tabs").exact_size(32.0).frame(egui::Frame::NONE.fill(t.tab_bar)).show(ui, |ui| { @@ -318,6 +319,7 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) { }); } +/// Draws status mode/messages, zoom controls/view-mode buttons and selection statistics. pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::bottom("status_bar").exact_size(26.0).frame(egui::Frame::NONE.fill(t.status_bar).inner_margin(egui::Margin::symmetric(12, 0))).show( diff --git a/crates/ui-egui/src/text_box.rs b/crates/ui-egui/src/text_box.rs index 21b788cfa..c618c89f4 100644 --- a/crates/ui-egui/src/text_box.rs +++ b/crates/ui-egui/src/text_box.rs @@ -17,6 +17,7 @@ pub struct EditState { rect: Option, } +/// Returns the stable id for the text-box editor. pub(crate) fn editor_id() -> egui::Id { egui::Id::unique("gridcraft.text_box_editor") } @@ -110,6 +111,7 @@ pub(crate) fn contains_pointer(app: &SheetApp, ctx: &egui::Context) -> bool { app.text_box_editor.as_ref().and_then(|ed| ed.rect).is_some_and(|r| ctx.input(|i| i.pointer.hover_pos().is_some_and(|p| r.contains(p)))) } +/// Draws the text-box editor with focus/caret state, save shortcuts, scrolling/clipping, and clears editing mode when the shape is gone. pub(crate) fn show(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, sh: &gridcraft_engine::model::Sheet) { let Some(mut ed) = app.text_box_editor.take() else { return }; if app.view_key() != Some((ed.uid, ed.sheet)) { diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 63f35e51d..0c7ec878a 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -693,6 +693,7 @@ pub fn font_definitions_with(language: crate::i18n::Language, used: &[String]) - font_definitions_impl(language, used, true) } +/// Builds the role font chains, script fallbacks, installed workbook fonts and optional craft-font faces, drops unparseable data and records which families were bound. fn font_definitions_impl(language: crate::i18n::Language, used: &[String], system: bool) -> FontDefinitions { let mut fonts = FontDefinitions::default(); let base_prop: Vec = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default(); @@ -1098,6 +1099,7 @@ pub fn symbol_face_installed() -> bool { } } +/// Installs light and dark token visuals in both theme slots, keeps the 2-3 px widget and 8-10 px window/menu corner radii from 0.36, and sets spacing, text styles and the active theme. pub fn apply(ctx: &egui::Context, dark: bool) { // Install both palettes before System can switch between the style slots. for theme in [egui::Theme::Light, egui::Theme::Dark] { @@ -1298,6 +1300,7 @@ mod tests { use crate::i18n::Language; #[test] + /// Loaded script-fallback faces end each role chain just before the craft fonts, in the language's order. fn fallback_faces_are_appended_to_every_family() { // Structural: whatever loaded, the non-Latin fallback keys must sit at the end of each chain // (after the Latin/default fonts) so Latin still wins for Latin text, in the language's order. @@ -1320,6 +1323,7 @@ mod tests { } #[test] + /// Every font family ends with exactly the permitted craft faces in manifest order. fn craft_fonts_are_the_last_fallback_in_every_family() { // Ordering: app/bundled faces, Hack/emoji, system script fallbacks, permitted craft faces. // A craft face must never displace a system face that already covers a glyph. @@ -1335,6 +1339,7 @@ mod tests { } #[test] + /// Skips on wasm or when required mode is off; a required native build must embed LXGW with `Hani`/`Hans`. fn a_required_native_build_embeds_the_chinese_face() { if cfg!(target_arch = "wasm32") || !craft_fonts_required() { return; @@ -1348,6 +1353,7 @@ mod tests { } #[test] + /// No embedded craft face belongs to the barred Noto CJK, Adobe or Source families. fn barred_faces_never_enter_the_font_set() { for (family, _, _) in craft_font_scripts() { assert!(!family.contains("CJK"), "the barred Noto CJK collection must never be embedded: {family}"); @@ -1356,6 +1362,7 @@ mod tests { } #[test] + /// Skips when craft fonts are absent unless required mode is set; otherwise the required faces and their exact script tags are embedded. fn the_required_craft_faces_keep_their_exact_scripts() { if !craft_fonts_loaded() { assert!(!craft_fonts_required(), "CRAFT_FONTS_REQUIRED=1 but no craft-fonts faces were embedded"); @@ -1377,6 +1384,7 @@ mod tests { } #[test] + /// Interface text maps unbundled `⌃`, `✓` and `✕` to readable chrome fallbacks; document text is not rewritten. fn available_text_maps_unbundled_chrome_symbols() { // No bundled face has ⌃ ✓ ✕; chrome text must show ^ ✔ × instead of tofu. Document text is // never rewritten: this helper is only used for interface tooltips. @@ -1452,6 +1460,7 @@ mod tests { } #[test] + /// A font file shared by several fallback scripts is loaded once, with one shared font blob rather than a copy per script. fn fallback_faces_load_each_file_once() { // The region-merged Noto Sans CJK .ttc serves Japanese, Chinese and Hangul; it must be loaded // once, not once per script (tens of MB each). @@ -1468,6 +1477,7 @@ mod tests { } #[test] + /// Checks that the proportional and cell families have glyphs for every present script sample; samples with no system or craft face are printed as skipped. fn fallback_covers_its_script() { // Real glyph check, per script: the proportional family (what cells and the UI use) must // have a glyph for every non-space character in the sample. A script runs when a system @@ -1528,6 +1538,7 @@ mod tests { } #[test] + /// Empty, garbage, truncated and bad-index fonts are refused; a bad face is dropped from the definitions and `set_fonts` does not panic. fn unparseable_font_files_are_rejected_not_fatal() { // #226: epaint panics in `set_fonts` on a face it can't parse ("An offset was out of // bounds"). Garbage, a truncated real font and a bad collection index must all be refused. @@ -1558,6 +1569,7 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] #[test] + /// A broken existing font candidate is skipped in favor of a later readable candidate. fn try_load_skips_a_broken_candidate() { // A candidate that exists but is not a readable font must not win over a good one behind it. let dir = std::env::temp_dir().join(format!("gridcraft-font-test-{}", std::process::id())); @@ -1576,6 +1588,7 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] #[test] + /// Repeated loads reuse one shared font blob, while a face the kept file does not contain is still refused. fn interface_fonts_are_read_once_and_shared_with_egui() { // egui copies owned font bytes into its text engine and keeps the original too; borrowed // bytes it shares. A rebuild of the font set reuses the file read the first time. diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 6713b5700..ea398ea4d 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -307,6 +307,7 @@ pub fn remember_color(recent: &mut Vec, hex: &str) { recent.truncate(RECENT_COLORS); } +/// Returns the stable id for the recent-colors cache. fn recent_colors_id() -> egui::Id { egui::Id::unique("gridcraft.recent_colors") } @@ -338,6 +339,15 @@ pub fn color_palette_fixed(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &s palette(ui, theme_colors, none_label, false) } +/// Shared body of [`color_palette`] and [`color_palette_fixed`]: a 7×10 palette (six tinted theme rows +/// above ten standard colours), then Recent Colors; the separator and More Colors… button appear only +/// when `more_colors` is true; an existing picker state renders while open. Arrow keys move a highlight +/// through all 70 swatches (rows clamp, columns +/// wrap), Enter picks it, and those key events are stripped so the grid behind does not react; while More +/// Colors… is open its HSVA/hex widgets take the keys instead. The None label returns `Some("none")`; +/// otherwise the result is the picked hex or `None`. Highlight, picker state and recent colours live in +/// egui temp storage, and More Colors… OK falls back to the picker colour for an invalid typed hex and +/// records the result as the newest recent colour (capped at `RECENT_COLORS`). fn palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str, more_colors: bool) -> Option { let t = Tokens::get(ui.ctx()); let mut picked = None; @@ -533,6 +543,7 @@ fn palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str, more_colors: picked } +/// Draws the modal warning/error message; OK, Enter or Esc clears it and restores editor focus when an edit exists. pub fn message_box(app: &mut SheetApp, ctx: &egui::Context) { let Some((title, msg)) = app.message.clone() else { return }; let title = app.ui.language.tr(&title).to_owned(); @@ -567,6 +578,7 @@ pub fn message_box(app: &mut SheetApp, ctx: &egui::Context) { } } +/// Shows a translated toast for 2.5 seconds and clears it after the deadline. pub fn toast(app: &mut SheetApp, ctx: &egui::Context) { let Some((msg, at)) = app.toast.clone() else { return }; if crate::now_ms() - at > 2500.0 { @@ -671,6 +683,7 @@ mod glyph_button_tests { use super::*; #[test] + /// The `✓` gallery button paints vector paths instead of the missing character. fn a_symbol_with_no_bundled_glyph_paints_a_code_icon() { // ✓ has no bundled egui face. The gallery button must paint Icon::Check (vector paths), // not the missing character, without weakening the font or accessibility policy. @@ -691,6 +704,7 @@ mod tooltip_shortcut_tests { use super::tooltip_shortcut; #[test] + /// Windows/Linux tooltips use Ctrl/Alt shortcuts and macOS uses Command glyphs; a command without a shortcut reports none. fn tooltips_show_each_platforms_own_shortcut() { // #1191: Windows and Linux show Excel for Windows' shortcuts, never the Mac ⌘ ones. assert_eq!(tooltip_shortcut("Bold", false), Some("Ctrl+B")); diff --git a/crates/ui-egui/tests/it/advanced_filter.rs b/crates/ui-egui/tests/it/advanced_filter.rs index 43b10f4bd..f838ce827 100644 --- a/crates/ui-egui/tests/it/advanced_filter.rs +++ b/crates/ui-egui/tests/it/advanced_filter.rs @@ -18,6 +18,7 @@ fn control(h: &mut egui_kittest::Harness<'static, SheetApp>, method: &str, param } #[test] +/// Checks that the copy destination field appears and that a unique extract writes `B` to C3, closing the dialog without a message. fn dialog_offers_copy_action_and_runs_unique_extract() { let mut h = harness(); h.state_mut().run("range.setValues", json!({"range": "A1", "values": [["Name"], ["A"], ["A"], ["B"]]})).unwrap(); diff --git a/crates/ui-egui/tests/it/autosave_persist.rs b/crates/ui-egui/tests/it/autosave_persist.rs index 5f211148f..b415baf01 100644 --- a/crates/ui-egui/tests/it/autosave_persist.rs +++ b/crates/ui-egui/tests/it/autosave_persist.rs @@ -3,6 +3,7 @@ use gridcraft_engine::Session; #[test] +/// AutoSave defaults off, toggles on and off, and survives `UiState` serialization. fn autosave_survives_a_restart() { let mut app = crate::common::app(Session::new()); assert!(!app.ui.autosave, "off by default"); diff --git a/crates/ui-egui/tests/it/chart_resize.rs b/crates/ui-egui/tests/it/chart_resize.rs index 1f811b1ee..a2c36e3f3 100644 --- a/crates/ui-egui/tests/it/chart_resize.rs +++ b/crates/ui-egui/tests/it/chart_resize.rs @@ -133,6 +133,7 @@ fn right_click(h: &mut Harness, pos: egui::Pos2, wobble: egui::Vec2) { } #[test] +/// A right-click selects the chart and opens its menu rather than the cell menu without moving the chart; Delete removes it, and an outside right-click still opens the cell menu. fn right_clicking_a_chart_opens_its_menu_not_the_cells() { // #2067: a right-click on a chart opened the menu of the cell under it, or nothing at all. for wobble in [vec2(0.0, 0.0), vec2(20.0, 10.0)] { diff --git a/crates/ui-egui/tests/it/committed_text_fields.rs b/crates/ui-egui/tests/it/committed_text_fields.rs index 1aaf0b5bb..e1e8c6d8a 100644 --- a/crates/ui-egui/tests/it/committed_text_fields.rs +++ b/crates/ui-egui/tests/it/committed_text_fields.rs @@ -37,6 +37,7 @@ fn retype(h: &mut Harness, field: egui::Rect, text: &str) { } #[test] +/// Editing the Format Chart title field sets the chart title. fn typing_a_chart_title_in_format_chart_area_sets_it() { let mut session = session(); session.execute("selection.set", json!({"range": "A1:B3"})).unwrap(); diff --git a/crates/ui-egui/tests/it/contextual_ribbon_tab.rs b/crates/ui-egui/tests/it/contextual_ribbon_tab.rs index d7b15f760..ae318a22a 100644 --- a/crates/ui-egui/tests/it/contextual_ribbon_tab.rs +++ b/crates/ui-egui/tests/it/contextual_ribbon_tab.rs @@ -21,6 +21,7 @@ fn session_with_table() -> Session { session } +/// Sets a saved ribbon tab on a fresh app and builds the stepped harness, for unavailable-tab cases. fn app_restored_on(tab: &str, session: Session) -> Harness { let mut app = crate::common::app(session); app.ui.ribbon_tab = tab.into(); diff --git a/crates/ui-egui/tests/it/dialog_button_row.rs b/crates/ui-egui/tests/it/dialog_button_row.rs index e0242bf6d..5bbaca51f 100644 --- a/crates/ui-egui/tests/it/dialog_button_row.rs +++ b/crates/ui-egui/tests/it/dialog_button_row.rs @@ -7,6 +7,7 @@ use gridcraft_ui_egui::SheetApp; use serde_json::json; #[test] +/// OK sits between 0 and 40 points below the last dialog option. fn ok_and_cancel_sit_under_the_last_field() { let mut s = Session::new(); s.new_workbook(); diff --git a/crates/ui-egui/tests/it/dialog_move.rs b/crates/ui-egui/tests/it/dialog_move.rs index 68b232844..1b63d601a 100644 --- a/crates/ui-egui/tests/it/dialog_move.rs +++ b/crates/ui-egui/tests/it/dialog_move.rs @@ -21,6 +21,7 @@ fn harness() -> Harness { h } +/// Returns the remembered egui area rect for a named dialog. fn dialog_rect(h: &Harness, name: &str) -> egui::Rect { h.ctx.memory(|m| m.area_rect(egui::Id::unique("dialog").with(name))).unwrap() } diff --git a/crates/ui-egui/tests/it/error_checking_menu.rs b/crates/ui-egui/tests/it/error_checking_menu.rs index 724babd80..e0129c5c1 100644 --- a/crates/ui-egui/tests/it/error_checking_menu.rs +++ b/crates/ui-egui/tests/it/error_checking_menu.rs @@ -14,6 +14,7 @@ type Harness = egui_kittest::Harness<'static, SheetApp>; /// longer folds it into the label, so screen readers (and kittest) see the purpose without it. const CIRCULAR: &str = "Circular References"; +/// Builds the Formulas-tab harness, optionally adding cross-sheet and same-sheet circular references and selecting H20. fn harness(loops: bool) -> Harness { let mut s = Session::new(); s.new_workbook(); @@ -131,6 +132,7 @@ fn circular_references_scrolls_to_the_cell_picked() { } #[test] +/// Wheel scrolling brings A100 into view, and clicking it selects A100. fn circular_references_scrolls_a_list_taller_than_the_window() { let mut h = harness(false); for row in 1..=100 { diff --git a/crates/ui-egui/tests/it/file_menu.rs b/crates/ui-egui/tests/it/file_menu.rs index 1cf0396f9..a2c7015af 100644 --- a/crates/ui-egui/tests/it/file_menu.rs +++ b/crates/ui-egui/tests/it/file_menu.rs @@ -99,6 +99,7 @@ fn save_actions_include_the_cell_draft_and_close_keeps_the_unsaved_prompt() { } #[test] +/// Options opens, named Backstage pages are reachable and accessible, an invalid page errors, and both close and Back leave Backstage. fn file_options_opens_the_options_page_and_pages_are_reachable_by_command() { let mut h = harness(Services::default()); let r = h.state_mut().run("file.options", serde_json::json!({})).unwrap(); diff --git a/crates/ui-egui/tests/it/filter_list_scroll.rs b/crates/ui-egui/tests/it/filter_list_scroll.rs index e58922f00..b3526c579 100644 --- a/crates/ui-egui/tests/it/filter_list_scroll.rs +++ b/crates/ui-egui/tests/it/filter_list_scroll.rs @@ -42,6 +42,7 @@ fn filter_menu_on_a_long_list() -> Harness { } #[test] +/// A wheel event over the menu's blank right area scrolls the value list. fn filter_list_scrolls_with_the_pointer_right_of_the_values() { let mut h = filter_menu_on_a_long_list(); let first = h.get_by_label("ITEM-001").rect(); diff --git a/crates/ui-egui/tests/it/font_size_box.rs b/crates/ui-egui/tests/it/font_size_box.rs index 057aa26ff..b34b195ff 100644 --- a/crates/ui-egui/tests/it/font_size_box.rs +++ b/crates/ui-egui/tests/it/font_size_box.rs @@ -105,6 +105,7 @@ fn escape_cancels_the_typed_size() { } #[test] +/// The size list has no filter; clicking 16 applies it, and Down then Enter picks 12. fn the_arrow_opens_the_plain_list_of_sizes() { let mut h = harness(); h.get_by_role_and_label(Role::Button, "Font Size").click(); diff --git a/crates/ui-egui/tests/it/formula_bar.rs b/crates/ui-egui/tests/it/formula_bar.rs index 351fb197a..7873332ed 100644 --- a/crates/ui-egui/tests/it/formula_bar.rs +++ b/crates/ui-egui/tests/it/formula_bar.rs @@ -10,6 +10,7 @@ use serde_json::json; type Harness = egui_kittest::Harness<'static, SheetApp>; +/// Builds a stepped 1200x800 harness around the caller's session and settles its frames. fn harness(session: Session) -> Harness { let mut h = crate::common::builder().with_size(vec2(1200.0, 800.0)).with_step_dt(1.0 / 60.0).build_ui_state( |ui, app: &mut SheetApp| { @@ -149,6 +150,7 @@ fn height_eases_instead_of_jumping() { assert!((layout(&h).height - (base + 4.0 * rh)).abs() < 0.5); } +/// Opens a multi-line edit in B3, checks the text is kept whole, and returns the painted editor height. fn in_cell_height(lines: usize) -> f32 { let mut s = blank(); s.execute("selection.set", json!({"cell": "B3"})).unwrap(); @@ -171,6 +173,7 @@ fn in_cell_editor_grows_with_every_line() { } #[test] +/// The code editor indents after Enter and preserves indentation across typed lines, with an accessibility check. fn code_editor_indents_and_keeps_indentation() { struct State { text: String, diff --git a/crates/ui-egui/tests/it/formula_locale.rs b/crates/ui-egui/tests/it/formula_locale.rs index 9be35d6e8..78af96a1a 100644 --- a/crates/ui-egui/tests/it/formula_locale.rs +++ b/crates/ui-egui/tests/it/formula_locale.rs @@ -78,6 +78,7 @@ fn localized_argument_hints_ignore_decimal_commas_arrays_and_nested_calls() { } #[test] +/// Spanish autocomplete offers `SUMA` instead of `SUM`; accepting it inserts `=SUMA(`, and English switches back. fn localized_autocomplete_is_rendered_and_inserts_the_selected_name() { let mut app = app(); app.run("app.language.set", json!({"language":"es"})).unwrap(); @@ -140,6 +141,7 @@ fn locale_preserves_text_cells_and_existing_entry_conveniences() { } #[test] +/// Insert Function and AutoSum use `=SUMA(`, while user-supplied formula text is not translated twice. fn canonical_engine_templates_enter_the_selected_formula_language() { let mut app = app(); app.run("app.language.set", json!({"language":"es"})).unwrap(); diff --git a/crates/ui-egui/tests/it/formula_popup.rs b/crates/ui-egui/tests/it/formula_popup.rs index dd342b574..4dfc9a832 100644 --- a/crates/ui-egui/tests/it/formula_popup.rs +++ b/crates/ui-egui/tests/it/formula_popup.rs @@ -21,11 +21,13 @@ fn frames(ctx: &Context, app: &mut SheetApp) { } } +/// Reports whether the named autocomplete/helper tooltip layer is visible. fn visible(ctx: &Context, editor: &str) -> bool { ctx.memory(|m| m.areas().is_visible(&LayerId::new(Order::Tooltip, Id::unique(editor).with("ac")))) } #[test] +/// Autocomplete and argument help hide when the edited cell scrolls out, the edit is preserved, and both return when it scrolls back. fn cell_formula_helpers_hide_when_scrolled_out_and_return_with_the_editor() { for text in ["=SU", "=SUM("] { let ctx = Context::default(); @@ -88,6 +90,7 @@ fn paints_visible_text(output: &egui::FullOutput, text: &str, cells: Rect) -> bo } #[test] +/// Formula-bar help stays visible while the sheet scrolls. fn formula_bar_hint_remains_visible_when_the_worksheet_scrolls() { let ctx = Context::default(); let mut session = Session::new(); @@ -103,6 +106,7 @@ fn formula_bar_hint_remains_visible_when_the_worksheet_scrolls() { } #[test] +/// Cell argument help is clipped to the worksheet instead of painting over the headers. fn cell_argument_hint_is_clipped_to_the_worksheet() { let ctx = Context::default(); let mut session = Session::new(); @@ -120,6 +124,7 @@ fn cell_argument_hint_is_clipped_to_the_worksheet() { } #[test] +/// An editor for a scrolled-out cell still handles Esc. fn scrolled_out_cell_editor_still_handles_escape() { let ctx = Context::default(); let mut session = Session::new(); @@ -149,6 +154,7 @@ fn scrolled_out_cell_editor_still_handles_escape() { } #[test] +/// The editor and its helper clip below the frozen top row, stay hidden while the cell is under it, and return on scroll-back. fn cell_editor_and_helper_stay_below_the_frozen_top_row() { let ctx = Context::default(); let mut session = Session::new(); @@ -240,6 +246,7 @@ fn frozen_column_editor_paints_in_both_panes_on_a_right_to_left_sheet() { } #[test] +/// The DATEDIF helper paints its signature and a wrapped unit description no wider than 400 points. fn argument_hint_shows_the_function_description() { // #363: inside DATEDIF( the hint lists the units the third argument takes. let ctx = Context::default(); diff --git a/crates/ui-egui/tests/it/interaction.rs b/crates/ui-egui/tests/it/interaction.rs index 21e6ca71b..a70efa782 100644 --- a/crates/ui-egui/tests/it/interaction.rs +++ b/crates/ui-egui/tests/it/interaction.rs @@ -8,6 +8,7 @@ use gridcraft_ui_egui::i18n::Language; use gridcraft_ui_egui::{SheetApp, grid::Geo}; use serde_json::json; +/// Builds the standard interaction harness around the caller's session with four warm-up passes. fn harness(session: Session) -> egui_kittest::Harness<'static, SheetApp> { crate::support::harness(session, 4) } @@ -201,6 +202,7 @@ fn space_toggles_the_active_checkbox_without_clicking_the_hovered_cell() { } #[test] +/// Name Box Enter selects A1; the next grid Enter moves down to A2. fn name_box_enter_goes_to_the_typed_cell_not_the_one_below() { // #247: typing A1 in the Name Box and pressing Enter selected A2; the grid replayed the Enter. let mut h = harness(blank()); @@ -234,6 +236,7 @@ fn name_box_shows_a_name_defined_on_a_sheet_with_an_apostrophe() { } #[test] +/// Typing and Enter commit 42 in A1; `=A1*2` and Tab commit 84 in A2 and move to B2, with accessibility checks. fn typing_enters_values_and_moves_down() { let mut h = harness(blank()); text(&mut h, "42"); @@ -405,6 +408,7 @@ fn insert_column_or_bar_menu_inserts_a_hundred_percent_stacked_bar() { } #[test] +/// Every main ribbon tab and dark mode render with accessibility checks. fn renders_every_ribbon_tab_without_panicking() { let mut s = Session::new(); s.execute("file.new", json!({"sample": "sales"})).unwrap(); @@ -420,6 +424,7 @@ fn renders_every_ribbon_tab_without_panicking() { } #[test] +/// Language commands are listed and every available locale renders and persists; CJK locales are omitted when craft fonts are absent. fn language_switch_renders_the_ribbon_and_persists() { let mut h = harness(blank()); // English by default whatever the host's locale: only the desktop app reads the system language. @@ -454,6 +459,7 @@ fn language_switch_renders_the_ribbon_and_persists() { } #[test] +/// The PivotTable Fields pane and its field menus switch to Spanish. fn pivot_fields_pane_follows_the_interface_language() { // #858: the PivotTable Fields pane and its field menus stayed in English after switching to Spanish. let mut h = harness(blank()); @@ -588,6 +594,7 @@ fn recommended_charts_dialog_inserts_the_chosen_chart() { } #[test] +/// Selecting 100% Stacked Area in the All Charts list inserts that chart type. fn all_charts_inserts_a_hundred_percent_stacked_area() { let mut s = blank(); s.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", 10, 30], ["Q2", 30, 10]]})).unwrap(); @@ -655,6 +662,7 @@ fn select_data_dialog_sets_the_category_axis_labels() { } #[test] +/// Each listed dialog opens, passes accessibility and closes. fn dialogs_open_and_close() { let mut h = harness(blank()); for name in [ diff --git a/crates/ui-egui/tests/it/interface_scale.rs b/crates/ui-egui/tests/it/interface_scale.rs index 1ecd36097..44991bbec 100644 --- a/crates/ui-egui/tests/it/interface_scale.rs +++ b/crates/ui-egui/tests/it/interface_scale.rs @@ -5,6 +5,7 @@ use gridcraft_ui_egui::{SheetApp, UiState}; use serde_json::json; #[test] +/// Interface size applies and persists, clamps to 75-200, accepts `auto`, and rejects missing or non-numeric values. fn interface_size_applies_persists_and_clamps() { let mut session = Session::new(); session.new_workbook(); diff --git a/crates/ui-egui/tests/it/keyboard_shortcuts.rs b/crates/ui-egui/tests/it/keyboard_shortcuts.rs index fe0cef562..8d3de6fcb 100644 --- a/crates/ui-egui/tests/it/keyboard_shortcuts.rs +++ b/crates/ui-egui/tests/it/keyboard_shortcuts.rs @@ -142,6 +142,7 @@ fn space_shortcuts_per_platform() { } #[test] +/// Alt+Down opens the active header's filter menu in LTR and RTL without editing the workbook; typing goes to the search box and Esc dismisses it. fn alt_down_opens_autofilter_headers_without_changing_the_workbook() { for rtl in [false, true] { for cell in ["A1", "B1"] { diff --git a/crates/ui-egui/tests/it/more_colors.rs b/crates/ui-egui/tests/it/more_colors.rs index 54675a5ba..b4e56301b 100644 --- a/crates/ui-egui/tests/it/more_colors.rs +++ b/crates/ui-egui/tests/it/more_colors.rs @@ -63,6 +63,7 @@ fn more_colors_applies_the_typed_colour_and_remembers_it() { } #[test] +/// A short hex value applies no fill, and Cancel applies nothing and remembers no color. fn a_colour_that_is_not_hex_does_not_apply_and_cancel_applies_nothing() { let mut h = harness(); open_fill_palette(&mut h); diff --git a/crates/ui-egui/tests/it/native_print.rs b/crates/ui-egui/tests/it/native_print.rs index 03548d85c..542083df4 100644 --- a/crates/ui-egui/tests/it/native_print.rs +++ b/crates/ui-egui/tests/it/native_print.rs @@ -6,6 +6,7 @@ use serde_json::json; use std::{cell::RefCell, rc::Rc}; #[test] +/// Opening settings and exporting do not spool; a configured native print submits its settings, and a spool error is reported. fn native_print_uses_selected_settings_and_reports_spool_errors() { let mut session = Session::new(); session.new_workbook(); @@ -43,6 +44,7 @@ fn native_print_uses_selected_settings_and_reports_spool_errors() { } #[test] +/// Print settings stay visible and accessible in an 800x700 window; toggling Grayscale updates the settings and readiness. fn print_settings_are_visible_in_a_narrow_window() { use egui_kittest::kittest::{NodeT, Queryable}; let mut session = Session::new(); @@ -77,6 +79,7 @@ fn print_settings_are_visible_in_a_narrow_window() { } #[test] +/// Every malformed print-settings request errors and leaves the prior settings unchanged. fn invalid_print_settings_are_rejected_without_partial_changes() { let mut session = Session::new(); session.new_workbook(); @@ -102,6 +105,7 @@ fn invalid_print_settings_are_rejected_without_partial_changes() { } #[test] +/// Two fast jobs each surface the same receipt exactly once. fn repeated_fast_jobs_each_surface_an_identical_receipt() { let mut session = Session::new(); session.new_workbook(); diff --git a/crates/ui-egui/tests/it/page_setup_margins.rs b/crates/ui-egui/tests/it/page_setup_margins.rs index f808afd40..79b36f897 100644 --- a/crates/ui-egui/tests/it/page_setup_margins.rs +++ b/crates/ui-egui/tests/it/page_setup_margins.rs @@ -8,6 +8,7 @@ fn margins(app: &SheetApp) -> [f32; 6] { app.session.active().and_then(|d| d.wb.active()).map(|s| s.print.margins).unwrap_or_default() } +/// Applies six custom page margins and asserts the engine stored them. fn app_with_custom_margins() -> SheetApp { let mut session = Session::new(); session.new_workbook(); diff --git a/crates/ui-egui/tests/it/pivot_pane.rs b/crates/ui-egui/tests/it/pivot_pane.rs index 5dbf21c9c..f24cb3a4b 100644 --- a/crates/ui-egui/tests/it/pivot_pane.rs +++ b/crates/ui-egui/tests/it/pivot_pane.rs @@ -5,6 +5,7 @@ use gridcraft_engine::Session; use gridcraft_ui_egui::theme; use serde_json::json; +/// Builds a pivot report at D3 and returns the painted shapes after six frames in the caller's light or dark mode. fn render_pivot_field_buttons(dark: bool) -> Vec { let mut session = Session::new(); session.new_workbook(); diff --git a/crates/ui-egui/tests/it/ribbon_layout.rs b/crates/ui-egui/tests/it/ribbon_layout.rs index dba432924..faa51b15f 100644 --- a/crates/ui-egui/tests/it/ribbon_layout.rs +++ b/crates/ui-egui/tests/it/ribbon_layout.rs @@ -102,6 +102,7 @@ struct Clicks { } #[test] +/// The main button clicks once and the arrow twice separately; the tooltip keeps the platform shortcut, and the icon sits above the full-width label. fn big_split_button_separates_the_default_action_from_the_menu() { let mut h = crate::common::builder().with_size(vec2(300.0, 200.0)).build_ui_state( |ui, c: &mut Clicks| { diff --git a/crates/ui-egui/tests/it/scrollbar_column_fill.rs b/crates/ui-egui/tests/it/scrollbar_column_fill.rs index ba3b4eb75..acda91ec7 100644 --- a/crates/ui-egui/tests/it/scrollbar_column_fill.rs +++ b/crates/ui-egui/tests/it/scrollbar_column_fill.rs @@ -7,6 +7,7 @@ use gridcraft_ui_egui::{SheetApp, theme::Tokens}; use serde_json::json; #[test] +/// Dark mode paints the tab-bar fill over the strip beside the final column heading. fn dark_theme_fills_the_strip_right_of_the_last_column_heading() { let mut session = Session::new(); session.new_workbook(); diff --git a/crates/ui-egui/tests/it/sheet_tabs.rs b/crates/ui-egui/tests/it/sheet_tabs.rs index da98331ff..2d9c6f0e9 100644 --- a/crates/ui-egui/tests/it/sheet_tabs.rs +++ b/crates/ui-egui/tests/it/sheet_tabs.rs @@ -10,6 +10,7 @@ fn harness(width: f32, active: usize) -> Harness { harness_with(width, 30, active) } +/// Builds a tab strip with the caller's width, sheet count and active sheet, using pinned fonts and an explicit grid rect. fn harness_with(width: f32, sheets: usize, active: usize) -> Harness { let mut s = Session::new(); s.new_workbook(); @@ -85,6 +86,7 @@ fn navigation_arrows_reveal_the_selected_tab() { } #[test] +/// Horizontal wheel/trackpad browsing leaves the workbook unchanged, does not snap back on idle frames, and activating a sheet still reveals its tab. fn manual_wheel_and_trackpad_browsing_does_not_change_selection_or_snap_back() { for delta in [vec2(0.0, -350.0), vec2(-350.0, 0.0)] { let mut h = harness(700.0, 0); diff --git a/crates/ui-egui/tests/it/symbol_glyphs.rs b/crates/ui-egui/tests/it/symbol_glyphs.rs index dfea9eb6a..4fb77e0b9 100644 --- a/crates/ui-egui/tests/it/symbol_glyphs.rs +++ b/crates/ui-egui/tests/it/symbol_glyphs.rs @@ -45,16 +45,19 @@ fn themed() -> egui::Context { ctx } +/// Returns the characters in `symbols` that lack a glyph in the given font family. fn missing(ctx: &egui::Context, symbols: &str, family: FontFamily) -> String { let font = FontId::new(14.0, family); symbols.chars().filter(|&c| !ctx.fonts_mut(|f| f.has_glyph(&font, c))).collect() } +/// Returns the proportional and cell font families checked by the symbol-glyph tests. fn families() -> [FontFamily; 2] { [FontFamily::Proportional, FontFamily::Name(theme::CELL.into())] } #[test] +/// Every required bundled interface symbol resolves in both checked font families. fn bundled_required_ui_symbols_resolve_to_real_glyphs() { let ctx = themed(); for family in families() { @@ -69,6 +72,7 @@ fn bundled_required_ui_symbols_resolve_to_real_glyphs() { } #[test] +/// The remaining bundled interface symbols resolve in both checked font families. fn bundled_symbols_cover_the_rest_of_the_interface() { let ctx = themed(); for family in families() { @@ -78,6 +82,7 @@ fn bundled_symbols_cover_the_rest_of_the_interface() { } #[test] +/// U+10FFFD is the only tofu probe and does not resolve in either family. fn unknown_codepoint_is_the_tofu_probe_not_a_symbol() { // A private-use code point no font covers. This is the only place that relies on the // replacement box: real-glyph checks above use `has_glyph`, because comparing a glyph's @@ -90,6 +95,7 @@ fn unknown_codepoint_is_the_tofu_probe_not_a_symbol() { } #[test] +/// `✕`/`✓` map to code icons and `⌃` falls back to `^`; the semantic `×` and `^` resolve. fn chrome_symbols_without_a_bundled_glyph_have_code_drawn_or_semantic_fallbacks() { // No bundled face has ✕, ✓ or ⌃. The buttons that used to paint ✕/✓ as text now draw the // original code icons Icon::Close/Icon::Check, and the one Control-key tooltip falls back to @@ -105,6 +111,7 @@ fn chrome_symbols_without_a_bundled_glyph_have_code_drawn_or_semantic_fallbacks( } #[test] +/// Without craft fonts it prints the optional `✓`/`⌃` gaps as a diagnostic; the strict bundled checks still run separately. fn no_font_mode_documents_the_optional_symbol_gaps() { if theme::craft_fonts_loaded() { return; @@ -119,6 +126,7 @@ fn no_font_mode_documents_the_optional_symbol_gaps() { } #[test] +/// With craft fonts, the seven-symbol union, Korean samples and exact permitted faces are checked; a required build without them fails instead of skipping. fn extended_craft_fonts_cover_the_required_symbol_union_and_korean() { if !theme::craft_fonts_loaded() { // Optional-input diagnostic mode only. A required acceptance build must fail, not skip. @@ -174,6 +182,7 @@ fn assert_exact_permitted_faces() { } #[test] +/// Advertised script samples resolve in the craft faces; Chinese samples join only when the LXGW face was embedded, and a required build without faces fails. fn craft_fonts_cover_the_advertised_scripts() { if !theme::craft_fonts_loaded() { assert!(!theme::craft_fonts_required(), "CRAFT_FONTS_REQUIRED=1 but the build embedded no craft-fonts faces"); diff --git a/crates/ui-egui/tests/it/table_headings.rs b/crates/ui-egui/tests/it/table_headings.rs index d95710270..ec15f876e 100644 --- a/crates/ui-egui/tests/it/table_headings.rs +++ b/crates/ui-egui/tests/it/table_headings.rs @@ -127,6 +127,7 @@ fn click(h: &mut Harness, at: Pos2) { } #[test] +/// The filter button in a replaced heading opens that column's menu without changing the selection; clicking beside it still selects the column. fn the_button_in_a_replaced_heading_opens_that_columns_filter_menu() { let mut s = session("C20"); s.execute("home.hideColumns", json!({"cols": "B:B"})).unwrap(); diff --git a/crates/ui-egui/tests/it/text_box_editing.rs b/crates/ui-egui/tests/it/text_box_editing.rs index 799f30cca..b89a59e2e 100644 --- a/crates/ui-egui/tests/it/text_box_editing.rs +++ b/crates/ui-egui/tests/it/text_box_editing.rs @@ -97,6 +97,7 @@ fn assert_cell_unchanged(h: &Harness) { } #[test] +/// Dragging inside the editor selects text and does not move the box; copy/cut/paste and Enter work, and an outside click commits the multiline text. fn double_click_edits_multiline_text_and_commits_on_outside_click() { let (mut h, id) = fixture(); edit_box(&mut h, id); diff --git a/crates/ui-egui/tests/it/wheel_zoom.rs b/crates/ui-egui/tests/it/wheel_zoom.rs index 82bbd6f22..634bc68ea 100644 --- a/crates/ui-egui/tests/it/wheel_zoom.rs +++ b/crates/ui-egui/tests/it/wheel_zoom.rs @@ -7,10 +7,12 @@ use serde_json::json; type Harness = egui_kittest::Harness<'static, SheetApp>; +/// Builds a mouse-wheel event from its unit, source, phase, delta and modifiers. fn wheel_event(unit: MouseWheelUnit, source: MouseWheelSource, phase: TouchPhase, delta: egui::Vec2, modifiers: Modifiers) -> Event { Event::MouseWheel { unit, delta, phase, modifiers, source } } +/// Builds a blank stepped harness and leaves the pointer centered over the grid. fn harness() -> Harness { let mut session = Session::new(); session.new_workbook(); @@ -222,6 +224,7 @@ fn gesture(h: &mut Harness, source: MouseWheelSource, delta: f32, frames: usize) } #[test] +/// A 40-point notch follows the same monotonic ladder and reaches 120%. fn point_units_use_the_same_zoom_ladder_as_lines() { let mut h = animated(); // egui converts a line to 40 points (native); the same physical notch must reach 120%. @@ -231,6 +234,7 @@ fn point_units_use_the_same_zoom_ladder_as_lines() { } #[test] +/// Trackpad and momentum sources follow the same monotonic ladder to 120%. fn trackpad_and_momentum_sources_keep_the_notch_ladder() { for source in [MouseWheelSource::Trackpad, MouseWheelSource::Momentum] { let mut h = animated(); @@ -241,6 +245,7 @@ fn trackpad_and_momentum_sources_keep_the_notch_ladder() { } #[test] +/// A missing touch End is released by the idle timeout, so the next gesture still reaches 140%. fn a_trackpad_gesture_without_an_end_still_steps_on_after_the_idle_timeout() { let mut h = animated(); let first = gesture(&mut h, MouseWheelSource::Trackpad, 40.0, 40); @@ -252,6 +257,7 @@ fn a_trackpad_gesture_without_an_end_still_steps_on_after_the_idle_timeout() { } #[test] +/// A page-unit scroll moves roughly a viewport, while 40 points scroll less than a page. fn page_and_point_units_scroll_the_grid_by_their_converted_distance() { let mut h = harness(); let grid = h.state().grid.cells_rect.unwrap();