mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
M-egui037: document shared UI behavior and regression contracts
This commit is contained in:
1 parent
253c425e2f
commit
4e03c031de
19 files changed
+137
No files matched your search
@@ -108,6 +108,8 @@ impl Draw<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate the note shown for an approximated imported shortcut; currently
|
||||
/// only the numeric-keypad read-as-main-key note exists.
|
||||
fn read_note(&self, n: ReadNote) -> String {
|
||||
match n {
|
||||
ReadNote::Numpad => self.ui("numeric keypad key read as main key"),
|
||||
|
||||
@@ -61,6 +61,7 @@ impl Fx {
|
||||
self.km.bindings(&CommandRef::from(id)).iter().map(|b| b.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Whether any outcome recorded by the dialog harness is `Outcome::Cancel`.
|
||||
fn canceled(&self) -> bool {
|
||||
self.outcomes.iter().any(|o| matches!(o, Outcome::Cancel))
|
||||
}
|
||||
|
||||
@@ -117,6 +117,8 @@ fn click(h: &mut Harness<'_, Fx>, label: &str) {
|
||||
h.run();
|
||||
}
|
||||
|
||||
/// Click the widget carrying `label` and run a frame; used to put a command row
|
||||
/// into the snapshot's selected state.
|
||||
fn select(h: &mut Harness<'_, Fx>, label: &str) {
|
||||
h.get_by_label(label).click();
|
||||
h.run();
|
||||
|
||||
@@ -14,6 +14,7 @@ impl DockContent<u32> for Panels {
|
||||
ui.label(format!("Body of panel {panel}"));
|
||||
}
|
||||
|
||||
/// Return the example content's logical and accessibility label, `Panel {panel}`.
|
||||
fn logical_label(&self, panel: &u32) -> String {
|
||||
format!("Panel {panel}")
|
||||
}
|
||||
|
||||
@@ -177,10 +177,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synthetic tab widths from 40 to 119 pt, cycling with `(i * 17) % 80`.
|
||||
fn tab_widths(count: usize) -> Vec<f32> {
|
||||
(0..count).map(|i| 40.0 + ((i * 17) % 80) as f32).collect()
|
||||
}
|
||||
|
||||
/// Synthetic toolbar widths from 24 to 71 pt, cycling with `(i * 13) % 48`.
|
||||
fn toolbar_widths(count: usize) -> Vec<f32> {
|
||||
(0..count).map(|i| 24.0 + ((i * 13) % 48) as f32).collect()
|
||||
}
|
||||
@@ -263,6 +265,8 @@ fn font_definitions() -> Result<(egui::FontDefinitions, Vec<String>), Box<dyn Er
|
||||
Ok((fonts, names))
|
||||
}
|
||||
|
||||
/// Describe the report's font source: the pinned `CRAFT_UI_PERF_FONT` file when
|
||||
/// set, otherwise egui's bundled default fonts.
|
||||
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)" }
|
||||
}
|
||||
@@ -281,6 +285,7 @@ fn context(scale: f32, fonts: &egui::FontDefinitions) -> Context {
|
||||
ctx
|
||||
}
|
||||
|
||||
/// A headless screen rectangle from `(0, 0)` to `viewport[0] × viewport[1]`.
|
||||
fn screen_rect(viewport: [f32; 2]) -> Rect {
|
||||
Rect::from_min_size(pos2(0.0, 0.0), vec2(viewport[0], viewport[1]))
|
||||
}
|
||||
@@ -424,6 +429,7 @@ fn dock_layout(panels: u32, with_floating: bool) -> Layout<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the benchmark content's logical label, `Panel {panel}`.
|
||||
fn panel_label(panel: &u32) -> String {
|
||||
format!("Panel {panel}")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ fn observe_repaints(ctx: &Context) -> Arc<Mutex<Vec<ViewportId>>> {
|
||||
#[test]
|
||||
/// Compile-time assertion that `CaptureSlot` and `ScreenshotCallback` are `Send + Sync`.
|
||||
fn slot_is_send_and_sync() {
|
||||
/// Compile-time helper requiring `T: Send + Sync`; the enclosing test uses it to
|
||||
/// assert the public capture types are thread-safe.
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<CaptureSlot>();
|
||||
assert_send_sync::<ScreenshotCallback>();
|
||||
|
||||
@@ -100,6 +100,8 @@ impl Driver {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run three empty frames so floating areas finish their sizing pass, then
|
||||
/// return the driver.
|
||||
fn start(mut driver: Self) -> Self {
|
||||
// Floating areas settle after their sizing pass.
|
||||
for _ in 0..3 {
|
||||
@@ -144,10 +146,13 @@ impl Driver {
|
||||
output
|
||||
}
|
||||
|
||||
/// Read the response rectangle for the dock widget named `name` and scoped to
|
||||
/// `panel`.
|
||||
fn rect(&self, name: &str, panel: u32) -> Rect {
|
||||
self.ctx.read_response(Id::unique(AREA).with((name, Some(&panel)))).unwrap().rect
|
||||
}
|
||||
|
||||
/// Read the response rectangle for `panel`'s tab.
|
||||
fn tab(&self, panel: u32) -> Rect {
|
||||
self.ctx.read_response(Id::unique(AREA).with(("tab", &panel))).unwrap().rect
|
||||
}
|
||||
@@ -413,6 +418,8 @@ fn tab_dragged_to_the_window_edge_without_a_dock_makes_a_dock_of_that_panel() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A group dropped on a column's tab strip joins as a tab, on a group body
|
||||
/// appends as its last tab, and in the empty space below starts a new group.
|
||||
fn a_column_takes_groups_anywhere_in_it_and_as_tabs() {
|
||||
let windows = [(5, [400.0, 40.0, 150.0, 120.0]), (6, [400.0, 220.0, 150.0, 120.0]), (7, [400.0, 400.0, 150.0, 120.0])];
|
||||
let mut d = Driver::new(with_floating(docked(Side::Start, &[&[&[2], &[3]]]), &windows));
|
||||
@@ -461,6 +468,8 @@ fn a_column_too_tall_for_the_window_scrolls() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dragging a column bar to the window edge moves the column first; dragging it
|
||||
/// away from the dock floats every group in the column and shows a follow ghost.
|
||||
fn column_bar_drag_moves_the_column_and_away_from_a_dock_floats_its_groups() {
|
||||
let mut d = Driver::new(docked(Side::Start, &[&[&[2], &[3]], &[&[5]]]));
|
||||
// The second column to the window edge: it becomes the first.
|
||||
@@ -619,6 +628,8 @@ fn right_to_left_mirrors_docks_their_edges_and_new_docks() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dropping a column's first tab just past its own edge anchors it after the
|
||||
/// other column's panel; a panel alone in its column stays in place.
|
||||
fn a_column_s_first_tab_dropped_on_its_own_column_edge_anchors_on_another_panel() {
|
||||
// Two columns, 0..240 and 240..480; the dragged tab is the first panel of the last one.
|
||||
let wide = |columns: &[&[&[u32]]]| {
|
||||
|
||||
@@ -109,6 +109,7 @@ struct Driver {
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
/// Create a floating test driver and run two empty frames to settle sizing.
|
||||
fn new(layout: Layout<u32>, split_tree: bool) -> Self {
|
||||
let mut driver = Self {
|
||||
ctx: Context::default(),
|
||||
@@ -172,10 +173,12 @@ impl Driver {
|
||||
self.ctx.read_response(id).map(|r| r.rect)
|
||||
}
|
||||
|
||||
/// Read the response rectangle of the move bar whose first panel is `first`.
|
||||
fn title(&self, first: u32) -> Rect {
|
||||
self.response(Id::unique(AREA).with(("move-group", Some(&first)))).unwrap()
|
||||
}
|
||||
|
||||
/// Read `panel`'s tab response rectangle, or `None` when it is not rendered.
|
||||
fn tab(&self, panel: u32) -> Option<Rect> {
|
||||
self.response(Id::unique(AREA).with(("tab", &panel)))
|
||||
}
|
||||
@@ -261,6 +264,8 @@ fn title_bar_drag_moves_every_stacked_group_and_drops_into_a_dock_as_stacked_gro
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Releasing a floating window's title bar on a dock seam stacks its group
|
||||
/// between the target groups; releasing on the edge band makes an end dock.
|
||||
fn title_bar_drag_onto_dock_targets_docks_the_window() {
|
||||
// Released on the seam between two docked groups: the window's group stacks between them.
|
||||
let mut d = Driver::new(workspace(), false);
|
||||
@@ -398,6 +403,8 @@ fn outer_edge_band_splits_at_the_root() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// With split-tree chrome, a drop 7 pt inside the outer edge targets the edge
|
||||
/// dock, while a drop at 10 pt targets frame [1]'s split zone.
|
||||
fn the_split_tree_outer_edge_band_is_8_pt() {
|
||||
let mut d = Driver::new(frames(), true);
|
||||
let tab = d.tab(3).unwrap().center();
|
||||
@@ -481,6 +488,8 @@ fn close_other_panels_keeps_only_the_chosen_tab() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Hiding every panel draws no tabs and leaves the layout unchanged; the
|
||||
/// `Except` set keeps only its panels, with a retained window below the resize band.
|
||||
fn hide_panels_keeps_protected_panels_and_the_except_set() {
|
||||
let mut d = Driver::new(workspace(), false);
|
||||
d.hide = Hide::All;
|
||||
@@ -521,6 +530,8 @@ fn a_canvas_that_accepts_splits_is_still_protected() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// When viewports are embedded, a floating window marked native draws in the
|
||||
/// main window at its usual rect without emitting actions.
|
||||
fn native_window_falls_back_in_window_when_viewports_are_embedded() {
|
||||
let mut layout = workspace();
|
||||
layout.floating[0].native = Some([1200.0, 900.0, 240.0, 300.0]);
|
||||
@@ -663,6 +674,8 @@ fn hidden_panels_leave_their_split_and_resizing_names_the_layout_path() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Hiding floating window 5 removes its coverage of the dock seam, so dragging
|
||||
/// window 8 there stacks its group between panels 3 and 4.
|
||||
fn a_hidden_floating_window_does_not_cover_dock_targets() {
|
||||
// Window 5 covers the column's seam at x 150..240 until it is hidden.
|
||||
let mut layout = workspace();
|
||||
|
||||
@@ -59,6 +59,8 @@ struct TestContent {
|
||||
}
|
||||
|
||||
impl DockContent<u32> for TestContent {
|
||||
/// Test body that fills its rect when `fill` is set, otherwise takes the
|
||||
/// configured height or draws a one-line `Body` label.
|
||||
fn body(&mut self, ui: &mut egui::Ui, _: &u32) {
|
||||
if self.fill {
|
||||
ui.allocate_rect(ui.max_rect(), egui::Sense::hover());
|
||||
@@ -72,6 +74,8 @@ impl DockContent<u32> for TestContent {
|
||||
fn logical_label(&self, panel: &u32) -> String {
|
||||
format!("Panel {panel}")
|
||||
}
|
||||
/// Mark panels in `protected` as protected and leave the rest with default
|
||||
/// permissions.
|
||||
fn permissions(&self, panel: &u32) -> Permissions {
|
||||
if self.protected.contains(panel) { Permissions::PROTECTED } else { Permissions::default() }
|
||||
}
|
||||
@@ -114,6 +118,8 @@ impl Driver {
|
||||
Self::with(layout, TestContent::default(), Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 600.0)))
|
||||
}
|
||||
|
||||
/// Create a group test driver for `layout`, `content` and `area`, then settle
|
||||
/// two frames.
|
||||
fn with(layout: Layout<u32>, content: TestContent, area: Rect) -> Self {
|
||||
let mut driver = Self {
|
||||
ctx: Context::default(),
|
||||
@@ -162,6 +168,8 @@ impl Driver {
|
||||
output
|
||||
}
|
||||
|
||||
/// Run one frame, assert it reports no error, and apply each returned action
|
||||
/// with the content's permissions.
|
||||
fn step(&mut self, events: Vec<Event>) -> DockOutput<u32> {
|
||||
let output = self.frame(events);
|
||||
assert!(output.error.is_none(), "{output:?}");
|
||||
@@ -172,10 +180,13 @@ impl Driver {
|
||||
output
|
||||
}
|
||||
|
||||
/// Read the response rectangle for the dock widget named `name` and scoped to
|
||||
/// `panel`.
|
||||
fn rect(&self, name: &str, panel: u32) -> Rect {
|
||||
self.ctx.read_response(Id::unique(AREA).with((name, Some(&panel)))).unwrap().rect
|
||||
}
|
||||
|
||||
/// Read the response rectangle for `panel`'s tab.
|
||||
fn tab(&self, panel: u32) -> Rect {
|
||||
self.ctx.read_response(Id::unique(AREA).with(("tab", &panel))).unwrap().rect
|
||||
}
|
||||
@@ -222,6 +233,9 @@ fn moved_group(actions: &[Action<u32>]) -> Option<&Target<u32>> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A group bar dropped on another strip emits one `MoveGroup` and keeps panel
|
||||
/// order and the active panel; dropped on empty space it floats at its docked
|
||||
/// width and 600 pt height; inside a column it joins the group below.
|
||||
fn docked_two_tab_group_drags_by_its_bar_and_keeps_order_and_active_tab() {
|
||||
// Onto another group's strip: one MoveGroup, both panels in order, 3 still selected.
|
||||
let mut d = Driver::new(frames());
|
||||
@@ -376,6 +390,8 @@ fn group_drop_on_a_frame_edge_splits_and_on_a_protected_canvas_floats() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Holding Ctrl or Command at release turns either a group-bar drop or a tab
|
||||
/// drop into a float instead of a dock.
|
||||
fn ctrl_or_cmd_held_at_release_always_floats() {
|
||||
for modifiers in [Modifiers::CTRL, Modifiers::MAC_CMD, Modifiers::COMMAND] {
|
||||
let mut d = Driver::new(frames());
|
||||
@@ -528,6 +544,8 @@ fn floating_border_and_drag_bar_move_and_cancel_as_a_group() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Moving a floating window by eight fractional pointer steps lands within one
|
||||
/// point of the accumulated 80.3 × 60.7 pt offset, without step-rounding drift.
|
||||
fn floating_border_movement_does_not_accumulate_fractional_step_rounding() {
|
||||
let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0])] };
|
||||
let mut d = Driver::new(layout);
|
||||
@@ -542,6 +560,8 @@ fn floating_border_movement_does_not_accumulate_fractional_step_rounding() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// With host-owned headers, dragging either the border or the bar reaches
|
||||
/// `whole_group`, emits no action or group drop, and leaves the layout unchanged.
|
||||
fn docked_header_drag_reaches_the_host_hook_and_preserves_the_group() {
|
||||
for name in ["drag-border", "drag-bar"] {
|
||||
let content = TestContent { host_owned: true, ..Default::default() };
|
||||
@@ -559,7 +579,9 @@ fn docked_header_drag_reaches_the_host_hook_and_preserves_the_group() {
|
||||
fn a_lone_protected_panel_draws_no_strip_and_no_bar() {
|
||||
struct Bare;
|
||||
impl DockContent<u32> for Bare {
|
||||
/// A bare content body that draws nothing.
|
||||
fn body(&mut self, _: &mut egui::Ui, _: &u32) {}
|
||||
/// Mark panel 1 protected and leave every other panel with default permissions.
|
||||
fn permissions(&self, panel: &u32) -> Permissions {
|
||||
if *panel == 1 { Permissions::PROTECTED } else { Permissions::default() }
|
||||
}
|
||||
@@ -615,6 +637,8 @@ fn drag_bar_menu_has_the_group_items_and_opens_from_the_keyboard() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dragging one group out of a two-group floating stack floats only that group
|
||||
/// at the stack width and its recorded height in the stack.
|
||||
fn a_group_dragged_out_of_a_floating_stack_tears_out_alone() {
|
||||
let stack = Floating::stack(vec![Group::new(vec![5], 0), Group::new(vec![6, 7], 1)], [80.0, 70.0, 300.0, 400.0]).unwrap();
|
||||
let layout = Layout { root: Some(tabs(&[1], 0)), floating: vec![stack] };
|
||||
@@ -671,6 +695,8 @@ fn a_short_docked_panel_floats_at_its_content_height_up_to_its_floating_size() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A fill-height panel floats at the 320 pt docked cap without a host size
|
||||
/// and at the host's height when one is supplied.
|
||||
fn a_panel_that_fills_its_frame_floats_at_its_floating_size_or_320() {
|
||||
let float = |floating_size: Option<Vec2>| {
|
||||
let content = TestContent { fill: true, floating_size, ..Default::default() };
|
||||
|
||||
@@ -103,6 +103,9 @@ fn widening_an_iconic_column_shows_labels() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// An icon click activates its panel and opens a canvas-side flyout; another
|
||||
/// icon switches it, and the same icon closes it even with auto-collapse off. Escape
|
||||
/// and outside presses close it only with auto-collapse on; a programmatic raise opens it.
|
||||
fn icon_click_opens_a_flyout_on_the_canvas_side_and_auto_collapse_closes_it() {
|
||||
let mut d = DockDriver::new(docked(&[&[1, 2], &[3]], true), Content);
|
||||
let icon = hit(&d, Hit::Icon(2)).center();
|
||||
|
||||
@@ -1491,6 +1491,8 @@ fn the_bars_close_button_wins_over_the_resize_bands() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Resize bands avoid the close button, are at most 6 pt deep, use 16 pt
|
||||
/// corners along each edge, and leave the bar below the top band inert.
|
||||
fn resize_bands_are_a_few_points_deep_and_leave_out_the_bar_buttons() {
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(240.0, 300.0));
|
||||
let close = Rect::from_center_size(pos2(224.0, 9.0), vec2(18.0, 18.0));
|
||||
@@ -1624,6 +1626,9 @@ fn dimmed(d: &DockDriver<u32, Content>, viewport: egui::ViewportId) -> bool {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// While input is blocked, a native window is dimmed, loses always-on-top,
|
||||
/// ignores clicks, drags, key presses and close requests, but still forwards key
|
||||
/// releases; unblocking restores normal input and activation.
|
||||
fn a_native_window_takes_no_input_while_the_app_blocks_it() {
|
||||
let mut d = blocked_driver();
|
||||
let id = id_of(&d);
|
||||
@@ -2238,6 +2243,8 @@ fn escape_and_cancel_drag_end_the_routing_of_an_app_drag() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dragging a tab onto a native window's body emits a tab `Move` rather than
|
||||
/// an app body drag and leaves the content's drops empty.
|
||||
fn a_tab_drag_onto_a_native_window_is_no_app_drag() {
|
||||
let mut d = app_drag(layout(true), 1, &[5]);
|
||||
let id = window(&d, 5).id;
|
||||
|
||||
@@ -848,6 +848,7 @@ fn body_input_key() -> Id {
|
||||
pub(in crate::docking) type InputHook = Box<dyn FnMut(&Context, &mut RawInput) + Send>;
|
||||
|
||||
impl egui::Plugin for NativeInput {
|
||||
/// The egui plugin's diagnostic name, `craft-ui docking native input`.
|
||||
fn debug_name(&self) -> &'static str {
|
||||
"craft-ui docking native input"
|
||||
}
|
||||
|
||||
@@ -472,6 +472,8 @@ fn saved_layout_and_hidden_location_round_trip() {
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
/// Deserialize an older hidden `Location` without a `sibling` field; the
|
||||
/// missing list stays empty and the anchor panel is preserved.
|
||||
fn old_hidden_locations_accept_panel_ids_without_a_default_identity() {
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum Panel {
|
||||
@@ -515,6 +517,8 @@ fn frame(ctx: &Context, layout: &Layout<u32>, events: Vec<Event>, focused: bool)
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Moving a panel from a root tab into a floating window keeps its body scope
|
||||
/// id across frames.
|
||||
fn renderer_keeps_content_identity_when_panel_moves_into_a_float() {
|
||||
let ctx = Context::default();
|
||||
let mut layout = Layout { root: Some(tabs(&[1], 0)), floating: Vec::new() };
|
||||
@@ -564,6 +568,7 @@ fn step(ctx: &Context, layout: &mut Layout<u32>, events: Vec<Event>) -> Vec<Acti
|
||||
out.actions
|
||||
}
|
||||
|
||||
/// A primary-button pointer event at `pos` with `pressed` and no modifiers.
|
||||
fn pointer_button(pos: egui::Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
|
||||
}
|
||||
@@ -812,6 +817,8 @@ fn split_inner_edge_remains_draggable_beside_a_scrollbar() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Closing a panel in a nested vertical/horizontal layout and restoring its
|
||||
/// saved location reproduces the complete layout.
|
||||
fn restoring_a_panel_beside_a_nested_subtree_preserves_the_complete_workspace() {
|
||||
let mut layout = Layout {
|
||||
root: Some(Node::Split {
|
||||
@@ -968,6 +975,8 @@ fn floating_group_touch_release_reports_drop_after_pointer_gone() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A floating group drag cancelled by pointer loss, with or without Escape, restores the
|
||||
/// original floating rect and emits no group drag or drop.
|
||||
fn floating_group_exit_with_escape_or_focus_loss_restores_original_geometry() {
|
||||
for escape in [false, true] {
|
||||
let ctx = Context::default();
|
||||
@@ -1035,13 +1044,17 @@ impl DockContent<u32> for CustomContent {
|
||||
self.labels.push(logical.to_owned());
|
||||
ui.painter().layout_no_wrap("Custom visual label".into(), font.clone(), color).into()
|
||||
}
|
||||
/// Custom visual action label that prefixes the logical text with
|
||||
/// `Action visual: `.
|
||||
fn action_label(&mut self, ui: &egui::Ui, logical: &str, font: &egui::FontId, color: egui::Color32) -> egui::WidgetText {
|
||||
ui.painter().layout_no_wrap(format!("Action visual: {logical}"), font.clone(), color).into()
|
||||
}
|
||||
/// Record an app menu draw and emit a clickable `App menu` button.
|
||||
fn panel_menu(&mut self, ui: &mut egui::Ui, _: &u32) {
|
||||
self.menus += 1;
|
||||
ui.button("App menu").clicked();
|
||||
}
|
||||
/// Test custom chrome declares a 40 pt group header width.
|
||||
fn group_header_width(&self, _: &[u32], _: &u32, _: bool, _: f32) -> f32 {
|
||||
40.0
|
||||
}
|
||||
@@ -1054,6 +1067,8 @@ impl DockContent<u32> for CustomContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one 800×600 customized dock frame with optional blockers and a
|
||||
/// programmatic raise of panel 5; returns the output with texture deltas cleared.
|
||||
fn custom_frame(
|
||||
ctx: &Context,
|
||||
layout: &Layout<u32>,
|
||||
@@ -1077,6 +1092,9 @@ fn custom_frame(
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom chrome receives logical labels, draws custom headers and bodies and app-menu
|
||||
/// popups, while a programmatic raise sets no user focus or focus ring and emits
|
||||
/// no action.
|
||||
fn custom_chrome_shaped_labels_and_programmatic_raise_are_available() {
|
||||
let ctx = Context::default();
|
||||
let layout = sample();
|
||||
@@ -1128,6 +1146,7 @@ fn overlay_appearing_mid_drag_blocks_release_and_float_fallback() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Escape during a floating group move restores the original floating rect.
|
||||
fn group_move_escape_restores_original_geometry() {
|
||||
let ctx = Context::default();
|
||||
let mut layout = sample();
|
||||
@@ -1248,6 +1267,8 @@ fn resize_floating_group(permissions: impl Fn(&u32) -> Permissions + Copy) -> Ve
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A floating group's grip resizes a movable group, but a group containing
|
||||
/// protected panel 6 blocks the `MoveFloating` action.
|
||||
fn protected_group_member_blocks_floating_group_resize() {
|
||||
let moved = |actions: &[Action<u32>]| actions.iter().any(|a| matches!(a, Action::MoveFloating { .. }));
|
||||
assert!(moved(&resize_floating_group(|_| Permissions::default())), "the grip resizes a movable group");
|
||||
@@ -1455,6 +1476,8 @@ fn workspace_cancel_preserves_other_area_and_body_drag_ownership() {
|
||||
/// 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};
|
||||
/// Recursively search a shape or shape vector for a text galley whose text is
|
||||
/// exactly `text`.
|
||||
fn paints(shape: &egui::Shape, text: &str) -> bool {
|
||||
match shape {
|
||||
egui::Shape::Text(shape) => shape.galley.text() == text,
|
||||
@@ -1658,6 +1681,8 @@ fn collapsed_floating_height_cap_does_not_disable_keyboard_width_resizing() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dropping a panel back on its own singleton or multi-tab source body is an
|
||||
/// explicit no-op with no insertion marker, with or without split permission.
|
||||
fn returning_to_a_singleton_or_floating_source_body_is_an_explicit_noop() {
|
||||
for panels in [vec![1u32], vec![1, 2, 3]] {
|
||||
for allow_splits in [false, true] {
|
||||
@@ -1722,6 +1747,8 @@ fn pointer_floating_resize_stops_at_edges_and_waits_for_cursor_to_return() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Dropping a panel within its existing tab slots is a no-op whose insertion
|
||||
/// marker stays inside the strip; positions outside its own slots move it.
|
||||
fn unchanged_tab_slots_are_explicit_noops_with_bounded_insertion_markers() {
|
||||
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(300.0, 200.0));
|
||||
let target = DropTarget {
|
||||
@@ -1758,6 +1785,8 @@ fn host_owned_noop_consumes_release_without_a_fallback_float() {
|
||||
struct Content(usize);
|
||||
impl DockContent<u32> for Content {
|
||||
fn body(&mut self, _ui: &mut egui::Ui, _panel: &u32) {}
|
||||
/// Test drop handler that counts releases and accepts every request with
|
||||
/// default feedback, consuming host-owned drops.
|
||||
fn resolve_drag(&mut self, _ctx: &Context, _style: &DockStyle, request: &DropRequest<'_, u32>) -> Option<DropFeedback> {
|
||||
if request.released {
|
||||
self.0 += 1;
|
||||
@@ -1828,6 +1857,8 @@ fn overflow_noop_detection_uses_logical_order_including_hidden_tabs() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// For clipped and singleton strips of several widths, a no-op drop keeps its
|
||||
/// insertion marker inside the visible strip and at most 2 pt wide.
|
||||
fn insertion_markers_stay_inside_clipped_and_singleton_strips() {
|
||||
for width in [0.5, 1.0, 20.0, 140.0] {
|
||||
let rect = Rect::from_min_size(pos2(10.0, 20.0), vec2(width, 200.0));
|
||||
@@ -1854,6 +1885,8 @@ fn insertion_markers_stay_inside_clipped_and_singleton_strips() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// A subpixel-wide tab strip crossing zero still yields a no-op insertion
|
||||
/// marker contained by the strip.
|
||||
fn insertion_marker_handles_subpixel_strip_crossing_zero_without_panicking() {
|
||||
let rect = Rect::from_min_max(pos2(-0.001, 0.0), pos2(0.002, 30.0));
|
||||
let target =
|
||||
|
||||
@@ -73,6 +73,8 @@ impl DockContent<u32> for GalleyLabels {
|
||||
fn logical_label(&self, panel: &u32) -> String {
|
||||
format!("Character Styles {panel}")
|
||||
}
|
||||
/// Host-provided galley label that lays the logical text out without wrapping,
|
||||
/// letting the dock handle pre-shaped widget text.
|
||||
fn label(&mut self, ui: &egui::Ui, _: &u32, logical: &str, font: &egui::FontId, color: Color32) -> egui::WidgetText {
|
||||
egui::WidgetText::Galley(ui.painter().layout_no_wrap(logical.to_owned(), font.clone(), color))
|
||||
}
|
||||
@@ -162,6 +164,8 @@ fn double_click_on_a_tab_of_a_minimized_group_ends_expanded() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Double-clicking the active tab of a minimized floating window or docked
|
||||
/// group expands it; a second double-click minimizes the docked group again.
|
||||
fn double_click_on_the_active_tab_of_a_minimized_group_or_window_ends_expanded() {
|
||||
let window = Floating { minimized: true, ..Floating::single(vec![5, 6], 1, [80.0, 70.0, 300.0, 220.0]) };
|
||||
let mut d = Driver::new(Layout { root: Some(tabs(&[1], 0)), floating: vec![window] });
|
||||
@@ -268,6 +272,9 @@ fn a_press_in_a_stripless_panel_reports_it_focused() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// In a right-to-left strip the first tab sits at the right edge and the bar
|
||||
/// lies to its left; drops just right of the first tab insert before panel 2,
|
||||
/// and insertion slots are mirrored at the strip edges.
|
||||
fn right_to_left_mirrors_the_tab_strip_and_its_insertion_slots() {
|
||||
let mut d = Driver::with(frames(), TestContent { header: 30.0, ..Default::default() }, AREA_600);
|
||||
d.tweak = |style| style.right_to_left = true;
|
||||
@@ -360,6 +367,8 @@ fn hidden_close_controls_leave_closing_to_the_host() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// `apply_all` returns every panel closed by close-others and close-window in
|
||||
/// order, leaving the remaining panel open until a later `apply` closes it.
|
||||
fn apply_all_returns_every_panel_its_actions_closed() {
|
||||
let mut layout = Layout { root: Some(tabs(&[1, 2, 3], 0)), floating: vec![Floating::single(vec![5, 6], 0, [80.0, 70.0, 300.0, 220.0])] };
|
||||
let closed = layout
|
||||
|
||||
@@ -107,6 +107,8 @@ struct HandleTest {
|
||||
}
|
||||
|
||||
impl HandleTest {
|
||||
/// Create a handle test with horizontal axis, a 4×140 pt rect, the label
|
||||
/// `Resize panels`, enabled state and value 100.
|
||||
fn new() -> Self {
|
||||
let ctx = Context::default();
|
||||
ctx.enable_accesskit();
|
||||
|
||||
@@ -29,6 +29,8 @@ impl PaneContent<Pane> for Content {
|
||||
fn body(&mut self, ui: &mut Ui, pane: &Pane) {
|
||||
ui.label(format!("{pane:?} body"));
|
||||
}
|
||||
/// Default docked pane state: end for Comments and Pivot, start otherwise,
|
||||
/// with a 250 pt size.
|
||||
fn default_state(&self, pane: &Pane) -> PaneState {
|
||||
let edge = if matches!(pane, Pane::Comments | Pane::Pivot) { Edge::End } else { Edge::Start };
|
||||
PaneState::docked(edge, 250.0)
|
||||
@@ -53,6 +55,8 @@ struct App {
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create an app with `open` panes, empty state and log, no focused pane, no
|
||||
/// document-focus requests, left-to-right layout and column side policy.
|
||||
fn new(open: &[Pane]) -> Self {
|
||||
Self { open: open.to_vec(), states: HashMap::new(), log: Vec::new(), focused: None, to_document: 0, rtl: false, policy: SidePolicy::Columns }
|
||||
}
|
||||
@@ -85,6 +89,8 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an 800×600 harness around `App::frame`, mark input focused, and run
|
||||
/// two frames before returning it.
|
||||
fn harness(app: App) -> Harness<'static, App> {
|
||||
let mut h = Harness::builder().with_size(vec2(800.0, 600.0)).build_ui_state(|ui, app: &mut App| app.frame(ui), app);
|
||||
h.input_mut().focused = true;
|
||||
@@ -324,6 +330,8 @@ fn float_rect_is_finite_and_inside_the_bounds() {
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[test]
|
||||
/// A `PaneState` with a docked edge, size and float rect serializes to JSON and
|
||||
/// deserializes back unchanged.
|
||||
fn pane_state_round_trips_through_json() {
|
||||
let state = PaneState { place: Place::Docked(Edge::End), size: Some(280.0), float_rect: Some([1.0, 2.0, 270.0, 480.0]) };
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
@@ -339,6 +347,7 @@ fn a_body_wider_than_its_pane_is_clipped_and_the_document_starts_at_the_pane_edg
|
||||
fn title(&self, pane: &Pane) -> String {
|
||||
format!("{pane:?}")
|
||||
}
|
||||
/// A pane body that requests 400×40 pt inside its pane to exercise clipping.
|
||||
fn body(&mut self, ui: &mut Ui, _: &Pane) {
|
||||
ui.allocate_exact_size(vec2(400.0, 40.0), Sense::hover());
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@ fn failed_discovery_cannot_reuse_a_previously_available_queue() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Only one print job can be active: a second `begin` fails, the receiver stays
|
||||
/// with the job, and submission, queue refusal and a dropped worker report
|
||||
/// `Submitted`, `Failed` and `WorkerStopped` while returning to ready.
|
||||
fn active_jobs_keep_their_receiver_and_report_receipt_refusal_or_worker_failure() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut state = State::default();
|
||||
|
||||
@@ -127,6 +127,8 @@ struct TabTest {
|
||||
}
|
||||
|
||||
impl TabTest {
|
||||
/// Create a two-tab test with duplicate `Layers` labels, the first selected and
|
||||
/// enabled, click sense, a 90×26 pt rect and zero counters.
|
||||
fn new() -> Self {
|
||||
let ctx = Context::default();
|
||||
ctx.enable_accesskit();
|
||||
|
||||
@@ -40,6 +40,9 @@ struct Harness {
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
/// Create a workflow harness with the requested theme, width and pixel scale,
|
||||
/// zero animation and no cursor blink, and a canvas plus side tabs and floating
|
||||
/// brushes layout, then settle it.
|
||||
fn new(dark: bool, width: f32, scale: f32) -> Self {
|
||||
let ctx = Context::default();
|
||||
ctx.set_theme(if dark { egui::Theme::Dark } else { egui::Theme::Light });
|
||||
@@ -177,6 +180,7 @@ impl Harness {
|
||||
self.layout.validate().unwrap();
|
||||
}
|
||||
|
||||
/// Run four empty workflow frames to settle sizing.
|
||||
fn settle(&mut self) {
|
||||
for _ in 0..4 {
|
||||
self.step(Vec::new());
|
||||
|
||||
Reference in new issue
Block a user