Docking trial: craft-ui 13be52f, clear stale opened panel

This commit is contained in:
aiongg
2026-10-19 14:16:53 +00:00
parent 37e80a4ae2
commit 67cba4b364
6 changed files with 120 additions and 6 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=bfda13d2afd92947f2fe0f671385d350b4d5932a#bfda13d2afd92947f2fe0f671385d350b4d5932a"
source = "git+https://github.com/aiongg/craft-libs?rev=13be52fbaa2c4936c8d7b3d2b5abe6ab72892123#13be52fbaa2c4936c8d7b3d2b5abe6ab72892123"
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 = "bfda13d2afd92947f2fe0f671385d350b4d5932a" }
craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "13be52fbaa2c4936c8d7b3d2b5abe6ab72892123" }
+2 -1
View File
@@ -322,7 +322,8 @@ pub struct UiState {
pub docking: Option<craft_ui::docking::Layout<String>>,
#[serde(default, skip_serializing_if = "HiddenPanels::is_empty")]
pub docking_hidden: HiddenPanels,
/// The panel (or the canvas) last clicked or selected in the docking layout.
/// The panel (or the canvas) last clicked or selected in the docking layout, while the panel
/// stays where the user left it.
#[serde(skip)]
pub docking_focused: Option<String>,
/// Invalidates in-flight gestures when a workspace is replaced.
+1 -3
View File
@@ -1871,9 +1871,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
}
"window.workspace" => {
app.ui.docking_generation = app.ui.docking_generation.wrapping_add(1);
app.ui.docking_raise = None;
app.ui.docking_opened = None;
app.ui.docking_flyout = None;
crate::panel_docking::forget_panels(app);
let name = p.get("name").and_then(Value::as_str).unwrap_or("Essentials");
if let Some(w) = app.ui.custom_workspaces.iter().find(|w| w.name == name).cloned() {
if let Some(hidden) = w.hidden_menu_items {
+76
View File
@@ -298,6 +298,52 @@ pub(crate) fn tab_group(layout: &Layout<String>, panel: &str) -> Vec<String> {
vec![panel.to_string()]
}
/// Where a panel is: its floating window and group, or the path to its tab group, accordion or
/// dock in the tree with the dock column and group. A different place means the panel moved.
#[derive(Debug, PartialEq)]
enum Place {
Floating { window: usize, group: usize },
Tree { path: Vec<bool>, column: usize, group: usize },
}
fn place(layout: &Layout<String>, panel: &str) -> Option<Place> {
let has = |group: &Group<String>| group.panels.iter().any(|p| p == panel);
for (window, floating) in layout.floating.iter().enumerate() {
if let Some(group) = floating.groups.iter().position(has) {
return Some(Place::Floating { window, group });
}
}
let mut visit: Vec<(&Node<String>, Vec<bool>)> = layout.root.iter().map(|node| (node, Vec::new())).collect();
let mut seen = 0usize;
while let Some((node, path)) = visit.pop() {
seen = seen.saturating_add(1);
if seen > 2 * craft_ui::docking::MAX_PANELS || path.len() > craft_ui::docking::MAX_DEPTH {
return None;
}
match node {
Node::Split { first, second, .. } => {
let mut second_path = path.clone();
second_path.push(true);
let mut first_path = path;
first_path.push(false);
visit.push((second, second_path));
visit.push((first, first_path));
}
Node::Tabs { panels, .. } if panels.iter().any(|p| p == panel) => return Some(Place::Tree { path, column: 0, group: 0 }),
Node::Stack { entries } if entries.iter().any(|e| e.panel == panel) => return Some(Place::Tree { path, column: 0, group: 0 }),
Node::Dock { columns, .. } => {
for (column, c) in columns.iter().enumerate() {
if let Some(group) = c.groups.iter().position(has) {
return Some(Place::Tree { path, column, group });
}
}
}
_ => {}
}
}
None
}
/// Is `panel` an open entry of an accordion (which Minimize closes)?
fn open_in_stack(layout: &Layout<String>, panel: &str) -> bool {
let mut visit: Vec<&Node<String>> = layout.root.iter().collect();
@@ -567,6 +613,12 @@ impl Transaction {
}
let reveal = reveal.filter(|panel| layout.contains(panel));
let flyout = reveal.as_ref().filter(|panel| in_iconic_column(&layout, panel)).cloned();
// Focus stays on a panel only while it stays where the user left it, as craft-ui's tab
// focus does: a command that moves it gives the focus up.
let before = layout_of(app);
if app.ui.docking_focused.as_deref().is_some_and(|panel| panel != CANVAS && place(&before, panel) != place(&layout, panel)) {
app.ui.docking_focused = None;
}
app.ui.docking = Some(layout);
app.ui.docking_hidden = hidden;
release_closed(app, &closed);
@@ -578,6 +630,7 @@ impl Transaction {
} else if app.ui.docking_opened.as_ref().is_some_and(|panel| !app.ui.docking.as_ref().is_some_and(|layout| layout.contains(panel))) {
app.ui.docking_opened = None;
}
release_flyout(app);
app.ui.open_panel = None;
json!({"layout": app.ui.docking, "closed": closed, "floating": floating_panels(app)})
}
@@ -701,6 +754,22 @@ fn release_closed(app: &mut DesignApp, closed: &[String]) {
}
}
/// Forget the panel focused, opened, raised or showing as a flyout: the workspace was replaced.
pub(crate) fn forget_panels(app: &mut DesignApp) {
app.ui.docking_focused = None;
app.ui.docking_opened = None;
app.ui.docking_raise = None;
app.ui.docking_flyout = None;
}
/// A flyout opens only from an iconic column; forget it once its panel is no longer in one.
fn release_flyout(app: &mut DesignApp) {
let layout = layout_of(app);
if app.ui.docking_flyout.as_deref().is_some_and(|panel| !in_iconic_column(&layout, panel)) {
app.ui.docking_flyout = None;
}
}
/// Window › Collapse to Icons collapses every dock column; `iconic: false` (the control
/// channel's `dockExpanded`) expands the outer column.
pub(crate) fn set_iconic(app: &mut DesignApp, iconic: bool) -> Result<Value, String> {
@@ -721,6 +790,7 @@ pub(crate) fn set_iconic(app: &mut DesignApp, iconic: bool) -> Result<Value, Str
app.ui.docking = Some(layout);
app.ui.dock_expanded = !iconic;
app.ui.open_panel = None;
release_flyout(app);
Ok(Value::Null)
}
@@ -736,6 +806,7 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
}
if app.ui.docking.as_ref().is_some_and(|layout| !valid(layout)) {
app.ui.docking = None;
forget_panels(app);
app.status(crate::i18n::tr(&app.ui.language, "Invalid panel layout; restored the default dock"));
}
let layout = layout_of(app);
@@ -770,11 +841,16 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
if let Some(error) = output.error {
app.ui.status = error.to_string();
}
// The user's own gesture keeps the focus on the panel it moves, while that panel stays open.
let focused = app.ui.docking_focused.clone();
for action in output.actions {
if let Err(error) = app.run("window.panel.layout", json!({"action": action})) {
app.ui.status = error;
}
}
if let Some(panel) = focused.filter(|panel| panel == CANVAS || layout_of(app).contains(panel)) {
app.ui.docking_focused = Some(panel);
}
}
/// What the control channel reports: the layout in use (craft-ui's serialized `Layout`),
+39
View File
@@ -93,6 +93,7 @@ fn ordinary_panel_tab_drag_enters_shared_docking_and_can_redock_into_another_tab
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())));
assert!(valid(layout));
assert_eq!(h.state().ui.docking_focused.as_deref(), Some("layers"), "the user's drag keeps the focus on the panel it moved");
}
#[test]
@@ -404,6 +405,8 @@ fn table_and_control_visibility_work_after_customization_without_document_edits(
app.run("window.panel", json!({"panel":"properties"})).unwrap();
assert_eq!(app.ui.docking_flyout.as_deref(), Some("properties"), "it opens as a flyout from the collapsed dock");
assert_eq!(app.ui.docking_raise.as_deref(), Some("properties"));
set_iconic(&mut app, false).unwrap();
assert_eq!(app.ui.docking_flyout, None, "expanding the column closes its flyout");
assert_eq!(serde_json::to_value(&app.session.active().unwrap().doc).unwrap(), doc);
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
}
@@ -838,6 +841,42 @@ fn ui_docking_reports_the_layout_and_the_focused_panel() {
assert_eq!(v["result"]["hidden"], json!(["layers"]));
}
/// A reset replaces the panels: none of them stays focused, opened for `ui.set closePanel` or
/// queued to raise.
#[test]
fn workspace_reset_forgets_the_focused_opened_and_raised_panel() {
let mut app = app();
app.run("window.panel.float", json!({"panel": "layers"})).unwrap();
app.ui.docking_focused = Some("layers".into());
assert_eq!(app.ui.docking_opened.as_deref(), Some("layers"));
assert_eq!(app.ui.docking_raise.as_deref(), Some("layers"));
app.run("window.workspace.reset", json!({})).unwrap();
assert_eq!(app.ui.docking_focused, None);
assert_eq!(app.ui.docking_opened, None);
assert_eq!(app.ui.docking_raise, None);
close_visible(&mut app).unwrap();
assert!(layout_of(&app).contains(&"layers".to_string()), "closePanel after a reset closes nothing");
}
/// A command that moves the focused panel takes the focus away and leaves it the panel last
/// opened; one that closes it forgets both.
#[test]
fn moving_or_closing_the_focused_panel_by_command_updates_the_panel_state() {
let mut app = app();
app.ui.docking_focused = Some("layers".into());
app.run("window.panel.activate", json!({"panel": "pages"})).unwrap();
assert_eq!(app.ui.docking_focused.as_deref(), Some("layers"), "a command that leaves Layers in place keeps its focus");
app.ui.docking_opened = Some("layers".into());
app.run("window.panel.move", json!({"panel": "layers", "anchor": "swatches", "zone": "tab"})).unwrap();
assert_eq!(app.ui.docking_focused, None);
assert_eq!(app.ui.docking_opened.as_deref(), Some("layers"));
app.ui.docking_focused = Some("layers".into());
app.run("window.panel.close", json!({"panel": "layers"})).unwrap();
assert_eq!(app.ui.docking_focused, None);
assert_eq!(app.ui.docking_opened, None);
assert_eq!(app.ui.docking_raise, None);
}
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"))
}