mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 14:16:49 -06:00
Docking trial: craft-ui docking branch, group drag by the drag bar
Patch craft-ui to aiongg/craft-libs 4bf1164 (docking branch). Floating windows hold groups (Floating::single, panels()); matches on Node and Action get wildcard arms; apply_action validates MoveGroup, MoveColumn, MoveWindow, the group close actions, minimize and resize, and records return locations for every panel a close action removes. PanelContent gives preferred floating sizes. A pointer test drags the docked Character Styles | Paragraph Styles group by its drag bar to the canvas and onto another strip: order and the front tab are kept.
This commit is contained in:
Generated
+1
-1
@@ -849,7 +849,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51eeb3de70e735c8ce87a59a7dedd47675"
|
||||
source = "git+https://github.com/aiongg/craft-libs?rev=4bf11647adb05e1b6f5a49c1ae5f7105bf746c71#4bf11647adb05e1b6f5a49c1ae5f7105bf746c71"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
|
||||
@@ -123,3 +123,7 @@ hayro = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e
|
||||
hayro-interpret = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
hayro-syntax = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" }
|
||||
|
||||
# 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 = "4bf11647adb05e1b6f5a49c1ae5f7105bf746c71" }
|
||||
|
||||
@@ -5059,7 +5059,7 @@ mod tests {
|
||||
// Paragraph Styles opens floating, grouped with Character Styles, as in InDesign.
|
||||
activate(&mut app, "app.paragraphStylesPanel", &Value::Null);
|
||||
let layout = app.ui.docking.as_ref().unwrap();
|
||||
assert!(layout.floating.iter().any(|g| g.panels == ["characterStyles", "paragraphStyles"] && g.active == 1));
|
||||
assert!(layout.floating.iter().flat_map(|w| &w.groups).any(|g| g.panels == ["characterStyles", "paragraphStyles"] && g.active == 1));
|
||||
let story = app.run("frame.create", json!({"rect": [36, 36, 200, 100], "content": "text", "text": "Page "})).unwrap()["story"].clone();
|
||||
app.run("text.select", json!({"story": story, "anchor": 5, "focus": 5})).unwrap();
|
||||
activate(&mut app, "app.insertCurrentPageNumber", &Value::Null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Application-owned panel IDs and commands around the shared docking layout.
|
||||
|
||||
use craft_ui::docking::{Action, Floating, Layout, Node, Placement, Zone};
|
||||
use craft_ui::docking::{Action, Floating, Layout, Node, Placement, Target, Zone};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::DesignApp;
|
||||
@@ -53,7 +53,7 @@ fn legacy(app: &DesignApp) -> Layout<String> {
|
||||
let mut layout = Layout { root: Some(Node::Tabs { panels, active }), floating: Vec::new() };
|
||||
for (id, at) in &app.ui.floating {
|
||||
if normalize(id).is_some() && !layout.contains(id) && at.iter().all(|v| v.is_finite()) {
|
||||
layout.floating.push(Floating { panels: vec![id.clone()], active: 0, rect: [at[0], at[1], 280.0, 360.0] });
|
||||
layout.floating.push(Floating::single(vec![id.clone()], 0, [at[0], at[1], 280.0, 360.0]));
|
||||
}
|
||||
}
|
||||
if let Some(id) = &app.ui.open_panel
|
||||
@@ -72,7 +72,7 @@ fn legacy(app: &DesignApp) -> Layout<String> {
|
||||
/// The panels tabbed with `panel`: its floating group, or its tab group in the dock. A panel in
|
||||
/// an accordion, or not in the layout, is a group of its own.
|
||||
fn tab_group(layout: &Layout<String>, panel: &str) -> Vec<String> {
|
||||
if let Some(group) = layout.floating.iter().find(|g| g.panels.iter().any(|p| p == panel)) {
|
||||
if let Some(group) = layout.floating.iter().flat_map(|w| &w.groups).find(|g| g.panels.iter().any(|p| p == panel)) {
|
||||
return group.panels.clone();
|
||||
}
|
||||
let mut visit: Vec<&Node<String>> = layout.root.iter().collect();
|
||||
@@ -88,6 +88,11 @@ fn tab_group(layout: &Layout<String>, panel: &str) -> Vec<String> {
|
||||
visit.push(second);
|
||||
}
|
||||
Node::Tabs { panels, .. } if panels.iter().any(|p| p == panel) => return panels.clone(),
|
||||
Node::Dock { columns, .. } => {
|
||||
if let Some(group) = columns.iter().flat_map(|c| &c.groups).find(|g| g.panels.iter().any(|p| p == panel)) {
|
||||
return group.panels.clone();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +118,7 @@ fn open_in_stack(layout: &Layout<String>, panel: &str) -> bool {
|
||||
return e.open;
|
||||
}
|
||||
}
|
||||
Node::Tabs { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -216,6 +221,29 @@ fn apply_action(app: &mut DesignApp, action: Action<String>) -> Result<Value, St
|
||||
| Action::SetStackOpen { panel, .. }
|
||||
| Action::ResizeStack { panel, .. } => registered(panel),
|
||||
Action::ResizeSplit { path, .. } => path.len() <= craft_ui::docking::MAX_DEPTH,
|
||||
Action::MoveGroup { panel, to } | Action::MoveColumn { panel, to } | Action::MoveWindow { panel, to } => {
|
||||
registered(panel)
|
||||
&& match to {
|
||||
Target::At { anchor, placement } => {
|
||||
registered(anchor)
|
||||
&& match placement {
|
||||
Placement::Tab { before: Some(id) } => registered(id),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
Action::CloseGroup { panel }
|
||||
| Action::CloseWindow { panel }
|
||||
| Action::CloseOthers { panel }
|
||||
| Action::SetMinimized { panel, .. }
|
||||
| Action::SetIconic { panel, .. }
|
||||
| Action::ResizeColumn { panel, .. }
|
||||
| Action::ResizeGroup { panel, .. }
|
||||
| Action::ResizeIconic { panel, .. } => registered(panel),
|
||||
// Native OS windows are not enabled in DesignCraft; newer actions are rejected.
|
||||
_ => false,
|
||||
};
|
||||
if !known {
|
||||
return Err("docking action contains an unknown panel or an excessive split path".into());
|
||||
@@ -224,19 +252,24 @@ fn apply_action(app: &mut DesignApp, action: Action<String>) -> Result<Value, St
|
||||
return Err("saved panel layout is invalid; reset the workspace first".into());
|
||||
}
|
||||
let mut layout = app.ui.docking.clone().unwrap_or_else(|| legacy(app));
|
||||
let returning = match &action {
|
||||
Action::Close { panel } => layout.location(panel).ok().map(|location| (panel.clone(), location)),
|
||||
// Window › a closed panel, and Dock Panel after a float, bring it back where it was.
|
||||
let returning: Vec<_> = match &action {
|
||||
Action::Float { panel, .. } => {
|
||||
layout.location(panel).ok().filter(|location| location.floating.is_none()).map(|location| (panel.clone(), location))
|
||||
layout.location(panel).ok().filter(|location| location.floating.is_none()).map(|location| (panel.clone(), location)).into_iter().collect()
|
||||
}
|
||||
_ => None,
|
||||
_ => layout
|
||||
.closed_by(&action)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|panel| layout.location(&panel).ok().map(|location| (panel, location)))
|
||||
.collect(),
|
||||
};
|
||||
let reveal = match &action {
|
||||
Action::Activate { panel } | Action::Open { panel, .. } | Action::OpenAt { panel, .. } => Some(panel.clone()),
|
||||
_ => None,
|
||||
};
|
||||
layout.apply(action).map_err(|error| error.to_string())?;
|
||||
if let Some((panel, location)) = returning {
|
||||
for (panel, location) in returning {
|
||||
app.ui.docking_hidden.insert(panel, location);
|
||||
}
|
||||
app.ui.docking = Some(layout);
|
||||
@@ -266,7 +299,8 @@ fn front_tabs(layout: &Layout<String>) -> Vec<String> {
|
||||
visit.push(second);
|
||||
}
|
||||
Node::Tabs { panels, active } => out.extend(panels.get(*active).cloned()),
|
||||
Node::Stack { .. } => {}
|
||||
Node::Dock { columns, .. } => out.extend(columns.iter().flat_map(|c| &c.groups).filter_map(|g| g.active_panel().cloned())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -291,7 +325,14 @@ fn restore_front_tabs(layout: &mut Layout<String>, front: &[String]) {
|
||||
*active = i;
|
||||
}
|
||||
}
|
||||
Node::Stack { .. } => {}
|
||||
Node::Dock { columns, .. } => {
|
||||
for group in columns.iter_mut().flat_map(|c| &mut c.groups) {
|
||||
if let Some(i) = group.panels.iter().position(|p| front.contains(p)) {
|
||||
group.active = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,7 +564,9 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) -> bool {
|
||||
app.ui.status = error.to_string();
|
||||
}
|
||||
if ui.input(|input| input.pointer.any_released()) {
|
||||
if !output.actions.iter().any(|action| matches!(action, Action::Move { .. } | Action::Float { .. })) && cancel_legacy_drag(app, ui.ctx()) {
|
||||
if !output.actions.iter().any(|action| matches!(action, Action::Move { .. } | Action::Float { .. } | Action::MoveGroup { .. }))
|
||||
&& cancel_legacy_drag(app, ui.ctx())
|
||||
{
|
||||
return app.ui.docking.is_some();
|
||||
}
|
||||
ui.ctx().data_mut(|data| data.remove::<LegacyDrag>(egui::Id::new("designcraft-panel-docking").with("legacy-origin")));
|
||||
@@ -548,7 +591,7 @@ fn icon_rail(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
for &(id, label, icon) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) {
|
||||
if app.ui.docking.as_ref().is_some_and(|layout| {
|
||||
layout.contains(&id.to_string()) && (app.ui.dock_expanded || layout.floating.iter().any(|g| g.panels.iter().any(|p| p == id)))
|
||||
layout.contains(&id.to_string()) && (app.ui.dock_expanded || layout.floating.iter().any(|g| g.panels().any(|p| p == id)))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
@@ -652,6 +695,16 @@ pub(crate) fn panel_menu(app: &mut DesignApp, ui: &mut egui::Ui, panel: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The size a panel floats at (tab strip included), whatever the size of the dock it left.
|
||||
fn floating_size(panel: &str) -> egui::Vec2 {
|
||||
match panel {
|
||||
"properties" | "pages" | "layers" => egui::vec2(300.0, 420.0),
|
||||
"characterStyles" | "paragraphStyles" => egui::vec2(290.0, 340.0),
|
||||
"glyphs" | "library" | "links" | "preflight" | "dataMerge" => egui::vec2(320.0, 400.0),
|
||||
_ => egui::vec2(280.0, 360.0),
|
||||
}
|
||||
}
|
||||
|
||||
struct PanelContent<'a>(&'a mut DesignApp);
|
||||
impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
fn body(&mut self, ui: &mut egui::Ui, id: &String) {
|
||||
@@ -674,6 +727,9 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
fn floating_size(&self, id: &String) -> Option<egui::Vec2> {
|
||||
Some(floating_size(id))
|
||||
}
|
||||
fn group_header_width(&self, _: &[String], _: &String, _: bool) -> f32 {
|
||||
24.0
|
||||
}
|
||||
@@ -750,7 +806,7 @@ fn sync_legacy(app: &mut DesignApp) {
|
||||
app.ui.floating = layout
|
||||
.floating
|
||||
.iter()
|
||||
.flat_map(|group| group.panels.iter().map(|id| (id.clone(), [group.rect[0], group.rect[1]])))
|
||||
.flat_map(|window| window.panels().map(|id| (id.clone(), [window.rect[0], window.rect[1]])))
|
||||
.chain(float_only)
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ fn panel_commands_group_split_reorder_float_and_restore_without_duplicate_owners
|
||||
app.run("window.panel.move", json!({"panel":"properties", "anchor":"swatches", "before":"swatches"})).unwrap();
|
||||
let before_float = app.ui.docking.clone().unwrap();
|
||||
app.run("window.panel.float", json!({"panel":"properties", "x":50,"y":70,"width":330,"height":410})).unwrap();
|
||||
assert!(app.ui.docking.as_ref().unwrap().floating.iter().any(|g| g.panels == ["properties"]));
|
||||
assert!(app.ui.docking.as_ref().unwrap().floating.iter().flat_map(|w| &w.groups).any(|g| g.panels == ["properties"]));
|
||||
app.run("window.panel.dock", json!({"panel":"properties"})).unwrap();
|
||||
assert_eq!(app.ui.docking.as_ref().unwrap(), &before_float);
|
||||
app.run("window.panel.close", json!({"panel":"properties"})).unwrap();
|
||||
@@ -87,12 +87,12 @@ fn ordinary_panel_tab_drag_enters_shared_docking_and_can_redock_into_another_tab
|
||||
let source = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
|
||||
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().any(|g| g.panels.contains(&"layers".into())), "{layout:?}");
|
||||
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();
|
||||
drag(&mut h, source, destination);
|
||||
let layout = h.state().ui.docking.as_ref().unwrap();
|
||||
assert!(!layout.floating.iter().any(|g| g.panels.contains(&"layers".into())));
|
||||
assert!(!layout.floating.iter().flat_map(|w| &w.groups).any(|g| g.panels.contains(&"layers".into())));
|
||||
assert!(valid(layout));
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ fn serialized_layout_actions_cover_geometry_reorder_accordion_and_atomic_rejecti
|
||||
app.run("window.panel.float", json!({"panel":"properties"})).unwrap();
|
||||
app.run("window.panel.layout", json!({"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}})).unwrap();
|
||||
assert_eq!(
|
||||
app.ui.docking.as_ref().unwrap().floating.iter().find(|group| group.panels.iter().any(|p| p == "properties")).unwrap().rect,
|
||||
app.ui.docking.as_ref().unwrap().floating.iter().find(|window| window.panels().any(|p| p == "properties")).unwrap().rect,
|
||||
[44.0, 66.0, 360.0, 450.0]
|
||||
);
|
||||
app.run("window.panel.layout", json!({"action":{"Move":{"panel":"properties","anchor":"swatches","placement":{"Tab":{"before":"swatches"}}}}}))
|
||||
@@ -712,7 +712,7 @@ fn paragraph_styles_open_floating_with_character_styles() {
|
||||
let mut app = app();
|
||||
app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap();
|
||||
let layout = app.ui.docking.clone().unwrap();
|
||||
let group = layout.floating.iter().find(|g| g.panels.iter().any(|p| p == "paragraphStyles")).expect("a floating group");
|
||||
let group = layout.floating.iter().flat_map(|w| &w.groups).find(|g| g.panels.iter().any(|p| p == "paragraphStyles")).expect("a floating group");
|
||||
assert_eq!(group.panels, ["characterStyles", "paragraphStyles"]);
|
||||
assert_eq!(group.panels[group.active], "paragraphStyles", "the one chosen is in front");
|
||||
// The dock still shows Properties in front.
|
||||
@@ -721,11 +721,11 @@ fn paragraph_styles_open_floating_with_character_styles() {
|
||||
app.run("window.panel", json!({"panel":"color"})).unwrap();
|
||||
app.run("window.panel", json!({"panel":"links"})).unwrap();
|
||||
let layout = app.ui.docking.clone().unwrap();
|
||||
assert!(layout.floating.iter().any(|g| g.panels == ["characterStyles", "paragraphStyles"]));
|
||||
assert!(layout.floating.iter().flat_map(|w| &w.groups).any(|g| g.panels == ["characterStyles", "paragraphStyles"]));
|
||||
// Once showing, Window › Character Styles brings that tab forward.
|
||||
app.run("window.panel", json!({"panel":"characterStyles"})).unwrap();
|
||||
let layout = app.ui.docking.clone().unwrap();
|
||||
let group = layout.floating.iter().find(|g| g.panels.iter().any(|p| p == "characterStyles")).unwrap();
|
||||
let group = layout.floating.iter().flat_map(|w| &w.groups).find(|g| g.panels.iter().any(|p| p == "characterStyles")).unwrap();
|
||||
assert_eq!(group.panels[group.active], "characterStyles");
|
||||
assert_eq!(layout.panels().iter().filter(|p| p.as_str() == "characterStyles").count(), 1);
|
||||
}
|
||||
@@ -744,7 +744,7 @@ fn paragraph_styles_closed_from_the_dock_reopen_there() {
|
||||
app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap();
|
||||
let layout = app.ui.docking.clone().unwrap();
|
||||
assert!(layout.contains(&"paragraphStyles".to_string()));
|
||||
assert!(layout.floating.iter().all(|g| !g.panels.iter().any(|p| p == "paragraphStyles")), "{:?}", layout.floating);
|
||||
assert!(layout.floating.iter().flat_map(|w| &w.groups).all(|g| !g.panels.iter().any(|p| p == "paragraphStyles")), "{:?}", layout.floating);
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'static, DesignApp>, at: Pos2, button: PointerButton) {
|
||||
@@ -787,3 +787,53 @@ fn pages_keeps_its_panel_menu_in_the_docking_layout() {
|
||||
assert!(h.query_by_label("Close Panel").is_none());
|
||||
assert!(h.query_by_label("Panel Options…").is_none(), "the panel's own commands stay in its panel menu");
|
||||
}
|
||||
|
||||
fn group_with<'a>(layout: &'a Layout<String>, panel: &str) -> (bool, &'a [String], usize) {
|
||||
if let Some(g) = layout.floating.iter().flat_map(|w| &w.groups).find(|g| g.panels.iter().any(|p| p == panel)) {
|
||||
return (true, &g.panels, g.active);
|
||||
}
|
||||
let mut visit: Vec<&Node<String>> = layout.root.iter().collect();
|
||||
while let Some(node) = visit.pop() {
|
||||
match node {
|
||||
Node::Split { first, second, .. } => visit.extend([&**first, &**second]),
|
||||
Node::Tabs { panels, active } if panels.iter().any(|p| p == panel) => return (false, panels, *active),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
panic!("{panel} is not in {layout:?}");
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// The docked Character Styles | Paragraph Styles group moves as one by its drag bar: dropped on
|
||||
/// the canvas it floats, dropped on another strip it joins as tabs. Order and the tab in front
|
||||
/// are kept.
|
||||
#[test]
|
||||
fn dragging_a_docked_group_by_its_bar_moves_every_tab() {
|
||||
let mut app = app();
|
||||
app.run("window.panel", json!({"panel":"paragraphStyles"})).unwrap();
|
||||
let below = json!({"MoveGroup": {"panel": "characterStyles", "to": {"At": {"anchor": "properties", "placement": {"Split": "Bottom"}}}}});
|
||||
app.run("window.panel.layout", json!({"action": below})).unwrap();
|
||||
let styles = ["characterStyles", "paragraphStyles"];
|
||||
assert_eq!(group_with(app.ui.docking.as_ref().unwrap(), "characterStyles"), (false, &styles.map(String::from)[..], 1));
|
||||
|
||||
let mut h = harness(app, vec2(1280.0, 800.0));
|
||||
let bar = drag_bar(&h, "characterStyles");
|
||||
drag(&mut h, bar.center(), egui::pos2(400.0, 400.0));
|
||||
let layout = h.state().ui.docking.as_ref().unwrap();
|
||||
assert_eq!(group_with(layout, "paragraphStyles"), (true, &styles.map(String::from)[..], 1), "{layout:?}");
|
||||
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();
|
||||
drag(&mut h, bar.center(), properties);
|
||||
let layout = h.state().ui.docking.as_ref().unwrap();
|
||||
let (floating, panels, active) = group_with(layout, "characterStyles");
|
||||
assert!(!floating, "{layout:?}");
|
||||
let at = panels.iter().position(|p| p == "characterStyles").unwrap();
|
||||
assert_eq!(panels.get(at + 1).map(String::as_str), Some("paragraphStyles"), "{panels:?}");
|
||||
assert_eq!(panels[active], "paragraphStyles");
|
||||
assert!(layout.floating.is_empty(), "{:?}", layout.floating);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user