mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
Docking trial: craft-ui 13be52f, clear stale opened panel
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/aiongg/craft-libs?rev=bfda13d2afd92947f2fe0f671385d350b4d5932a#bfda13d2afd92947f2fe0f671385d350b4d5932a"
|
||||
source = "git+https://github.com/aiongg/craft-libs?rev=13be52fbaa2c4936c8d7b3d2b5abe6ab72892123#13be52fbaa2c4936c8d7b3d2b5abe6ab72892123"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user