#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.
This commit is contained in:
aiongg
2026-10-20 18:01:07 +00:00
parent 0d6fe6bff7
commit 4239e1b0c2
6 changed files with 77 additions and 28 deletions
Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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" }
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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!({})),
+25 -6
View File
@@ -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<String>) -> egui::Id {
craft_ui::docking::DockArea::<String>::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::<String>::floating_layer(area_id(), &window).unwrap_or_else(egui::LayerId::background)
}
pub(crate) fn cancel_gesture(ctx: &egui::Context) {
craft_ui::docking::cancel_drag::<String>(ctx, area_id());
}
@@ -486,7 +505,7 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result<Value
let mut shown = false;
if let Some(panel) = shows.as_ref().filter(|p| !txn.workspace.layout.contains(p)) {
let fallback = first_dock_panel(&txn.workspace.layout, panel);
txn.workspace.set_visible(panel.clone(), true, fallback).map_err(|e| e.to_string())?;
txn.workspace.set_visible(panel.clone(), true, fallback, model_permissions).map_err(|e| e.to_string())?;
shown = true;
}
let returned = match &command {
@@ -895,19 +914,19 @@ const DOCK_LABELS: &[(&str, &str)] = &[
("dock.panels", "Panels"),
("dock.resize", "Resize panels"),
("dock.resizeWindow", "Resize panel window"),
("dock.moveGroup", "Move Tab Group"),
("dock.moveGroup", "Move tab group"),
("dock.closeGroup", "Close Tab Group"),
("dock.minimize", "Minimize"),
("dock.expand", "Expand"),
("dock.floatGroup", "Float Tab Group"),
("dock.moveGroupTo", "Group Tab Group with"),
("dock.moveColumn", "Move Panel Column"),
("dock.moveColumn", "Move panel column"),
("dock.collapseToIcons", "Collapse to Icons"),
("dock.expandPanels", "Expand Panels"),
("dock.collapseColumn", "Collapse to Icons"),
("dock.expandColumn", "Expand Panels"),
("dock.moveWindow", "Move Panel Window"),
("dock.closeWindow", "Close Panel Window"),
("dock.moveWindow", "Move panel window"),
("dock.closeWindow", "Close panel window"),
("dock.undock", "Undock Panel"),
("dock.undockGroup", "Undock Panel Group"),
("dock.closePanelGroup", "Close Panel Group"),
@@ -1032,7 +1051,7 @@ impl craft_ui::docking::DockContent<String> 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) {
+47 -17
View File
@@ -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<String>, 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<String> = 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<String>, egui::Rect)> {
craft_ui::docking::DockArea::<String>::hit_rects(&h.ctx, egui::Id::new("designcraft-panel-docking"))
craft_ui::docking::DockArea::<String>::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"]);