From 253c425e2fa9a7e1ab49088652129d9e48d3493c Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Sun, 25 Oct 2026 14:26:08 +0100 Subject: [PATCH] Document migrated UI contracts and regression coverage --- crates/keymap/src/ui/keyboard.rs | 7 ++++ crates/keymap/src/ui/review.rs | 8 ++++ crates/keymap/src/ui/view.rs | 18 ++++++++ crates/keymap/tests/support/fonts.rs | 5 +++ crates/keymap/tests/ui_dialog.rs | 4 ++ crates/keymap/tests/ui_snapshot.rs | 5 +++ crates/ui/examples/docking_native.rs | 2 + crates/ui/examples/ui_hotpaths.rs | 48 +++++++++++++++++++++ crates/ui/src/capture.rs | 23 ++++++++++ crates/ui/src/capture/tests.rs | 36 ++++++++++++++++ crates/ui/src/docking/chrome.rs | 18 ++++++++ crates/ui/src/docking/dock_tests.rs | 10 +++++ crates/ui/src/docking/floating_tests.rs | 23 ++++++++++ crates/ui/src/docking/group_tests.rs | 19 +++++++++ crates/ui/src/docking/iconic_tests.rs | 2 + crates/ui/src/docking/native_tests.rs | 9 ++++ crates/ui/src/docking/render/group.rs | 20 +++++++++ crates/ui/src/docking/render/mod.rs | 7 ++++ crates/ui/src/docking/render/native.rs | 14 +++++++ crates/ui/src/docking/tests.rs | 56 +++++++++++++++++++++++++ crates/ui/src/docking/trial_tests.rs | 10 +++++ crates/ui/src/layout/resize.rs | 8 ++++ crates/ui/src/layout/tests.rs | 2 + crates/ui/src/panes/tests.rs | 6 +++ crates/ui/src/printing/tests.rs | 4 ++ crates/ui/src/tabs/tests.rs | 2 + crates/ui/src/text.rs | 46 ++++++++++++++++++++ crates/ui/tests/docking_workflows.rs | 4 ++ 28 files changed, 416 insertions(+) diff --git a/crates/keymap/src/ui/keyboard.rs b/crates/keymap/src/ui/keyboard.rs index bf09d92f..edf8aedf 100644 --- a/crates/keymap/src/ui/keyboard.rs +++ b/crates/keymap/src/ui/keyboard.rs @@ -483,6 +483,13 @@ impl Draw<'_, '_> { } #[allow(clippy::too_many_arguments)] + /// Draw one key cap, modifier, Caps Lock or System cap at `rect`. + /// + /// `Cap::Gap` is a no-op with no hit target. A key cap is colored by its holder class, selects + /// the application-wide holder as visible title when present, and sets `clicks.key` on click; + /// it exposes the selected state and a description naming the class. Modifier caps toggle into + /// `clicks.toggle`, while Caps Lock and System caps are only painted. Sizing derives from `u`; + /// a key's hover text lists every holder label. fn cap(&mut self, ui: &mut Ui, rect: Rect, cap: Cap, u: f32, mods: Mods, index: &Index, clicks: &mut Clicks) { if matches!(cap, Cap::Gap) { return; diff --git a/crates/keymap/src/ui/review.rs b/crates/keymap/src/ui/review.rs index 010ab578..132a810d 100644 --- a/crates/keymap/src/ui/review.rs +++ b/crates/keymap/src/ui/review.rs @@ -114,6 +114,14 @@ impl Draw<'_, '_> { } } + /// Draw the import review and return the chosen action. + /// + /// The set name defaults to a translated "Imported"; it is only considered valid when no set + /// catalog exists or `set_name` accepts it, but the raw text is stored in `state.review.name` + /// even when invalid. Import is offered independently of the command count. The summary counts + /// changed commands plus unmatched, shortcutless, unreadable, approximated and truncated report + /// entries. Returns `Act::Import`, `Act::Close`, or `Act::Pick` when a suggestion is chosen; the + /// caller applies the action. fn plan_view(&mut self, ui: &mut Ui, plan: &ImportPlan, mut name: String) -> Option { let mut act = None; let report = &plan.report; diff --git a/crates/keymap/src/ui/view.rs b/crates/keymap/src/ui/view.rs index 7aa4cb87..56b65c6d 100644 --- a/crates/keymap/src/ui/view.rs +++ b/crates/keymap/src/ui/view.rs @@ -620,6 +620,15 @@ impl Draw<'_, '_> { (chips + ICON + CHIP_GAP).max(110.0).min(width * 0.4) } + /// Draw the command table and own its keyboard focus and row navigation. + /// + /// The table is a focusable non-interactive region. While focused and not recording, arrow, + /// Home/End, Enter and Delete are consumed by `table_keys`; while a shortcut cell is recording, + /// focus uses `FOCUS_LOCK` so the recording receives keys, and losing focus stops recording. + /// Rows are built from the cached lines, reveal scrolling centers the selected row when needed, + /// and the selected rendered row becomes the active accessibility descendant. Dispatched row + /// actions select, record, reset or toggle a group; group toggles keep focus, other actions + /// refocus the table. fn table(&mut self, ui: &mut Ui) { let rect = ui.max_rect(); let table_id = self.base.with("table"); @@ -828,6 +837,15 @@ impl Draw<'_, '_> { clicked.then(|| RowAction::Toggle(name.to_string())) } + /// Draw one command row and return at most one action. + /// + /// The row always senses clicks and can be selected even when the dialog is read-only; + /// `editable` additionally enables shortcut-cell hit testing and the add/reset controls. The + /// accessibility label gains "(cycles)" and "(Changed)" suffixes, and the visual row uses + /// selected/focused and RTL-mirrored styling. A recording cell for this command shows the + /// recording text; clicking a chip records that binding, clicking an empty cell records the + /// first binding, and the plus button records an appended binding. The final fallback action is + /// selecting the clicked row. fn row(&self, ui: &mut Ui, rect: Rect, r: &Row, shortcut_w: f32, focused: bool) -> Option { let editable = self.env.options.editable; let selected = self.state.selected.as_ref() == Some(&r.command); diff --git a/crates/keymap/tests/support/fonts.rs b/crates/keymap/tests/support/fonts.rs index d72b576e..a6845650 100644 --- a/crates/keymap/tests/support/fonts.rs +++ b/crates/keymap/tests/support/fonts.rs @@ -17,6 +17,10 @@ const INPUTS: [(&str, &str, &str); 2] = [ ), ]; +/// Load the pinned craft-fonts inputs once for all dialog tests. +/// +/// Requires an absolute `CRAFT_FONTS_DIR`, verifies the SHA-256 of both symbol faces, refuses any +/// installed-font fallback, and appends the two pinned faces plus egui's Hack to every family. fn definitions() -> FontDefinitions { static FONTS: OnceLock = OnceLock::new(); FONTS @@ -45,6 +49,7 @@ fn definitions() -> FontDefinitions { .clone() } +/// Install the deterministic fonts on `ctx` before its first layout pass. pub fn configure(ctx: &egui::Context) { ctx.set_fonts(definitions()); } diff --git a/crates/keymap/tests/ui_dialog.rs b/crates/keymap/tests/ui_dialog.rs index 97e6b950..cf7ceab3 100644 --- a/crates/keymap/tests/ui_dialog.rs +++ b/crates/keymap/tests/ui_dialog.rs @@ -66,6 +66,10 @@ impl Fx { } } +/// Build an egui_kittest harness around `Fx`. +/// +/// Installs and checks the pinned fonts every frame, selects window or inline dialog mode, records +/// emitted outcomes, and flags any key press still present after the dialog ran. fn harness(fx: Fx, size: Vec2) -> Harness<'static, Fx> { let mut h = Harness::builder().with_size(size).with_context_setup(test_fonts::configure).build_ui_state( |ui, fx: &mut Fx| { diff --git a/crates/keymap/tests/ui_snapshot.rs b/crates/keymap/tests/ui_snapshot.rs index 885d78aa..39113ead 100644 --- a/crates/keymap/tests/ui_snapshot.rs +++ b/crates/keymap/tests/ui_snapshot.rs @@ -189,6 +189,11 @@ const CASES: &[Case] = &[ ]; #[test] +/// Write review screenshots when `CRAFT_KEYMAP_SNAPSHOT_DIR` is set. +/// +/// For every case whose name starts with `CRAFT_KEYMAP_SNAPSHOT_ONLY`, renders light and dark at +/// 1× and 2× with the pinned fonts, optional sets and long labels, after running the case action. +/// Without the directory variable the test does nothing; these are not golden comparisons. fn dialog_screenshots() { let Some(dir) = std::env::var_os("CRAFT_KEYMAP_SNAPSHOT_DIR").map(PathBuf::from) else { return }; std::fs::create_dir_all(&dir).unwrap_or_else(|e| panic!("{}: {e}", dir.display())); diff --git a/crates/ui/examples/docking_native.rs b/crates/ui/examples/docking_native.rs index ccea71f5..94b3d19a 100644 --- a/crates/ui/examples/docking_native.rs +++ b/crates/ui/examples/docking_native.rs @@ -24,6 +24,8 @@ struct App { } impl eframe::App for App { + /// Render the dock, apply every returned action with default permissions, print floating + /// window places, and forward residual native-window events to the host. fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { let style = DockStyle::from_ui(ui); let out = DockArea::new(egui::Id::unique("dock")).show_content(ui, &self.layout, &style, &mut Panels); diff --git a/crates/ui/examples/ui_hotpaths.rs b/crates/ui/examples/ui_hotpaths.rs index 509fb519..705136b4 100644 --- a/crates/ui/examples/ui_hotpaths.rs +++ b/crates/ui/examples/ui_hotpaths.rs @@ -52,6 +52,9 @@ struct Frames { galleys_after_warmup: usize, } +/// Run all micro and dock/integration cases at 1× and 2×, gate each case, and emit the schema-3 +/// JSON report to `CRAFT_UI_PERF_OUT` (or stdout). Fails when no bundled or fixture font is +/// available. fn main() -> Result<(), Box> { let (fonts, font_names) = font_definitions()?; if font_names.is_empty() { @@ -182,6 +185,9 @@ fn toolbar_widths(count: usize) -> Vec { (0..count).map(|i| 24.0 + ((i * 13) % 48) as f32).collect() } +/// Time `op` after at least 50 warmup calls and 3 seconds, record 50 batch-mean samples (2000 +/// operations per batch for split cases, 16 otherwise), and also record diagnostic single-call +/// latencies. Returns the counts, samples and summaries without p95/p99 where noted. fn micro_entry(name: &str, params: Value, mut op: impl FnMut()) -> Value { let warmup_start = Instant::now(); let mut warmups = 0; @@ -223,6 +229,7 @@ fn micro_entry(name: &str, params: Value, mut op: impl FnMut()) -> Value { "individual_latency_note": "diagnostic only; timer overhead dominates single sub-microsecond calls"}) } +/// Sort a copy of `samples` and return count, median, p95, p99, min and max in nanoseconds. fn summarize(samples: &[u64]) -> Value { let mut sorted = samples.to_vec(); sorted.sort_unstable(); @@ -231,6 +238,8 @@ fn summarize(samples: &[u64]) -> Value { "max_ns": sorted.last().copied().unwrap_or(0)}) } +/// Nearest-rank percentile of an ascending slice. Empty input returns 0; the index is clamped to +/// the last element. fn percentile(sorted: &[u64], percent: usize) -> u64 { if sorted.is_empty() { return 0; @@ -238,6 +247,8 @@ fn percentile(sorted: &[u64], percent: usize) -> u64 { sorted[(sorted.len() * percent).div_ceil(100).saturating_sub(1).min(sorted.len() - 1)] } +/// Use egui's bundled fonts, or an empty definition set with the `CRAFT_UI_PERF_FONT` file inserted +/// first in both proportional and monospace families. Returns the used font names. fn font_definitions() -> Result<(egui::FontDefinitions, Vec), Box> { let mut fonts = egui::FontDefinitions::default(); if let Some(path) = env::var_os("CRAFT_UI_PERF_FONT") { @@ -256,6 +267,8 @@ fn font_source() -> &'static str { if env::var_os("CRAFT_UI_PERF_FONT").is_some() { "CRAFT_UI_PERF_FONT (pinned fixture file)" } else { "egui default_fonts (bundled)" } } +/// A headless context at `scale` with the given fonts, dark visuals, zero animation time and no +/// cursor blinking. fn context(scale: f32, fonts: &egui::FontDefinitions) -> Context { let ctx = Context::default(); ctx.set_pixels_per_point(scale); @@ -272,6 +285,7 @@ fn screen_rect(viewport: [f32; 2]) -> Rect { Rect::from_min_size(pos2(0.0, 0.0), vec2(viewport[0], viewport[1])) } +/// A focused 60 Hz input for `viewport` with a bounded texture side for headless frames. fn raw_input(viewport: [f32; 2], frame: usize) -> egui::RawInput { egui::RawInput { screen_rect: Some(screen_rect(viewport)), @@ -282,6 +296,9 @@ fn raw_input(viewport: [f32; 2], frame: usize) -> egui::RawInput { } } +/// Check that tab fitting partitions every index exactly once, uses finite nonnegative widths, +/// stays within `available` (plus epsilon) including the overflow control, and keeps the selected +/// tab (clamped to the last tab). fn check_tabs(natural: &[f32], selected: usize, available: f32, minimum: f32, overflow: f32) -> Result<(), Box> { let fit = tabs::fit(natural, selected, available, minimum, overflow); let mut seen = vec![false; natural.len()]; @@ -309,6 +326,8 @@ fn check_tabs(natural: &[f32], selected: usize, available: f32, minimum: f32, ov Ok(()) } +/// Check a split layout: both panes are positive and sum to the span minus the gap, do not overlap +/// or escape, `resized` stays finite, and an inverted rect is rejected. Returns the layout. fn check_split(rect: Rect, axis: SplitAxis, size: SplitSize, gap: f32, minimum: f32) -> Result> { let layout = split_rect(rect, axis, size, gap, minimum).ok_or("split_rect returned None")?; let (span, first, second) = match axis { @@ -338,6 +357,9 @@ fn check_split(rect: Rect, axis: SplitAxis, size: SplitSize, gap: f32, minimum: Ok(layout) } +/// Check that toolbar fitting is an exact prefix partition: shown widths match the natural widths, +/// overflow is the remaining suffix, a nonempty overflow reserves a positive control, and a +/// zero-width group is rejected. fn check_toolbar(natural: &[f32], available: f32, spacing: f32, overflow: f32) -> Result<(), Box> { let fit = fit_toolbar(natural, available, spacing, overflow).ok_or("fit_toolbar returned None")?; let mut expected = 0; @@ -359,10 +381,12 @@ fn check_toolbar(natural: &[f32], available: f32, spacing: f32, overflow: f32) - Ok(()) } +/// A one-panel tabs node with the panel active. fn leaf(panel: u32) -> Node { Node::Tabs { panels: vec![panel], active: 0 } } +/// A balanced recursive split fixture over `panels`, alternating the axis with `depth`. fn tree(panels: &[u32], depth: usize) -> Node { if panels.len() == 1 { return leaf(panels[0]); @@ -376,6 +400,8 @@ fn tree(panels: &[u32], depth: usize) -> Node { } } +/// The benchmark layout: with `with_floating`, a fixed 240 pt second column beside a vertical split +/// plus floating window id 1; otherwise a balanced tree over `panels`. fn dock_layout(panels: u32, with_floating: bool) -> Layout { if with_floating { Layout { @@ -402,6 +428,7 @@ fn panel_label(panel: &u32) -> String { format!("Panel {panel}") } +/// Count texture-set images, their color bytes, and freed images in `delta`. fn texture_counts(delta: &egui::TexturesDelta) -> (usize, usize, usize) { let mut images = 0; let mut bytes = 0; @@ -415,6 +442,8 @@ fn texture_counts(delta: &egui::TexturesDelta) -> (usize, usize, usize) { (images, bytes, delta.free.len()) } +/// Sum mesh vertices and indices, rejecting renderer callbacks because benchmark fixtures must not +/// produce them. fn count_mesh(primitives: &[egui::epaint::ClippedPrimitive]) -> Result<(usize, usize), Box> { let (mut vertices, mut indices) = (0, 0); for clipped in primitives { @@ -432,6 +461,8 @@ fn count_mesh(primitives: &[egui::epaint::ClippedPrimitive]) -> Result<(usize, u // Fingerprint of one frame's tessellated primitives, beyond equal vertex/index counts. Texture IDs and // clip bounds are included. This is a 64-bit FNV-1a checksum, not an exact mesh identity. Timings stop // before this evidence collection. +/// A 64-bit FNV-1a fingerprint over primitive count, clip rects, texture ids, vertex data and +/// indices. This is a diagnostic checksum, not exact mesh identity; callbacks are rejected. fn mesh_hash(primitives: &[egui::epaint::ClippedPrimitive]) -> Result> { let mut hash = 0xcbf29ce484222325u64; let mut feed = |bytes: &[u8]| { @@ -476,6 +507,10 @@ fn mesh_hash(primitives: &[egui::epaint::ClippedPrimitive]) -> Result, floating: &BTreeSet) -> Result> { for (panel, rect) in bodies { if !rect.is_positive() || !rect.min.is_finite() || !rect.max.is_finite() { @@ -545,6 +582,8 @@ fn check_bodies(screen: Rect, bodies: &BTreeMap, floating: &BTreeSet< Ok(bodies.len()) } +/// Check every interaction owned by the dock has a response with a positive hit rect inside the +/// viewport. Returns the interaction count. fn check_interactions(ctx: &Context, dock_id: Id, screen: Rect) -> Result> { let owned = ctx.data(|data| data.get_temp::>(dock_id.with("owned-interactions"))).unwrap_or_default(); if owned.is_empty() { @@ -562,6 +601,8 @@ fn check_interactions(ctx: &Context, dock_id: Id, screen: Rect) -> Result { scale: f32, } +/// Render the fixture dock through `DockArea`, optionally recording each panel body's rect on the +/// collection frame, and return the dock error. fn render_dock( ui: &mut egui::Ui, dock_id: Id, @@ -629,6 +672,8 @@ struct Scene { } impl Scene { + /// Render the dock inside an optional 180 pt left panel and central panel, capturing panel geometry + /// and preserving the first dock error; otherwise render at the available rect. fn render(&mut self, layout: &Layout, ui: &mut egui::Ui, bodies: &mut BTreeMap, collect: bool) { if self.style.is_none() { self.style = Some(DockStyle::from_ui(ui)); @@ -657,6 +702,9 @@ impl Scene { } } +/// Run one case end to end: validate the layout, measure frames, gate sample count, warmup, one +/// completed pass per frame and dock error, check body count, interactions and geometry, run the +/// hover probe, and return the case JSON. fn run_case(case: Case<'_>, fonts: &egui::FontDefinitions) -> Result> { let ctx = context(case.scale, fonts); let dock_id = Id::unique(("ui-perf", case.label)); diff --git a/crates/ui/src/capture.rs b/crates/ui/src/capture.rs index 009bac94..848469ff 100644 --- a/crates/ui/src/capture.rs +++ b/crates/ui/src/capture.rs @@ -49,6 +49,9 @@ pub enum CaptureError { } impl core::fmt::Display for CaptureError { + /// Format a human-readable rejection reason. The wording is informative only and is not an + /// API contract; the variants keep checked-overflow, malformed-image and size-cap cases + /// distinguishable. fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::DimensionsOverflow { width, height } => { @@ -136,33 +139,46 @@ impl CaptureSlot { } #[cfg(test)] + /// Nonblocking probe used by the wakeup test to prove that the slot mutex is free while the + /// repaint observer runs. fn lock_is_free(&self) -> bool { self.shared.state.try_lock().is_ok() } #[cfg(test)] + /// Count strong references to the shared slot state, proving callbacks retain none. fn state_strong_count(&self) -> usize { Arc::strong_count(&self.shared) } #[cfg(test)] + /// A weak handle to the shared state, used to inject a second completion directly when egui has + /// already consumed the public callback. fn state_weak(&self) -> Weak { Arc::downgrade(&self.shared) } } impl Drop for CaptureSlot { + /// Cancel the request as the slot goes away. A later completion upgrades no live state and + /// releases its image, a retained ready image is dropped, and no repaint is requested. Results + /// already taken by the caller are unaffected. fn drop(&mut self) { close(&self.shared); } } impl core::fmt::Debug for CaptureSlot { + /// Format only the viewport and omit the remaining fields. The retained image and error payload + /// are deliberately hidden so logging cannot hold or dump screenshot contents. fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("CaptureSlot").field("viewport", &self.shared.viewport).finish_non_exhaustive() } } +/// Lock the slot state, recovering from poisoning instead of panicking. The guard must be +/// released before entering the egui context or a repaint observer; completion and close do so +/// before waking the viewport. fn lock(state: &Mutex) -> MutexGuard<'_, SlotState> { state.lock().unwrap_or_else(PoisonError::into_inner) } @@ -208,6 +224,13 @@ fn complete(weak: &Weak, image: Arc) { } } +/// Validate `image` against the slot's per-image byte bound before publishing it. +/// +/// Zero width or height is malformed before any arithmetic; the pixel count and its byte size use +/// checked multiplication; the buffer length must equal `width * height`; and only then is the +/// byte count compared with `max_image_bytes`, rejecting a count greater than it (equality is +/// accepted). `max_image_bytes` bounds this one image, not an aggregate request budget. Returns the +/// same `Arc` without waking the viewport or requesting a repaint. fn checked_image(image: Arc, max_image_bytes: usize) -> Result, CaptureError> { let [width, height] = image.size; if width == 0 || height == 0 { diff --git a/crates/ui/src/capture/tests.rs b/crates/ui/src/capture/tests.rs index 84459913..9170125a 100644 --- a/crates/ui/src/capture/tests.rs +++ b/crates/ui/src/capture/tests.rs @@ -3,10 +3,12 @@ use std::sync::{Arc, Barrier}; use super::*; +/// Allocate a filled `Arc` fixture of `size` with every pixel set to `color`. fn image(size: [usize; 2], color: egui::Color32) -> Arc { Arc::new(ColorImage::filled(size, color)) } +/// Install a repaint observer and return the viewport requests it records. fn observe_repaints(ctx: &Context) -> Arc>> { let requests = Arc::new(Mutex::new(Vec::new())); let observed = requests.clone(); @@ -15,6 +17,7 @@ fn observe_repaints(ctx: &Context) -> Arc>> { } #[test] +/// Compile-time assertion that `CaptureSlot` and `ScreenshotCallback` are `Send + Sync`. fn slot_is_send_and_sync() { fn assert_send_sync() {} assert_send_sync::(); @@ -22,6 +25,8 @@ fn slot_is_send_and_sync() { } #[test] +/// Two independent slots for different viewports may complete in reverse order; each `take` +/// returns exactly its own image and dimensions. fn distinct_requests_complete_in_reverse_order() { let ctx = Context::default(); let first_viewport = ViewportId::from_hash_of("first-window"); @@ -44,6 +49,9 @@ fn distinct_requests_complete_in_reverse_order() { } #[test] +/// Slots keep separate results and byte caps: a 2×2 image over the first slot's 4-byte cap is +/// rejected as too large while the second stays pending, then the second accepts its own image and +/// a taken slot remains empty. fn slots_do_not_share_results_or_byte_bounds() { let ctx = Context::default(); let first_viewport = ViewportId::from_hash_of("first-slot"); @@ -64,6 +72,8 @@ fn slots_do_not_share_results_or_byte_bounds() { } #[test] +/// Cloned callbacks share one completion: the first result wins, the losing image is released, and +/// `take` yields the result only once. fn callback_clones_share_one_completion_and_take_is_single_use() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 1024); @@ -82,6 +92,8 @@ fn callback_clones_share_one_completion_and_take_is_single_use() { } #[test] +/// After `take`, both a duplicate callback and a direct completion release their images without +/// storing a result or requesting a repaint. fn duplicate_completion_after_take_neither_stores_nor_wakes() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -110,6 +122,7 @@ fn duplicate_completion_after_take_neither_stores_nor_wakes() { } #[test] +/// An early `take` returns `None` without closing the request; a later completion still delivers. fn take_before_completion_is_empty_and_keeps_the_request_open() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 1024); @@ -120,6 +133,8 @@ fn take_before_completion_is_empty_and_keeps_the_request_open() { } #[test] +/// Cancelling before completion releases a late image, leaves `take` empty, and suppresses the +/// repaint wakeup. fn cancel_releases_late_images_without_waking() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -138,6 +153,8 @@ fn cancel_releases_late_images_without_waking() { } #[test] +/// A ready slot retains its image and has woken once; `cancel` releases the image and does not wake +/// again. fn cancel_after_ready_releases_the_image() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -157,6 +174,7 @@ fn cancel_after_ready_releases_the_image() { } #[test] +/// Dropping the slot releases an image that is only retained by the completed slot. fn dropping_the_slot_releases_a_completed_image() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 1024); @@ -171,6 +189,7 @@ fn dropping_the_slot_releases_a_completed_image() { } #[test] +/// A completion after the slot is dropped releases the image and does not wake the viewport. fn late_completion_after_drop_is_inert() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -188,6 +207,8 @@ fn late_completion_after_drop_is_inert() { } #[test] +/// Callbacks hold only a weak reference: the slot state has one strong owner, dropping the slot +/// releases the context and state, and a later completion is inert. fn callback_retains_only_a_weak_reference_to_live_state() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -208,6 +229,8 @@ fn callback_retains_only_a_weak_reference_to_live_state() { } #[test] +/// A completion distinguishes the per-image size cap, a malformed pixel buffer, and overflow of +/// the pixel or byte count; a returned error is taken at most once. fn rejects_oversized_malformed_and_overflowing_images() { let ctx = Context::default(); @@ -232,6 +255,8 @@ fn rejects_oversized_malformed_and_overflowing_images() { } #[test] +/// Oversized and malformed images are dropped immediately and each rejection still wakes the error +/// consumer exactly once before `take` reports it. fn rejected_oversized_and_malformed_images_are_released_before_take() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -254,6 +279,8 @@ fn rejected_oversized_and_malformed_images_are_released_before_take() { } #[test] +/// Any zero width or height is rejected as malformed before overflow or size-cap decisions, even +/// for extreme extents such as `usize::MAX × 0`. fn zero_area_images_are_rejected_even_with_extreme_extents() { let ctx = Context::default(); @@ -270,6 +297,7 @@ fn zero_area_images_are_rejected_even_with_extreme_extents() { } #[test] +/// A 2×2 image at exactly the 16-byte cap is accepted: the bound is inclusive. fn accepts_an_image_exactly_at_the_byte_bound() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 16); @@ -279,6 +307,8 @@ fn accepts_an_image_exactly_at_the_byte_bound() { } #[test] +/// A worker thread completes a child-window slot; every recorded wake belongs to that slot's +/// viewport and the expected image is delivered. fn another_thread_completes_and_wakes_only_the_owning_viewport() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -298,6 +328,8 @@ fn another_thread_completes_and_wakes_only_the_owning_viewport() { } #[test] +/// The repaint observer can `take` the image re-entrantly: `try_lock` succeeds during the wakeup, +/// proving the slot lock is released before `request_repaint_of`. fn wakeup_runs_after_the_slot_lock_is_released() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 1024); @@ -386,6 +418,7 @@ fn complete_cancel_and_take_race_without_retaining_or_double_waking() { } #[test] +/// A zero byte budget rejects any nonempty image as too large and wakes once. fn zero_budget_rejects_a_nonempty_image_and_wakes() { let ctx = Context::default(); let requests = observe_repaints(&ctx); @@ -396,6 +429,7 @@ fn zero_budget_rejects_a_nonempty_image_and_wakes() { } #[test] +/// Two threads racing on `take` produce exactly one result, and a subsequent take is empty. fn concurrent_takers_receive_the_result_exactly_once() { let ctx = Context::default(); let (slot, callback) = CaptureSlot::new(&ctx, ViewportId::ROOT, 4); @@ -422,6 +456,8 @@ fn concurrent_takers_receive_the_result_exactly_once() { } #[test] +/// A renderer thread can complete a capture while a UI closure is blocked; the image is delivered +/// and the owning child viewport is woken exactly once. fn renderer_completion_during_a_live_ui_pass_can_wake_and_deliver() { let ctx = Context::default(); let requests = observe_repaints(&ctx); diff --git a/crates/ui/src/docking/chrome.rs b/crates/ui/src/docking/chrome.rs index 468d8591..6ac7466b 100644 --- a/crates/ui/src/docking/chrome.rs +++ b/crates/ui/src/docking/chrome.rs @@ -175,6 +175,15 @@ impl<'a> FloatingFrame<'a> { shown.inner } + /// Draw the resize handles around `rect` and return `(resized, started, responses)`. + /// + /// `resized` is set only for an enabled, uncancelled handle whose clamped next rect differs + /// from the current one. `started` names the handle whose drag began, which can remain set after + /// Escape or focus loss cancels that drag. Keyboard arrow steps move an edge by 8 pt; pointer + /// drags derive the next rect from the cursor relative to the press origin. Limits clamp the + /// proposal. Stored pointer anchors are removed on cancellation or once the primary button is + /// released. Handles are accessibility buttons; the diagonal one is painted while hovered, + /// dragged or focused. fn handles(&self, ui: &mut Ui, rect: Rect, bounds: Rect, style: &ChromeStyle) -> (Option, Option, Vec) { let mut resized = None; let mut started = None; @@ -345,6 +354,15 @@ impl<'a> TitleBar<'a> { self } + /// Draw the title bar into `rect` and return its responses. + /// + /// `close_at` rects are in logical points, already mirrored in a right-to-left layout; without it + /// the × is a square the bar's height centered 10 pt in from the trailing edge. Menu and close + /// buttons reserve space on the trailing side, the draggable bar excludes them, and a title is + /// truncated to the bar's remaining width. The bar senses clicks and drags, while double-click + /// behavior (for example minimizing or closing) is the caller's. A right-to-left style mirrors + /// the layout; `move_label` remains the accessible name of the drag surface. The bar's own id + /// is used, with `"menu"` and `"close"` child ids for the buttons. pub fn show(self, ui: &mut Ui, rect: Rect, style: &ChromeStyle) -> TitleResponse { let painter = ui.painter().with_clip_rect(rect.intersect(ui.clip_rect())); painter.rect_filled(rect, 0.0, style.title_background); diff --git a/crates/ui/src/docking/dock_tests.rs b/crates/ui/src/docking/dock_tests.rs index 1aac1eb1..7abdce16 100644 --- a/crates/ui/src/docking/dock_tests.rs +++ b/crates/ui/src/docking/dock_tests.rs @@ -108,6 +108,9 @@ impl Driver { driver } + /// One 800×600 frame with injected events and the driver's focus/right-to-left settings. + /// Renders the dock in a 600×600 scope, clears texture deltas, records shapes, and returns + /// `DockOutput`. fn frame(&mut self, events: Vec) -> DockOutput { let mut output = DockOutput::default(); self.time += 1.0 / 60.0; @@ -432,6 +435,8 @@ fn a_column_takes_groups_anywhere_in_it_and_as_tabs() { } #[test] +/// A column of five groups with a 160 pt minimum is taller than the window; scrolling up over its +/// tab strip moves the last tab more than 100 pt into view. fn a_column_too_tall_for_the_window_scrolls() { let groups: [&[u32]; 5] = [&[2], &[3], &[4], &[5], &[6]]; let mut d = Driver::with(docked(Side::Start, &[&groups]), TestContent { min_height: 160.0 }, false); @@ -483,6 +488,8 @@ fn column_bar_drag_moves_the_column_and_away_from_a_dock_floats_its_groups() { } #[test] +/// Escape, focus loss, pointer exit and `cancel_drag` each end a held column drag without emitting +/// actions or a group drop; the release changes nothing and the layout equals the original. fn column_drag_escape_focus_loss_pointer_exit_and_cancel_drag_leave_the_layout_unchanged() { let layout = docked(Side::Start, &[&[&[2], &[3]], &[&[5]]]); for case in ["escape", "focus", "exit", "cancel"] { @@ -635,6 +642,9 @@ fn a_column_s_first_tab_dropped_on_its_own_column_edge_anchors_on_another_panel( } #[test] +/// A driver that never reported window focus can still drag tab 3 to a column edge (moving it after +/// panel 4). When focus arrives and is then lost mid-drag, releasing emits no action and restores the +/// original layout. fn a_window_that_never_reported_focus_drags_tabs_and_losing_focus_cancels_a_drag() { let mut layout = docked(Side::Start, &[&[&[2]], &[&[3, 4]]]); if let Some(Node::Split { size, .. }) = &mut layout.root { diff --git a/crates/ui/src/docking/floating_tests.rs b/crates/ui/src/docking/floating_tests.rs index f4535ca4..3137ccbb 100644 --- a/crates/ui/src/docking/floating_tests.rs +++ b/crates/ui/src/docking/floating_tests.rs @@ -125,6 +125,8 @@ impl Driver { driver } + /// One 800×600 frame with an inserted modifier event, optional split-tree style/native + /// windows/maximized frame/hidden panels. Clears texture deltas and returns `DockOutput`. fn frame(&mut self, mut events: Vec) -> DockOutput { let mut output = DockOutput::default(); events.insert(0, Event::ModifiersChanged(self.modifiers)); @@ -286,6 +288,8 @@ fn title_bar_drag_onto_dock_targets_docks_the_window() { } #[test] +/// A group dragged to another floating window's top band splits that window like a frame; the gap +/// between groups is not a target, and the window divider resizes both sides. fn a_floating_window_is_a_tree_whose_groups_split_and_resize_like_frames() { // A group dragged by its bar to another window's top band splits that window's group. let mut d = Driver::new(workspace(), false); @@ -315,6 +319,8 @@ fn a_floating_window_is_a_tree_whose_groups_split_and_resize_like_frames() { } #[test] +/// Double-clicking the window title toggles minimization of the whole window; the close box sits +/// 10 pt in from the trailing edge and emits `CloseWindow`, removing every panel in the window. fn title_bar_double_click_minimizes_every_group_and_close_box_closes_the_window() { let mut d = Driver::new(workspace(), false); let title = d.title(5).center(); @@ -404,6 +410,9 @@ fn the_split_tree_outer_edge_band_is_8_pt() { } #[test] +/// Maximizing a frame makes it start at the area edge; another frame's tab is not rendered and the +/// hidden floating window's move-group response is absent, so dragging its title emits no move. A +/// floating panel cannot be maximized. fn maximized_frame_fills_the_area_and_other_frames_do_not_render_or_take_input() { let mut layout = frames(); layout.floating.push(Floating::single(vec![5], 0, [400.0, 300.0, 200.0, 150.0])); @@ -427,6 +436,8 @@ fn maximized_frame_fills_the_area_and_other_frames_do_not_render_or_take_input() } #[test] +/// The tab context menu exposes the expected group actions without Minimize; Close Other emits +/// `CloseOthers` leaving only the chosen tab, and Maximize Frame emits `ToggleMaximize`. fn close_other_panels_keeps_only_the_chosen_tab() { use egui_kittest::{Harness, kittest::Queryable}; type State = (Content, Vec>, Vec>); @@ -495,6 +506,8 @@ fn hide_panels_keeps_protected_panels_and_the_except_set() { } #[test] +/// A protected canvas that accepts splits is still kept when every panel is hidden, and draws +/// neither a tab nor a drag bar. fn a_canvas_that_accepts_splits_is_still_protected() { let mut d = Driver::new(workspace(), false); d.content.beside = true; @@ -522,6 +535,8 @@ fn native_window_falls_back_in_window_when_viewports_are_embedded() { #[cfg(feature = "native-windows")] #[test] +/// With real viewports, a close event from a native window's backend produces `CloseWindow` and +/// removes all of that window's panels. fn native_window_close_closes_its_panels() { use std::{cell::Cell, rc::Rc}; let close = Rc::new(Cell::new(false)); @@ -564,6 +579,8 @@ fn native_window_close_closes_its_panels() { } #[test] +/// `FloatingFrame` and `TitleBar` work without a dock area: close and menu clicks register, the bar +/// drag reports its delta, and an edge drag resizes the frame. fn chrome_frame_and_title_bar_work_without_a_dock_area() { use chrome::{ChromeStyle, FloatingFrame, TitleBar}; let ctx = Context::default(); @@ -612,6 +629,8 @@ fn chrome_frame_and_title_bar_work_without_a_dock_area() { } #[test] +/// Hidden panels give up their split space; the remaining split hit is named by its layout path and +/// dragging it emits `ResizeSplit` for exactly that path while the nested ratio changes. fn hidden_panels_leave_their_split_and_resizing_names_the_layout_path() { let layout = Layout { root: Some(Node::Split { @@ -661,6 +680,8 @@ fn a_hidden_floating_window_does_not_cover_dock_targets() { } #[test] +/// Each top resize edge sits directly above the title bar and keeps the opposite edges while +/// dragging; the title bar still moves the window, and a focused top edge responds to arrow keys. fn top_edges_resize_from_the_band_above_the_title_bar() { // W2 holds [8] at [560, 300, 200, 200]; each top handle keeps the opposite edges. let area = Id::unique(AREA); @@ -686,6 +707,8 @@ fn top_edges_resize_from_the_band_above_the_title_bar() { } #[test] +/// Undock Panel Group from one group of a stacked floating window emits `MoveGroup` to a float and +/// separates exactly that group; a window with only one group has no undock entry. fn undock_panel_group_floats_one_group_of_a_floating_window() { use egui_kittest::{Harness, kittest::Queryable}; let stacked = || Layout { diff --git a/crates/ui/src/docking/group_tests.rs b/crates/ui/src/docking/group_tests.rs index 3939c86f..ef246549 100644 --- a/crates/ui/src/docking/group_tests.rs +++ b/crates/ui/src/docking/group_tests.rs @@ -132,6 +132,8 @@ impl Driver { driver } + /// One 800×600 frame with inserted modifier events and optional transparent/tweaked style or + /// custom area. Captures shapes, clears texture deltas, and returns `DockOutput`. fn frame(&mut self, mut events: Vec) -> DockOutput { let mut output = DockOutput::default(); events.insert(0, Event::ModifiersChanged(self.modifiers)); @@ -255,6 +257,9 @@ fn docked_two_tab_group_drags_by_its_bar_and_keeps_order_and_active_tab() { } #[test] +/// In a 300 pt strip with eight tabs and a 24 pt header, the drag bar keeps at least 40 pt and ends +/// at the header; tabs shrink and overflow before it is narrowed, and dragging it floats all eight +/// panels as one group. fn drag_bar_survives_a_strip_full_of_tabs() { let layout = Layout { root: Some(tabs(&[1, 2, 3, 4, 5, 6, 7, 8], 0)), floating: Vec::new() }; let content = TestContent { header: 24.0, ..Default::default() }; @@ -392,6 +397,8 @@ fn ctrl_or_cmd_held_at_release_always_floats() { } #[test] +/// Escape, focus loss, pointer exit and `cancel_drag` each end a held group drag with no actions or +/// drop; releasing changes nothing and the layout equals the original. fn group_drag_escape_focus_loss_pointer_exit_and_cancel_drag_leave_the_layout_unchanged() { for case in ["escape", "focus", "exit", "cancel"] { let mut d = Driver::new(frames()); @@ -547,6 +554,8 @@ fn docked_header_drag_reaches_the_host_hook_and_preserves_the_group() { } #[test] +/// A lone protected panel renders neither a tab nor a drag bar, while another group still has its +/// bar. fn a_lone_protected_panel_draws_no_strip_and_no_bar() { struct Bare; impl DockContent for Bare { @@ -569,6 +578,8 @@ fn a_lone_protected_panel_draws_no_strip_and_no_bar() { } #[test] +/// The drag bar's context menu contains Close Tab Group, Minimize, Float Tab Group and Move Group +/// To; clicking Close emits `CloseGroup`, and Enter on the focused bar opens the menu. fn drag_bar_menu_has_the_group_items_and_opens_from_the_keyboard() { use egui_kittest::{Harness, kittest::Queryable}; let mut h = Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state( @@ -617,6 +628,9 @@ fn a_group_dragged_out_of_a_floating_stack_tears_out_alone() { } #[test] +/// A docked panel floats at the title bar plus strip plus recorded content height, with a 120 pt +/// minimum and the 320 pt docked cap; a host floating size supplies the width and caps the height, +/// and a group uses its active panel's content height. fn a_short_docked_panel_floats_at_its_content_height_up_to_its_floating_size() { let title = crate::docking::chrome::TITLE_HEIGHT; let float = |body: f32, floating_size: Option| { @@ -678,6 +692,8 @@ fn a_panel_that_fills_its_frame_floats_at_its_floating_size_or_320() { } #[test] +/// Keyboard focus on a tab is kept across a user drag, but a layout reset that moves the tab clears +/// it and reports no new focus; closing the panel also gives focus up. fn tab_focus_follows_only_the_users_input() { let mut d = Driver::new(docked()); let tab = Id::unique(AREA).with(("tab", &4u32)); @@ -715,6 +731,9 @@ fn tab_focus_follows_only_the_users_input() { } #[test] +/// Wheel selection is off by default. With the floating frame style, wheel up selects the previous +/// tab and stops at the first, trackpad points accumulate into a notch, huge deltas clamp at both +/// ends, and middle-click closes the tab. fn frame_tabs_follow_the_wheel_and_close_on_a_middle_click() { let second = |d: &Driver| match &d.layout.root { Some(Node::Split { second, .. }) => (**second).clone(), diff --git a/crates/ui/src/docking/iconic_tests.rs b/crates/ui/src/docking/iconic_tests.rs index 56c29620..68644d5c 100644 --- a/crates/ui/src/docking/iconic_tests.rs +++ b/crates/ui/src/docking/iconic_tests.rs @@ -160,6 +160,8 @@ fn icon_click_opens_a_flyout_on_the_canvas_side_and_auto_collapse_closes_it() { } #[test] +/// Choosing Auto-Collapse Iconic Panels from the iconic column's menu emits a +/// `DockEvent::ToggleAutoCollapse` event rather than mutating the layout directly. fn auto_collapse_toggle_is_a_column_menu_event() { use egui_kittest::{Harness, kittest::Queryable}; let layout = docked(&[&[1, 2]], true); diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index ce4ba70c..5f02b748 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -75,6 +75,9 @@ enum Marks { } impl DockContent for Content { + /// Test body that probes native input: records events/raw events, text focus and typing, button + /// focus, a click counter, drag source state, drop sites and payloads, file hover/drop, and + /// primary/secondary presses inside the body. fn body(&mut self, ui: &mut egui::Ui, panel: &u32) { ui.label(format!("panel {panel}")); if self.input_probe == Some(*panel) { @@ -1518,6 +1521,9 @@ fn resize_bands_are_a_few_points_deep_and_leave_out_the_bar_buttons() { } #[test] +/// On macOS the close glyph sits 8 pt from the trailing corner in both directions; its hit area +/// extends 10 pt away from the corner, reaches the bar bottom, leaves the drag area clear, and stays +/// reachable outside AppKit's corner resize region. fn on_macos_the_close_glyph_sits_by_the_corner_and_its_hit_area_reaches_away_from_it() { use crate::docking::chrome::{ChromeStyle, TitleBar}; use crate::docking::placement::BAR_HEIGHT; @@ -1672,6 +1678,9 @@ fn a_native_window_takes_no_input_while_the_app_blocks_it() { } #[test] +/// A blocked native window rejects typing but preserves native control-event order: IME reset +/// completion and focus loss reach both event and raw-event readers, while a shortcut release is +/// still forwarded to the host. fn a_blocked_native_window_preserves_ordered_ime_reset_completions() { let mut d = blocked_driver(); let id = id_of(&d); diff --git a/crates/ui/src/docking/render/group.rs b/crates/ui/src/docking/render/group.rs index 8f737078..c5f26e89 100644 --- a/crates/ui/src/docking/render/group.rs +++ b/crates/ui/src/docking/render/group.rs @@ -36,6 +36,15 @@ const OVERFLOW_WIDTH: f32 = 22.0; const WHEEL_NOTCH: f32 = 40.0; impl Renderer<'_, P> { + /// Draw one tab group at `rect`. + /// + /// An `active` index outside `panels` is a no-op. A lone protected panel is drawn stripless: + /// its whole rect is the body and its only tab has `Rect::NOTHING`. Side splits are offered + /// only for a `Frame` or `Floating` group whose selected panel accepts splits. Tab labels + /// shrink widest-first to the minimum width, then the remaining tabs move to the overflow menu; + /// hidden overflow tabs still appear in the drop target with `Rect::NOTHING`. `minimized` hides + /// the body but keeps the strip. Tabs, strip, header and drag bar are mirrored in right-to-left + /// layout. pub(super) fn group(&mut self, ui: &mut Ui, panels: &[P], active: usize, minimized: bool, rect: Rect, kind: GroupKind) { let Some(selected) = panels.get(active) else { return }; self.record_float(panels, rect.size(), rect.size()); @@ -329,6 +338,9 @@ impl Renderer<'_, P> { anchors } + /// A stock menu button for `panel` whose visible text comes from + /// [`crate::docking::DockContent::label`] while its accessible name stays the logical `label_of` + /// value. The function only returns the response; the caller performs the menu action. pub(super) fn menu_label(&mut self, ui: &mut Ui, panel: &P) -> egui::Response { let logical = self.label_of(panel); let text = self.content.label(ui, panel, &logical, &self.style.font, self.style.text); @@ -354,6 +366,14 @@ impl Renderer<'_, P> { ui.ctx().data(|d| d.get_temp::(key)).unwrap_or(false) } + /// Draw one tab inside `rect` and return its response. + /// + /// Clicking activates and focuses the panel. In a minimized group the first click expands and + /// records the time so that same click is not also interpreted as a double-click; a genuine + /// double-click toggles minimized only when the owner minimizes. Middle-click closes only when + /// the style enables it and the panel is closable. The context menu emits `Action`s, with float, + /// move and close entries gated by the panel's permissions. `owner: None` removes minimized + /// semantics and undock latching. An elided label exposes its full text as hover text. pub(super) fn tab(&mut self, ui: &mut Ui, panel: &P, rect: Rect, active: bool, group: Rect, owner: Option<&TabGroup<'_, P>>) { let minimized = owner.filter(|owner| self.minimizes(owner)).map(|owner| owner.minimized); let policy = self.permissions(panel); diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index 9623f19b..273e15bb 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -396,6 +396,13 @@ impl Renderer<'_, P> { } } + /// Draw a panel body into `rect`. + /// + /// An unusable rect returns without painting. Otherwise the body runs in a clipped child scope, + /// and its minimum height is recorded against `rect.height()` for floating-size policy. Any + /// pointer press inside the body focuses the panel, including secondary and middle presses; with + /// native windows that press origin is stored for body-drag routing and the panel is recorded as + /// the pointed body. The panel's content map keeps the sizing entry for later frames. pub(super) fn contents(&mut self, ui: &mut Ui, panel: &P, rect: Rect) { if !crate::docking::targets::usable(rect) { return; diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index a2e1ce61..82c788a9 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -836,6 +836,11 @@ pub(super) fn body_input(ctx: &Context) -> Arc>>(body_input_key()).clone()) } +/// The temp-data key for the context's native body-routing state. +/// +/// All dock areas in a context deliberately use this one container: its `routes` map is keyed by +/// dock-area `Id`, and queued and held events by target viewport. The key is unique only so this +/// state does not collide with unrelated temporary data. fn body_input_key() -> Id { Id::unique("craft-ui docking native body input") } @@ -847,6 +852,15 @@ impl egui::Plugin for NativeInput { "craft-ui docking native input" } + /// Route this viewport's input without deadlocking. + /// + /// Only the root viewport advances the frame pass; queued and held entries older than the + /// current pass are pruned. Events waiting for this viewport are taken before routing, so events + /// enqueued by routing wait for its next input. A held release is only released once a later + /// root pass exists. Earlier routed events are spliced before the window's own events, and any + /// accepted route requests a root repaint. The dock area's application hook is called only + /// after the routing mutex is dropped because it may cancel a drag and re-enter the shared + /// body state. fn input_hook(&mut self, ctx: &Context, input: &mut RawInput) { let viewport = input.viewport_id; let body = body_input(ctx); diff --git a/crates/ui/src/docking/tests.rs b/crates/ui/src/docking/tests.rs index e2032395..12ffe2fb 100644 --- a/crates/ui/src/docking/tests.rs +++ b/crates/ui/src/docking/tests.rs @@ -487,6 +487,8 @@ fn old_hidden_locations_accept_panel_ids_without_a_default_identity() { assert_eq!(location.anchor, Some(Panel::Layers)); } +/// One 800×600 `DockArea::show` frame with actual body labels. Returns `DockOutput` plus the body +/// scope ids and records the final cursor icon in temp data; clears texture deltas. fn frame(ctx: &Context, layout: &Layout, events: Vec, focused: bool) -> (DockOutput, Vec) { let mut output = DockOutput::default(); let mut ids = Vec::new(); @@ -525,6 +527,8 @@ fn renderer_keeps_content_identity_when_panel_moves_into_a_float() { } #[test] +/// Escape and window focus loss each discard an in-progress drag; a subsequent release emits no +/// actions. fn cancelling_or_losing_focus_during_a_drag_does_not_move_panels() { let ctx = Context::default(); let layout = sample(); @@ -565,6 +569,8 @@ fn pointer_button(pos: egui::Pos2, pressed: bool) -> Event { } #[test] +/// Real pointer input drags a floating panel's tab into the main tab strip, emits a `Move`, removes +/// it from the float, preserves the panel count, and activates it in the target group. fn actual_pointer_drag_docks_a_floating_panel_into_the_main_tab_strip() { let ctx = Context::default(); let mut layout = sample(); @@ -610,6 +616,9 @@ fn tab_dragged_out_of_the_window_floats_where_it_left() { } #[test] +/// At wide, narrow and wide-again sizes, every rendered body rect stays horizontally inside the +/// viewport and, at the wide sizes, panel min/max limits bound its width, without mutating the +/// layout's stored preferred widths. fn panel_size_limits_clamp_geometry_without_overwriting_preferred_widths() { let layout = Layout { root: Some(Node::Split { @@ -668,6 +677,8 @@ fn panel_size_limits_clamp_geometry_without_overwriting_preferred_widths() { } #[test] +/// A focused bottom-right floating resize grip moves width and height with Right/Down arrows and +/// then loses focus to Tab normally. fn focused_floating_resize_grip_accepts_both_arrow_axes_and_releases_tab() { let ctx = Context::default(); let mut layout = sample(); @@ -687,6 +698,8 @@ fn focused_floating_resize_grip_accepts_both_arrow_axes_and_releases_tab() { } #[test] +/// A stack divider dragged by pointer grows the open body above and shrinks the adjacent open body +/// while closed entries and unrelated bodies keep their heights; keyboard arrows continue resizing. fn stack_dividers_resize_adjacent_open_bodies_by_pointer_and_keyboard() { let ctx = Context::default(); let mut layout = Layout { @@ -738,6 +751,8 @@ fn stack_dividers_resize_adjacent_open_bodies_by_pointer_and_keyboard() { } #[test] +/// A horizontal split's inner edge inside a scrollable dock remains hit-testable and draggable next +/// to a scrollbar. fn split_inner_edge_remains_draggable_beside_a_scrollbar() { let ctx = Context::default(); let mut layout = Layout { @@ -820,6 +835,7 @@ fn restoring_a_panel_beside_a_nested_subtree_preserves_the_complete_workspace() } #[test] +/// A layout with only floating windows leaves the caller's available document rect unchanged. fn floating_only_surface_does_not_allocate_the_callers_document_region() { let ctx = Context::default(); let layout = Layout { root: None, floating: vec![Floating::single(vec![1u32], 0, [30.0, 40.0, 200.0, 200.0])] }; @@ -843,6 +859,8 @@ fn floating_only_surface_does_not_allocate_the_callers_document_region() { } #[test] +/// Dragging the floating group grip emits `MoveFloating` and changes the window rect while the +/// group's panels and active panel stay the same; dragging a tab detaches only that panel. fn floating_group_grip_moves_every_member_while_tab_drag_detaches_one() { let ctx = Context::default(); let mut layout = sample(); @@ -983,6 +1001,8 @@ struct CustomContent { } #[test] +/// `cancel_drag` discards a floating group's move before the caller replaces the layout, so later +/// pointer movement and release emit nothing and the replacement stays intact. fn host_cancel_discards_group_drag_before_workspace_replacement() { let ctx = Context::default(); let mut layout = sample(); @@ -1025,6 +1045,8 @@ impl DockContent for CustomContent { fn group_header_width(&self, _: &[u32], _: &u32, _: bool, _: f32) -> f32 { 40.0 } + /// Custom header: records the call, asserts it receives at most the declared 40 pt width, and + /// draws its own glyph. fn group_header(&mut self, ui: &mut egui::Ui, _: &[u32], _: &u32, _: bool) { self.headers += 1; assert!(ui.max_rect().width() <= 40.0); @@ -1089,6 +1111,7 @@ fn custom_chrome_shaped_labels_and_programmatic_raise_are_available() { } #[test] +/// An overlay appearing mid-drag blocks the release's float fallback; later frames emit no action. fn overlay_appearing_mid_drag_blocks_release_and_float_fallback() { let ctx = Context::default(); let layout = sample(); @@ -1125,6 +1148,8 @@ fn group_move_escape_restores_original_geometry() { } #[test] +/// Custom headers and labels render at 0.75×, 1×, 1.5× and 2×: headers and bodies are produced and +/// panel 6's label is rendered with its logical text. fn custom_headers_and_labels_render_at_small_and_large_scales() { for scale in [0.75, 1.0, 1.5, 2.0] { let ctx = Context::default(); @@ -1153,6 +1178,8 @@ fn custom_headers_and_labels_render_at_small_and_large_scales() { } #[test] +/// A protected member blocks whole-group gestures and host drops: drag/drop state stays empty and +/// no `MoveFloating` action is emitted. fn protected_group_member_blocks_whole_group_gesture_and_host_drop() { let ctx = Context::default(); let layout = sample(); @@ -1228,6 +1255,8 @@ fn protected_group_member_blocks_floating_group_resize() { } #[test] +/// Chrome focus recognizes a rendered tab but rejects a focused body text field, an invalid layout, +/// an empty layout and an unrendered id. fn chrome_focus_classifies_rendered_tab_but_excludes_body_text_field_and_invalid_layout() { let ctx = Context::default(); let layout = sample(); @@ -1267,6 +1296,8 @@ fn chrome_focus_classifies_rendered_tab_but_excludes_body_text_field_and_invalid } #[test] +/// Chrome focus recognizes rendered tabs, collapse buttons, stack resizers, split dividers and +/// floating resizers. fn chrome_focus_classifies_rendered_accordion_and_split_and_floating_resizers() { let ctx = Context::default(); let area = Id::unique("focus-chrome-controls"); @@ -1304,6 +1335,9 @@ fn chrome_focus_classifies_rendered_accordion_and_split_and_floating_resizers() } #[test] +/// `cancel_drag` ends an owned split/stack/floating resize until a new press: held movement stops +/// producing actions and the layout stays at the cancellation snapshot, then a fresh press resizes +/// again. fn workspace_cancel_stops_owned_resize_controls_until_new_press() { let area = Id::unique("test-area"); let fixtures = [ @@ -1350,6 +1384,8 @@ fn workspace_cancel_stops_owned_resize_controls_until_new_press() { } #[test] +/// Cancelling one area's drag leaves another area's split divider or a body-owned drag in control, +/// and that gesture continues producing its effects. fn workspace_cancel_preserves_other_area_and_body_drag_ownership() { for body_drag in [false, true] { let ctx = Context::default(); @@ -1415,6 +1451,8 @@ fn workspace_cancel_preserves_other_area_and_body_drag_ownership() { } #[test] +/// Custom action labels paint their visual substitution while the popup buttons keep their own +/// logical accessibility names; the visual text is not exposed as an accessibility label. fn popup_action_labels_use_custom_visual_text_and_logical_accessibility() { use egui_kittest::{Harness, kittest::Queryable}; fn paints(shape: &egui::Shape, text: &str) -> bool { @@ -1465,6 +1503,9 @@ fn popup_action_labels_use_custom_visual_text_and_logical_accessibility() { } #[test] +/// At the minimum tab width, only the tabs that fit beside the drag bar and overflow control are +/// shown, the active last tab replaces the last fitting tab, and the overflow menu lists exactly the +/// hidden panels. fn a_strip_too_narrow_for_its_tabs_at_their_minimum_width_lists_the_others_in_the_overflow_menu() { use egui::accesskit::Role; use egui_kittest::{Harness, kittest::Queryable}; @@ -1499,6 +1540,8 @@ fn a_strip_too_narrow_for_its_tabs_at_their_minimum_width_lists_the_others_in_th } #[test] +/// Long tab labels are truncated with an ellipsis inside their tab and expose the full label in a +/// tooltip on hover. fn tab_labels_that_do_not_fit_are_truncated_inside_their_tab_and_named_in_a_tooltip() { let ctx = Context::default(); ctx.all_styles_mut(|style| style.interaction.tooltip_delay = 0.0); @@ -1551,6 +1594,8 @@ fn tab_labels_that_do_not_fit_are_truncated_inside_their_tab_and_named_in_a_tool // Ported from storytold/craft-libs#110 by @Zenodeon. #[test] +/// A floating resize keeps drag ownership when another host control rewrites the owned-interaction +/// list; `cancel_drag` then ends it and further movement is ignored. fn resize_cancellation_retains_ownership_across_other_host_rendering() { let ctx = Context::default(); let area = Id::unique("test-area"); @@ -1572,6 +1617,8 @@ fn resize_cancellation_retains_ownership_across_other_host_rendering() { } #[test] +/// A collapsed 26 pt floating window still resizes its width with the Right arrow under a height +/// cap, growing to the minimum height instead of ignoring the gesture. fn collapsed_floating_height_cap_does_not_disable_keyboard_width_resizing() { let ctx = Context::default(); let mut layout = Layout { root: None, floating: vec![Floating::single(vec![1u32], 0, [80.0, 70.0, 300.0, 26.0])] }; @@ -1632,6 +1679,11 @@ fn returning_to_a_singleton_or_floating_source_body_is_an_explicit_noop() { } #[test] +/// Dragging each floating resize grip with the pointer past a viewport edge leaves every asserted +/// edge unchanged when the pointer moves partway back while still outside, and returning to the +/// start point restores the original geometry. `resize-right` and `resize` assert the clamped +/// right edge and fixed left edge; `resize-left` and `resize-bottom-left` assert only their fixed +/// right edge. `resize-bottom` asserts the clamped bottom edge and fixed top edge. fn pointer_floating_resize_stops_at_edges_and_waits_for_cursor_to_return() { for name in ["resize-right", "resize-left", "resize-bottom", "resize", "resize-bottom-left"] { let ctx = Context::default(); @@ -1700,6 +1752,8 @@ fn unchanged_tab_slots_are_explicit_noops_with_bounded_insertion_markers() { } #[test] +/// A host drag handler that accepts the release as a no-op consumes it, so no fallback float is +/// emitted and the host observes one release. fn host_owned_noop_consumes_release_without_a_fallback_float() { struct Content(usize); impl DockContent for Content { @@ -1810,6 +1864,7 @@ fn insertion_marker_handles_subpixel_strip_crossing_zero_without_panicking() { } #[test] +/// A strip narrower than its tabs paints at least one ellipsis overflow symbol. fn overflowing_strip_paints_a_single_ellipsis_symbol() { use egui_kittest::Harness; fn paints(shape: &egui::Shape, text: &str) -> bool { @@ -1853,6 +1908,7 @@ fn swapped() -> Layout { } #[test] +/// Replacing the layout during a tab drag ends the drag, so the new layout is not modified. fn replacing_the_layout_ends_a_drag_in_progress() { use super::testing::DockDriver; let area = Id::unique("replaced"); diff --git a/crates/ui/src/docking/trial_tests.rs b/crates/ui/src/docking/trial_tests.rs index df34eb6f..56bf03a6 100644 --- a/crates/ui/src/docking/trial_tests.rs +++ b/crates/ui/src/docking/trial_tests.rs @@ -79,6 +79,8 @@ impl DockContent for GalleyLabels { } #[test] +/// Galley-backed labels in shrunk tabs end in an ellipsis and stay wholly inside their tab, both +/// for the active tab and for inactive tabs, at wide and narrow dock widths. fn galley_labels_in_shrunk_tabs_end_with_an_ellipsis_inside_the_tab_active_or_not() { let panels = [1, 2, 3, 4, 5, 6]; let layout = Layout { root: Some(tabs(&panels, 3)), floating: Vec::new() }; @@ -189,6 +191,8 @@ fn double_click_on_the_active_tab_of_a_minimized_group_or_window_ends_expanded() } #[test] +/// A pointer click on a tab shows no focus outline even if the platform then focuses it, while +/// keyboard (Tab) focus paints the accent outline. fn a_clicked_tab_shows_no_focus_outline_but_tab_key_focus_does() { let mut d = Driver::new(frames()); let accent = d.ctx.global_style().visuals.selection.stroke.color; @@ -295,6 +299,8 @@ fn right_to_left_mirrors_the_tab_strip_and_its_insertion_slots() { } #[test] +/// In a right-to-left title bar the close and menu buttons sit on the left, the drag surface ends at +/// the bar's right edge, and a galley title is truncated with an ellipsis inside the bar. fn a_right_to_left_title_bar_puts_its_buttons_left_and_cuts_a_galley_title() { let ctx = Context::default(); let mut shapes = Vec::new(); @@ -327,6 +333,8 @@ fn a_right_to_left_title_bar_puts_its_buttons_left_and_cuts_a_galley_title() { } #[test] +/// With close buttons and close menu items disabled, no close control renders and tab menus contain +/// neither Close Panel nor Close Tab Group; the host can still apply its own CloseWindow action. fn hidden_close_controls_leave_closing_to_the_host() { let layout = Layout { root: Some(tabs(&[1, 2], 0)), floating: vec![Floating::single(vec![5, 6], 0, [80.0, 70.0, 300.0, 220.0])] }; let close = Id::unique(AREA).with(("move-group", Some(&5u32))).with("close"); @@ -361,6 +369,8 @@ fn apply_all_returns_every_panel_its_actions_closed() { } #[test] +/// The column bar button and the column menu item expose distinct accessible names +/// (`Collapse column to icons` versus `Collapse to Icons`). fn the_column_button_and_the_column_menu_item_have_distinct_names() { use egui_kittest::{Harness, kittest::Queryable}; let mut h = Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state( diff --git a/crates/ui/src/layout/resize.rs b/crates/ui/src/layout/resize.rs index 7fd055ab..4206190a 100644 --- a/crates/ui/src/layout/resize.rs +++ b/crates/ui/src/layout/resize.rs @@ -72,6 +72,14 @@ impl<'a> ResizeHandle<'a> { self } + /// Show the handle and return a resize proposal for the caller to apply. + /// + /// Invalid geometry or values make the handle inert. Pointer drags and arrow keys produce a + /// physical `delta` along the split axis; `reversed` flips only accessibility + /// increment/decrement/SetValue changes, not the pointer or arrow movement. A finite nonzero + /// delta marks the response changed. The accessibility role is `Splitter`, oriented + /// perpendicular to the movement axis, with Increment/Decrement and optional SetValue actions + /// and numeric value when configured. pub fn show(self, ui: &mut Ui) -> ResizeResponse { let hit = self.rect.expand2(match self.axis { SplitAxis::Horizontal => Vec2::new(self.hit_margin, 0.0), diff --git a/crates/ui/src/layout/tests.rs b/crates/ui/src/layout/tests.rs index 21342da1..d2a12e65 100644 --- a/crates/ui/src/layout/tests.rs +++ b/crates/ui/src/layout/tests.rs @@ -120,6 +120,8 @@ impl HandleTest { } } + /// Run one enabled `ResizeHandle` in a 400×200 frame and return its response plus the frame + /// output; texture deltas are cleared. fn frame(&mut self, events: Vec) -> (ResizeResponse, FullOutput) { let ctx = self.ctx.clone(); let mut result = None; diff --git a/crates/ui/src/panes/tests.rs b/crates/ui/src/panes/tests.rs index fffd99a1..e6cdeca4 100644 --- a/crates/ui/src/panes/tests.rs +++ b/crates/ui/src/panes/tests.rs @@ -57,6 +57,9 @@ impl App { Self { open: open.to_vec(), states: HashMap::new(), log: Vec::new(), focused: None, to_document: 0, rtl: false, policy: SidePolicy::Columns } } + /// Render `PaneHost` with the app's open panes, policy and RTL flag, then a central document + /// panel. Applies pane requests to stored state and records requests, focused pane and + /// document-focus transitions. fn frame(&mut self, ui: &mut Ui) { let states = self.states.clone(); let out = PaneHost::new(Id::unique("panes")).rtl(self.rtl).policy(self.policy).show(ui, &self.open, |p| states.get(p).copied(), &mut Content); @@ -89,6 +92,7 @@ fn harness(app: App) -> Harness<'static, App> { h } +/// The stable response id for `pane` under the `panes` host scope. fn pane_id(pane: Pane) -> Id { Id::unique("panes").with(("pane", &pane)) } @@ -327,6 +331,8 @@ fn pane_state_round_trips_through_json() { } #[test] +/// A pane body may request 400 pt inside its 250 pt side pane; the document region still begins at +/// the pane's inner edge on both sides. fn a_body_wider_than_its_pane_is_clipped_and_the_document_starts_at_the_pane_edge() { struct Wide; impl PaneContent for Wide { diff --git a/crates/ui/src/printing/tests.rs b/crates/ui/src/printing/tests.rs index a3eede0c..5da489ca 100644 --- a/crates/ui/src/printing/tests.rs +++ b/crates/ui/src/printing/tests.rs @@ -175,6 +175,8 @@ fn active_jobs_keep_their_receiver_and_report_receipt_refusal_or_worker_failure( } #[test] +/// Discovery and rendering localized controls populate the settings (printer, copies, duplex, +/// grayscale) and become ready, without starting or submitting a print job. fn discovery_and_controls_do_not_submit_a_job() { let mut state = State::default(); let discover: Discover = Box::new(|| { @@ -206,6 +208,8 @@ fn discovery_and_controls_do_not_submit_a_job() { } #[test] +/// At 220×360 with long localized labels, the grayscale checkbox stays inside the pane and works; +/// starting a job disables it, and clicking it while pending changes nothing. fn localized_controls_fit_narrow_ui_and_disable_while_submitting() { let ctx = egui::Context::default(); ctx.enable_accesskit(); diff --git a/crates/ui/src/tabs/tests.rs b/crates/ui/src/tabs/tests.rs index 7038f5e7..eb4accbb 100644 --- a/crates/ui/src/tabs/tests.rs +++ b/crates/ui/src/tabs/tests.rs @@ -144,6 +144,8 @@ impl TabTest { } } + /// Render the two tabs at fixed 90×26 rects with injected events. Clears texture deltas, counts + /// body paints, and updates the clicked/selected fixtures before returning the responses. fn frame(&mut self, events: Vec) -> (Vec, FullOutput) { self.time += 0.05; let ctx = self.ctx.clone(); diff --git a/crates/ui/src/text.rs b/crates/ui/src/text.rs index b56e9d49..ec4d7826 100644 --- a/crates/ui/src/text.rs +++ b/crates/ui/src/text.rs @@ -127,10 +127,13 @@ pub fn align_galley_rows(mut galley: Arc, width: f32, mut align: impl Fn mod tests { use super::*; + /// The three-row fixture (`Short` / `A longer line` / `End`) laid out at 1.0 ppp and left aligned. fn layout() -> Arc { layout_text("Short\nA longer line\nEnd", 1.0, Align::Min) } + /// A fresh context at `ppp` with one settling pass, then a simple job of `text` with the requested + /// halign and infinite width. fn layout_with_context(text: &str, ppp: f32, halign: Align) -> (egui::Context, Arc) { let ctx = egui::Context::default(); ctx.set_pixels_per_point(ppp); @@ -141,10 +144,13 @@ mod tests { (ctx, galley) } + /// Convenience wrapper returning only the galley from [`layout_with_context`]. fn layout_text(text: &str, ppp: f32, halign: Align) -> Arc { layout_with_context(text, ppp, halign).1 } + /// Destructure an aligned result into galley, paint offset and reading rect; a valid fixture must + /// not become `InvalidWidth`. fn parts(aligned: &AlignedGalley) -> (&Arc, Vec2, Rect) { match aligned { AlignedGalley::Aligned { galley, paint_offset, reading_rect } => (galley, *paint_offset, *reading_rect), @@ -152,6 +158,7 @@ mod tests { } } + /// Compare two rects componentwise within `epsilon`. fn close_rect(actual: Rect, expected: Rect, epsilon: f32) { assert!((actual.min - expected.min).length() <= epsilon, "{actual:?} != {expected:?}"); assert!((actual.max - expected.max).length() <= epsilon, "{actual:?} != {expected:?}"); @@ -159,6 +166,10 @@ mod tests { // The pre-normalization row placement, independent of the new wrapper/offset code. This is // the old physical-pixel floor formula, applied to the original shared glyph rows. + /// Reconstruct the previous physical-pixel floor placement as an independent oracle. The + /// reference starts from a rect of the requested width, recomputes each row's x from the alignment + /// factor, and unions each row rect and translated ink bound into its `rect` and `mesh_bounds`; + /// overwide rows therefore extend the rect past the requested width. fn reading_space_reference(original: &Galley, width: f32, alignments: &[Align]) -> Arc { let mut reference = original.clone(); reference.rect = Rect::from_min_size(Pos2::ZERO, vec2(width, original.rect.height())); @@ -174,6 +185,8 @@ mod tests { } #[test] + /// Per-row alignment at width 400 keeps row Y positions, the shared job and row arcs, and vertex + /// and index counts; the returned logical width is 400 and every translated ink bound is contained. fn independent_row_alignment_preserves_shaping_and_cached_geometry() { let original = layout(); let positions: Vec<_> = original.rows.iter().map(|row| row.pos).collect(); @@ -213,6 +226,9 @@ mod tests { } #[test] + /// NaN, infinite and negative widths return `InvalidWidth` without calling the alignment callback + /// or changing the galley and halign; there is no reading rect, and painting still uses the original + /// galley at the given origin. fn invalid_widths_preserve_the_input() { for halign in [Align::Min, Align::Center, Align::Max] { let original = layout_text("Short", 1.0, halign); @@ -231,6 +247,9 @@ mod tests { } #[test] + /// At 2.0 ppp, an unbreakably wide row at width 23.3 keeps the requested reading edge through the + /// paint offset for Min/Center/Max: physical x is pixel-aligned and within half a pixel of the + /// expected alignment, and all logical and ink bounds contain the row. fn overwide_rows_keep_the_requested_reading_edge() { let original = layout_text("unbreakable-long-token", 2.0, Align::Min); let width = 23.3; @@ -258,6 +277,8 @@ mod tests { } #[test] + /// A right-aligned overwide row extends left of zero while its right edge stays at the requested + /// reading edge, and the public logical rect contains the actual row. fn overwide_right_aligned_rows_have_complete_logical_bounds() { let original = layout_text("unbreakable-long-token", 2.0, Align::Min); let width = 23.3; @@ -272,6 +293,9 @@ mod tests { } #[test] + /// Fractional width 163.3 with any original halign normalizes the result to a left anchor: halign + /// becomes Min, offset is zero, the logical rect is 0..163.3, and rows keep pixel-snapped absolute + /// positions. fn fractional_width_and_nonleft_input_normalize_the_anchor() { for original_align in [Align::Min, Align::Center, Align::Max] { let original = layout_text("Short", 2.0, original_align); @@ -291,6 +315,8 @@ mod tests { } #[test] + /// Empty, whitespace-only and trailing-newline text produce a valid container rect of the requested + /// width and original height; a cleared row list gives zero height and `Rect::NOTHING` ink. fn empty_and_whitespace_rows_have_valid_logical_bounds() { for text in ["", " ", "Short\n"] { let original = layout_text(text, 1.0, Align::Min); @@ -315,6 +341,9 @@ mod tests { } #[test] + /// At fractional scales, zero and fractional widths, aligning directly and re-aligning after a + /// different first width produce identical reading rects, offsets, logical/ink bounds and row + /// positions; row x positions are pixel-snapped in physical space. fn fractional_scale_zero_width_and_repeated_alignment_keep_absolute_positions() { for ppp in [1.25, 1.5] { let original = layout_text("Short", ppp, Align::Center); @@ -343,6 +372,10 @@ mod tests { } #[test] + /// A synthetic cached quad with binary32 rounding differences keeps its glyph arcs shared. The + /// normalized offset is exactly the whitespace shift; `mesh_bounds` contains every translated vertex + /// and the complete legacy rect, equals the union of both rounding directions, survives repetition, + /// and remains `Rect::NOTHING` when all ink is empty. fn fractional_ink_bounds_contain_translated_rectangles_and_vertices() { // A synthetic cached quad isolates the two directions of binary32 rounding. The // whitespace row supplies the normalization offset without contributing any ink. @@ -456,6 +489,8 @@ mod tests { } #[test] + /// The logical container width follows the reading width while ink bounds stay the glyph extent; + /// whitespace has a container but no ink bounds. fn logical_container_and_ink_bounds_remain_distinct() { let result = align_galley_rows(layout_text("Short", 1.0, Align::Min), 400.0, |_| Align::Min); let (aligned, _, _) = parts(&result); @@ -468,6 +503,7 @@ mod tests { } #[test] + /// A uniquely owned galley is reused at the same allocation instead of being cloned. fn uniquely_owned_galley_reuses_its_allocation() { let original = Arc::new((*layout()).clone()); assert_eq!(Arc::strong_count(&original), 1); @@ -479,6 +515,8 @@ mod tests { const MIXED: &str = "First extraordinarily long reading row\nSecond independently aligned reading row\nThird right-aligned overwide reading row"; const ALIGNMENTS: [Align; 3] = [Align::Min, Align::Center, Align::Max]; + /// Assert the mixed fixture's three rows each overflow `width`, then apply Min/Center/Max in that + /// order through `align_galley_rows`. fn mixed(original: Arc, width: f32) -> AlignedGalley { assert_eq!(original.rows.len(), ALIGNMENTS.len()); assert!(original.rows.iter().all(|row| row.size.x > width), "each fixture row must overflow"); @@ -493,6 +531,9 @@ mod tests { } #[test] + /// Across fractional scales and widths 0/23.3, mixed overflow matches the independent oracle after + /// translating by the offset, shares row arcs, preserves absolute Y positions, and keeps the + /// reading rect at the requested width. fn mixed_overflow_normalizes_bounds_without_changing_absolute_rows_or_ink() { for ppp in [1.25, 1.5, 2.0] { let original = layout_text(MIXED, ppp, Align::Center); @@ -523,6 +564,8 @@ mod tests { } #[test] + /// `text_shape` anchors rotation at the complete logical bound rather than the reading rect; row + /// corners and the visual ink bounds match an independently rotated oracle for several anchors. fn public_text_shape_uses_the_complete_logical_bound_for_rotation_anchors() { let original = layout_text(MIXED, 1.5, Align::Min); let expected = reading_space_reference(&original, 23.3, &ALIGNMENTS); @@ -555,6 +598,9 @@ mod tests { } #[test] + /// At fractional DPI and origins, including points straddling a physical pixel, `paint` emits one + /// text shape whose tessellated vertices, indices, UVs and colors match the reading-space reference + /// at the same physical pixels, with text rounding both on and off. fn painter_preserves_visible_vertices_at_fractional_origins_and_dpi() { for ppp in [1.25, 1.5, 2.0] { let (ctx, original) = layout_with_context(MIXED, ppp, Align::Min); diff --git a/crates/ui/tests/docking_workflows.rs b/crates/ui/tests/docking_workflows.rs index d513cf15..571265c6 100644 --- a/crates/ui/tests/docking_workflows.rs +++ b/crates/ui/tests/docking_workflows.rs @@ -77,6 +77,9 @@ impl Harness { harness } + /// One 520 pt-high frame through the real wgpu test renderer. Applies returned actions with + /// canvas protection (round-tripping them through JSON when `serde` is enabled), validates the + /// layout, and records body ids and applied actions. fn step(&mut self, events: Vec) { self.frame += 1; let input = RawInput { @@ -180,6 +183,7 @@ impl Harness { } } + /// The response rect of `panel`'s tab in the workflow dock. fn tab(&self, panel: u32) -> Rect { self.ctx.read_response(Id::unique("workflow").with(("tab", &panel))).unwrap().rect }