From 4239e1b0c24fcd327bd01129f4d8e4ce36879c4c Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 20 Oct 2026 18:01:07 +0000 Subject: [PATCH] #1311: craft-ui 352a830 (craft-libs#187 review fixes) Pin craft-ui to aiongg/craft-libs 352a830. Workspace::set_visible takes the panels' permissions; group_header_width takes the strip width. Tests reach docking controls through DockArea::hit_id and floating_layer instead of rebuilding egui ids. Control names in DOCK_LABELS follow craft-ui's sentence case. Test: dragging a dock divider and then pressing an arrow key nudges the selection and leaves the dock's size alone. --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/ui-egui/src/dock.rs | 2 +- crates/ui-egui/src/menus.rs | 4 +- crates/ui-egui/src/panel_docking.rs | 31 ++++++++--- crates/ui-egui/src/panel_docking_tests.rs | 64 +++++++++++++++++------ 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 836d2b953..8f06d3e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=13be52fbaa2c4936c8d7b3d2b5abe6ab72892123#13be52fbaa2c4936c8d7b3d2b5abe6ab72892123" +source = "git+https://github.com/aiongg/craft-libs?rev=352a8308c5cc53c2ae31326fa38a4a6bcb0ff9eb#352a8308c5cc53c2ae31326fa38a4a6bcb0ff9eb" dependencies = [ "egui", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7adf940b8..550e1f21f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,4 +126,4 @@ hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0 # Docking trial: craft-ui from the craft-libs `docking` branch. [patch."https://github.com/storytold/craft-libs"] -craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "13be52fbaa2c4936c8d7b3d2b5abe6ab72892123" } +craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "352a8308c5cc53c2ae31326fa38a4a6bcb0ff9eb" } diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 0528cfdd6..329550e5f 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -217,7 +217,7 @@ mod visual_regressions { harness.run_steps(4); let stem = format!("designcraft-dock-{}-{width}-{scale}x", brightness.id()); harness.render().unwrap().save(directory.join(format!("{stem}.png"))).unwrap(); - let tab = egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string())); + let tab = crate::panel_docking::tab_id("properties"); let tab = harness.ctx.read_response(tab).unwrap().rect; let region = serde_json::json!({ "screen": [width, 640.0], "scale": scale, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 6ab3aa31b..1423ae884 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -4002,9 +4002,9 @@ mod tests { } }; // The docked panel's tab and a floating panel group. - let tab = egui::Id::new("designcraft-panel-docking").with(("tab", &"preflight".to_string())); + let tab = crate::panel_docking::tab_id("preflight"); let docked = || ctx.read_response(tab).is_some(); - let group = egui::LayerId::new(egui::Order::Middle, egui::Id::new("designcraft-panel-docking").with(("floating", "preflight"))); + let group = crate::panel_docking::floating_layer("preflight"); let shown = |layer: egui::LayerId| ctx.memory(|m| m.areas().is_visible(&layer)); let toggles = [ ("window.hidePanels", json!({}), json!({})), diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 010aa9224..b7439f29c 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -19,6 +19,25 @@ fn area_id() -> egui::Id { /// The dock area a headless app floats into, before it ever drew a frame. const HEADLESS_AREA: egui::Rect = egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1400.0, 900.0)); +/// The egui id of a docking control, for tests that press it or read its response. +#[cfg(test)] +pub(crate) fn hit_id(hit: &craft_ui::docking::Hit) -> egui::Id { + craft_ui::docking::DockArea::::hit_id(area_id(), hit) +} + +/// The egui id of a panel's tab. +#[cfg(test)] +pub(crate) fn tab_id(panel: &str) -> egui::Id { + hit_id(&craft_ui::docking::Hit::Tab(panel.to_owned())) +} + +/// The egui layer of the floating window whose first panel is `panel`. +#[cfg(test)] +pub(crate) fn floating_layer(panel: &str) -> egui::LayerId { + let window = Floating::single(vec![panel.to_owned()], 0, [0.0, 0.0, 1.0, 1.0]); + craft_ui::docking::DockArea::::floating_layer(area_id(), &window).unwrap_or_else(egui::LayerId::background) +} + pub(crate) fn cancel_gesture(ctx: &egui::Context) { craft_ui::docking::cancel_drag::(ctx, area_id()); } @@ -486,7 +505,7 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result for PanelContent<'_> { fn bare(&self, id: &String) -> bool { id == CANVAS } - fn group_header_width(&self, _: &[String], _: &String, _: bool) -> f32 { + fn group_header_width(&self, _: &[String], _: &String, _: bool, _: f32) -> f32 { 24.0 } fn group_header(&mut self, ui: &mut egui::Ui, _: &[String], active: &String, _: bool) { diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 41f08a20c..b4031201d 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -87,8 +87,8 @@ fn ordinary_panel_tab_drag_enters_shared_docking_and_can_redock_into_another_tab drag(&mut h, source, egui::pos2(160.0, 140.0)); let layout = h.state().ui.docking.as_ref().expect("ordinary tab drag starts shared docking"); assert!(layout.floating.iter().flat_map(|w| &w.groups).any(|g| g.panels.contains(&"layers".into())), "{layout:?}"); - let source = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"layers".to_string()))).unwrap().rect.center(); - let destination = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string()))).unwrap().rect.center(); + let source = h.ctx.read_response(tab_id("layers")).unwrap().rect.center(); + let destination = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); drag(&mut h, source, destination); let layout = h.state().ui.docking.as_ref().unwrap(); assert!(!layout.floating.iter().flat_map(|w| &w.groups).any(|g| g.panels.contains(&"layers".into()))); @@ -150,7 +150,7 @@ fn capture_custom_panel_docking_visual_fixtures() { .save(directory.join(format!("designcraft-shared-docking-{}-{language}-{width}-{scale}x.png", theme.id()))) .unwrap(); if language == "ar" { - let tab = egui::Id::new("designcraft-panel-docking").with(("tab", &"swatches".to_string())); + let tab = tab_id("swatches"); let at = harness.ctx.read_response(tab).unwrap().rect.center(); harness.input_mut().events.extend([ Event::PointerMoved(at), @@ -277,8 +277,8 @@ fn populated_canvas_keeps_a_real_stroke_edit_after_floating_and_redocking() { std::fs::create_dir_all(directory).unwrap(); h.render().unwrap().save(directory.join("designcraft-populated-stroke-floating.png")).unwrap(); } - let from = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"stroke".to_string()))).unwrap().rect.center(); - let to = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string()))).unwrap().rect.center(); + let from = h.ctx.read_response(tab_id("stroke")).unwrap().rect.center(); + let to = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); drag(&mut h, from, to); assert!(h.state().ui.docking.as_ref().unwrap().floating.is_empty()); assert_eq!(h.state().session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().stroke.join, designcraft_doc::Join::Bevel); @@ -439,15 +439,14 @@ fn modal_overlay_blocks_a_real_tab_drag_release() { let mut app = app(); app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); let mut h = harness(app, vec2(1280.0, 800.0)); - let area = egui::Id::new("designcraft-panel-docking"); - let source = h.ctx.read_response(area.with(("tab", &"properties".to_string()))).unwrap().rect.center(); + let source = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); h.event(Event::PointerMoved(source)); h.run_steps(1); h.event(Event::PointerButton { pos: source, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); h.run_steps(1); h.event(Event::PointerMoved(source - vec2(90.0, 0.0))); h.run_steps(2); - assert_eq!(h.ctx.dragged_id(), Some(area.with(("tab", &"properties".to_string()))), "test must start the owned tab gesture"); + assert_eq!(h.ctx.dragged_id(), Some(tab_id("properties")), "test must start the owned tab gesture"); let before = h.state().ui.docking.clone(); h.state_mut().ui.dialog = Some(crate::dialogs::Dialog::new("newDocument", json!({}))); let destination = egui::pos2(150.0, 150.0); @@ -489,7 +488,7 @@ fn first_story_editor_frame_conservatively_blocks_owned_tab_release() { app.run("window.panel.float", json!({"panel":"layers"})).unwrap(); let doc = serde_json::to_value(&app.session.active().unwrap().doc).unwrap(); let mut h = crate::test_window::open(app, vec2(1440.0, 900.0)); - let tab = egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string())); + let tab = tab_id("properties"); let from = h.ctx.read_response(tab).unwrap().rect.center(); h.event(Event::PointerMoved(from)); h.run_steps(1); @@ -522,7 +521,7 @@ fn first_story_editor_frame_conservatively_blocks_owned_tab_release() { .find(|p| !editor.contains(*p)) .expect("fixture has space outside editor"); let before_outside = h.state().app.ui.docking.clone(); - let tab = egui::Id::new("designcraft-panel-docking").with(("tab", &"layers".to_string())); + let tab = tab_id("layers"); let from = h.ctx.read_response(tab).unwrap().rect.center(); h.event(Event::PointerMoved(from)); h.run_steps(1); @@ -729,7 +728,7 @@ fn group_with<'a>(layout: &'a Layout, panel: &str) -> (bool, &'a [String } fn drag_bar(h: &Harness<'static, DesignApp>, first: &str) -> egui::Rect { - h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("drag-bar", Some(&first.to_string())))).expect("the group's drag bar").rect + h.ctx.read_response(hit_id(&craft_ui::docking::Hit::DragBar(first.to_string()))).expect("the group's drag bar").rect } /// The docked Character Styles | Paragraph Styles group moves as one by its drag bar: dropped on @@ -752,7 +751,7 @@ fn dragging_a_docked_group_by_its_bar_moves_every_tab() { assert!(valid(layout)); let bar = drag_bar(&h, "characterStyles"); - let properties = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string()))).unwrap().rect.center(); + let properties = h.ctx.read_response(tab_id("properties")).unwrap().rect.center(); drag(&mut h, bar.center(), properties); let layout = h.state().ui.docking.as_ref().unwrap(); let (floating, panels, active) = group_with(layout, "characterStyles"); @@ -799,9 +798,7 @@ fn default_workspaces_are_the_canvas_beside_an_end_dock() { #[test] fn the_dock_is_at_the_right_in_english_and_the_left_in_arabic() { - let tab = |h: &Harness<'static, DesignApp>| { - h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"properties".to_string()))).map(|r| r.rect).unwrap() - }; + let tab = |h: &Harness<'static, DesignApp>| h.ctx.read_response(tab_id("properties")).map(|r| r.rect).unwrap(); let h = harness(app(), vec2(1280.0, 800.0)); assert!(tab(&h).left() > 640.0, "{:?}", tab(&h)); let mut arabic = app(); @@ -829,7 +826,7 @@ fn ui_docking_reports_the_layout_and_the_focused_panel() { assert!(result["focused"].is_null()); let layout: Layout = serde_json::from_value(result["layout"].clone()).unwrap(); assert_eq!(layout, default_layout(&h.state().ui)); - let at = h.ctx.read_response(egui::Id::new("designcraft-panel-docking").with(("tab", &"layers".to_string()))).unwrap().rect.center(); + let at = h.ctx.read_response(tab_id("layers")).unwrap().rect.center(); click(&mut h, at, PointerButton::Primary); let v = docking(h.state_mut()); assert_eq!(v["result"]["focused"], "layers", "{v}"); @@ -877,8 +874,37 @@ fn moving_or_closing_the_focused_panel_by_command_updates_the_panel_state() { assert_eq!(app.ui.docking_raise, None); } +/// A divider dragged with the pointer gives the keyboard back: the next arrow key nudges the +/// selection and leaves the dock's size alone. +#[test] +fn arrow_after_dragging_a_dock_divider_nudges_the_selection_not_the_dock() { + let mut app = app(); + app.run("file.new", json!({"width":480,"height":360})).unwrap(); + let id = app.run("frame.create", json!({"rect":[80,70,330,230],"content":"none"})).unwrap()["id"].as_u64().unwrap(); + let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state( + |ui, app: &mut DesignApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(5); + let column = craft_ui::docking::Hit::Divider(craft_ui::docking::Divider::Column("properties".to_string())); + let divider = hits(&h).into_iter().find_map(|(hit, rect)| (hit == column).then_some(rect)).expect("the main dock column's divider"); + drag(&mut h, divider.center(), divider.center() - vec2(40.0, 0.0)); + let resized = h.state().ui.docking.clone().expect("the drag customizes the layout"); + let bounds = |h: &Harness<'static, DesignApp>| h.state().session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().bounds(); + let before = bounds(&h); + h.event(Event::Key { key: egui::Key::ArrowRight, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }); + h.run_steps(1); + h.event(Event::Key { key: egui::Key::ArrowRight, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE }); + h.run_steps(2); + assert!(bounds(&h).x0 > before.x0, "the arrow nudges the selection"); + assert_eq!(h.state().ui.docking.as_ref(), Some(&resized), "the arrow doesn't resize the dock"); +} + fn hits(h: &Harness<'static, DesignApp>) -> Vec<(craft_ui::docking::Hit, egui::Rect)> { - craft_ui::docking::DockArea::::hit_rects(&h.ctx, egui::Id::new("designcraft-panel-docking")) + craft_ui::docking::DockArea::::hit_rects(&h.ctx, area_id()) } /// `window.panel.*` ids and DesignCraft's aliases go through craft-ui's `DockCommand`. @@ -929,6 +955,10 @@ fn shared_panel_commands_float_dock_and_reject_atomically() { assert!( matches!(&app.ui.docking.as_ref().unwrap().root, Some(Node::Split { first, .. }) if matches!(&**first, Node::Dock { side: Side::Start, .. })) ); + // Docking it at that side again leaves it where it is. + let before = app.ui.docking.clone(); + app.run("window.panel.dock", json!({"panel": "stroke", "side": "Start"})).unwrap(); + assert_eq!(app.ui.docking, before, "it already leads the start dock's last column"); // Close Others and Minimize are shared commands too. app.run("window.panel.closeOthers", json!({"panel": "pages"})).unwrap(); assert_eq!(tab_group(app.ui.docking.as_ref().unwrap(), "pages"), ["pages"]);