mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
docking: showing a panel with no known place opens it in the first dock group or at an edge (#286)
* docking: open an unanchored panel in the first dock group or at an edge * docking: an unanchored open prefers the end-side dock; an edge frame is a quarter wide * docking: drop the redundant edge resize; an edge frame is already a quarter wide * docking: an unanchored open passes over minimized groups and iconic columns
This commit is contained in:
1 parent
17744430be
commit
6fdd2efbba
6 files changed
+119
-4
No files matched your search
@@ -501,7 +501,8 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
|
||||
/// Apply the command to `workspace`, all or nothing, and return the panels it closed so
|
||||
/// the application can release their state. Showing a hidden panel restores its last
|
||||
/// location; Lock sets [`Workspace::locked`]. Application-owned commands
|
||||
/// location, and a panel never laid out opens as [`Action::Open`] without an anchor does
|
||||
/// ([`Workspace::set_visible`]); Lock sets [`Workspace::locked`]. Application-owned commands
|
||||
/// ([`Self::is_app_owned`]) return [`DockError::NotAllowed`] and change nothing.
|
||||
/// `space` is where `window.panel.float` floats, as for [`Self::to_actions`].
|
||||
pub fn apply(
|
||||
|
||||
@@ -420,3 +420,27 @@ fn dock_to_the_side_a_panel_already_leads_the_last_column_of() {
|
||||
// The whole column already is the last one: nothing to do.
|
||||
assert_eq!(actions(json!({"command": "window.panel.dock", "panel": "layers", "column": true, "side": "Start"})), vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn showing_a_panel_never_laid_out_opens_it_in_the_first_dock_group_or_at_an_edge() {
|
||||
// "brushes" is in neither the layout nor `hidden`.
|
||||
for params in [
|
||||
json!({"command": "window.panel.activate", "panel": "brushes"}),
|
||||
json!({"command": "window.panel.dock", "panel": "brushes"}),
|
||||
json!({"command": "ui.dock", "operation": "open", "panel": "brushes"}),
|
||||
] {
|
||||
let (shown, _) = applied(params.clone());
|
||||
assert!(
|
||||
matches!(shown.layout.place(&s("brushes")), Some(dock_model::Place::Dock { column: 0, group: 0, .. })),
|
||||
"{params}: {:?}",
|
||||
shown.layout
|
||||
);
|
||||
assert_eq!(shown.layout.panel_state(&s("brushes")), PanelState::Front, "{params}");
|
||||
}
|
||||
// No dock, and the only frame is a protected canvas: a new frame at the edge, not a tab.
|
||||
let mut workspace = Workspace::new(Layout { root: Some(Node::Tabs { panels: vec![s("canvas")], active: 0 }), floating: Vec::new() });
|
||||
let command = parse(json!({"command": "window.panel.activate", "panel": "brushes"})).unwrap();
|
||||
command.apply(&mut workspace, false, &space(), permissions).unwrap();
|
||||
assert!(matches!(workspace.layout.place(&s("brushes")), Some(dock_model::Place::Node(_))), "{:?}", workspace.layout);
|
||||
assert_ne!(workspace.layout.place(&s("brushes")), workspace.layout.place(&s("canvas")));
|
||||
}
|
||||
@@ -616,6 +616,48 @@ fn screen_reader_values_follow_the_column_width_on_either_edge() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unanchored_open_prefers_the_end_dock_and_an_edge_frame_takes_a_quarter() {
|
||||
let start = Node::Dock { side: Side::Start, columns: vec![Column::new(vec![group(&[1])], 240.0)] };
|
||||
let end = docked(Side::End, &[&[&[2]]]).root.unwrap();
|
||||
let mut layout = Layout {
|
||||
root: Some(Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(start), second: Box::new(end) }),
|
||||
floating: Vec::new(),
|
||||
};
|
||||
layout.apply(Action::Open { panel: 9, anchor: None }).unwrap();
|
||||
assert_eq!(columns(&layout, Side::End), vec![vec![vec![2, 9]]]);
|
||||
assert_eq!(columns(&layout, Side::Start), vec![vec![vec![1]]]);
|
||||
// A minimized group and an iconic column are passed over: the panel would not show there.
|
||||
let start = Node::Dock { side: Side::Start, columns: vec![Column::new(vec![group(&[1])], 240.0)] };
|
||||
let mut end = docked(Side::End, &[&[&[2]]]).root.unwrap();
|
||||
if let Node::Split { second, .. } = &mut end
|
||||
&& let Node::Dock { columns, .. } = second.as_mut()
|
||||
{
|
||||
columns[0].groups[0].minimized = true;
|
||||
}
|
||||
let mut layout = Layout {
|
||||
root: Some(Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::FixedFirst(240.0), first: Box::new(start), second: Box::new(end) }),
|
||||
floating: Vec::new(),
|
||||
};
|
||||
let mut iconic = layout.clone();
|
||||
layout.apply(Action::Open { panel: 9, anchor: None }).unwrap();
|
||||
assert_eq!(columns(&layout, Side::Start), vec![vec![vec![1, 9]]]);
|
||||
if let Some(Node::Split { first, .. }) = &mut iconic.root
|
||||
&& let Node::Dock { columns, .. } = first.as_mut()
|
||||
{
|
||||
columns[0].iconic = true;
|
||||
}
|
||||
iconic.apply(Action::Open { panel: 9, anchor: None }).unwrap();
|
||||
assert!(matches!(iconic.place(&9), Some(dock_model::Place::Node(_))), "{iconic:?}");
|
||||
|
||||
// No dock: a frame at the right edge, a quarter of the width.
|
||||
let mut layout = Layout { root: Some(Node::Tabs { panels: vec![0], active: 0 }), floating: Vec::new() };
|
||||
layout.apply(Action::Open { panel: 9, anchor: None }).unwrap();
|
||||
let Some(Node::Split { axis: SplitAxis::Horizontal, size, second, .. }) = &layout.root else { panic!("{layout:?}") };
|
||||
assert_eq!(*size, SplitSize::Ratio(0.75));
|
||||
assert_eq!(**second, Node::Tabs { panels: vec![9], active: 0 });
|
||||
}
|
||||
|
||||
/// Panels 1-3 as a creative app's Color, Properties and Layers: Layers fills the column and asks
|
||||
/// for room for about ten rows.
|
||||
struct Creative;
|
||||
|
||||
@@ -365,6 +365,11 @@ pub enum Action<P> {
|
||||
panel: P,
|
||||
},
|
||||
/// Reveal a registered, currently absent panel. Applications validate their panel registry.
|
||||
/// `anchor` names the group it joins as the last tab. Without one it becomes the root of an
|
||||
/// empty layout, else joins the first dock group that accepts tabs and is shown (not
|
||||
/// minimized, not in an iconic column; the end-side dock's first), else gets a new frame a
|
||||
/// quarter wide at the right edge of the docked area
|
||||
/// ([`Placement::Edge`]).
|
||||
Open {
|
||||
panel: P,
|
||||
anchor: Option<P>,
|
||||
@@ -909,6 +914,13 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
/// application, particularly Open.
|
||||
pub fn apply_with_permissions(&mut self, action: Action<P>, permissions: impl Fn(&P) -> Permissions) -> Result<Vec<P>, DockError> {
|
||||
self.validate()?;
|
||||
let action = match action {
|
||||
Action::Open { panel, anchor: None } if self.root.is_some() && !self.contains(&panel) => {
|
||||
let (anchor, placement) = self.open_place(&permissions).ok_or(DockError::MissingTarget)?;
|
||||
Action::OpenAt { panel, anchor, placement }
|
||||
}
|
||||
action => action,
|
||||
};
|
||||
match &action {
|
||||
Action::Move { panel, anchor, placement } | Action::OpenAt { panel, anchor, placement } => {
|
||||
// Opening an absent panel moves nothing, so it works in a locked workspace too.
|
||||
@@ -1241,6 +1253,35 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
removed
|
||||
}
|
||||
|
||||
/// Where [`Action::Open`] without an anchor puts an absent panel in a layout with a root:
|
||||
/// the end of the first shown dock group that accepts tabs, in the end-side dock if one has
|
||||
/// such a group, else a new frame at the right edge of the docked area.
|
||||
fn open_place(&self, permissions: impl Fn(&P) -> Permissions) -> Option<(P, Placement<P>)> {
|
||||
let docked: Vec<(&P, Place)> = self
|
||||
.panels()
|
||||
.into_iter()
|
||||
.filter_map(|p| Some((p, self.place(p)?)))
|
||||
.filter(|(_, place)| !matches!(place, Place::Floating { .. }))
|
||||
.collect();
|
||||
let side = |place: &Place| match place {
|
||||
Place::Dock { path, .. } => match self.root.as_ref().and_then(|root| node_at(root, path)) {
|
||||
Some(Node::Dock { side, .. }) => Some(*side),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
// A minimized group or an iconic column would leave the panel open but not shown.
|
||||
let shown = |place: &Place| match place {
|
||||
Place::Dock { group, .. } => self.column(place).is_some_and(|c| !c.iconic && c.groups.get(*group).is_some_and(|g| !g.minimized)),
|
||||
_ => false,
|
||||
};
|
||||
let groups = docked.iter().filter(|(p, place)| side(place).is_some() && shown(place) && permissions(p).accepts_tabs);
|
||||
if let Some((panel, _)) = groups.clone().find(|(_, place)| side(place) == Some(Side::End)).or_else(|| groups.clone().next()) {
|
||||
return Some(((*panel).clone(), Placement::Tab { before: None }));
|
||||
}
|
||||
docked.first().map(|(panel, _)| ((*panel).clone(), Placement::Edge(Zone::Right)))
|
||||
}
|
||||
|
||||
fn insert(&mut self, entry: StackEntry<P>, anchor: &P, placement: Placement<P>) -> Result<(), DockError> {
|
||||
let place = self.place(anchor).ok_or(DockError::MissingTarget)?;
|
||||
let tree = place.tree_path().map(|(window, path)| (window, path.to_vec()));
|
||||
|
||||
@@ -120,7 +120,8 @@ impl<P: Clone + Eq + Hash> Workspace<P> {
|
||||
}
|
||||
|
||||
/// Show or hide one panel. Showing restores its recorded location, following the chain of
|
||||
/// hidden anchors when its own anchor is hidden too; otherwise it opens beside `fallback`.
|
||||
/// hidden anchors when its own anchor is hidden too; otherwise it opens as
|
||||
/// [`Action::Open`] with `fallback` as the anchor, following `permissions`.
|
||||
/// A shown panel becomes the active tab of its group. Hiding follows `permissions` and
|
||||
/// [`Self::locked`], as [`Self::apply`] does, so a protected canvas stays shown.
|
||||
pub fn set_visible(&mut self, panel: P, visible: bool, fallback: Option<P>, permissions: impl Fn(&P) -> Permissions) -> Result<(), DockError> {
|
||||
@@ -137,7 +138,7 @@ impl<P: Clone + Eq + Hash> Workspace<P> {
|
||||
let mut layout = self.layout.clone();
|
||||
if self.restore_into(&mut layout, &panel).is_err() {
|
||||
layout = self.layout.clone();
|
||||
layout.apply(Action::Open { panel: panel.clone(), anchor: fallback })?;
|
||||
layout.apply_with_permissions(Action::Open { panel: panel.clone(), anchor: fallback }, &permissions)?;
|
||||
}
|
||||
layout.apply(Action::Activate { panel: panel.clone() })?;
|
||||
self.layout = layout;
|
||||
|
||||
+7
-1
@@ -72,6 +72,10 @@ layout.apply(Action::Move {
|
||||
|
||||
- `Move`, `MovePanel`, `Float` and `Close` act on one panel. `MoveGroup`, `MoveColumn` and
|
||||
`MoveWindow` move a group, a dock column or a floating window, named by any panel in it.
|
||||
- `Open` reveals an absent panel as the last tab of its `anchor`'s group. Without an anchor it
|
||||
becomes the root of an empty layout, else joins the first dock group that accepts tabs and is
|
||||
shown, not minimized or in an iconic column (the end-side dock's first), else gets a new frame a quarter wide at the right edge of the docked
|
||||
area. `OpenAt` takes any placement.
|
||||
- A `Target` is `At { anchor, placement }`, `Float { rect }` or `NewDock { side }`.
|
||||
- A `Placement` is a tab position, a split zone, a new group above or below (with an optional
|
||||
preferred `height`), a new column, or a new frame on an outer edge.
|
||||
@@ -109,7 +113,7 @@ an optional saved arrangement for Reset, and `locked`.
|
||||
| Method | Does |
|
||||
|---|---|
|
||||
| `apply`, `apply_all` | Apply actions all or none, with the app's permissions. Closed panels are recorded in `hidden`. While `locked`, moving, floating and closing are refused; `SetNative`, which records the OS windows, still applies. |
|
||||
| `set_visible(panel, visible, fallback, permissions)` | Hide a panel that may close, or show it at its recorded place, else beside `fallback`. |
|
||||
| `set_visible(panel, visible, fallback, permissions)` | Hide a panel that may close, or show it at its recorded place, else as `Open` beside `fallback`, or where `Open` without an anchor puts it. |
|
||||
| `reconcile(registered, permissions, default)` | Fit a loaded workspace to the panels the app has now. Unknown panels move to `hidden`, so they come back when registered again. A registered panel that cannot close, such as the canvas, is always shown. |
|
||||
| `reset(default)` | Return to the saved arrangement, or to `default` for a built-in workspace. |
|
||||
| `validate` | Check a loaded workspace before use. |
|
||||
@@ -316,6 +320,8 @@ let closed = command.apply(&mut app.workspace, style.right_to_left, &space, |pan
|
||||
a new dock; when it already is that dock's last column, the command does nothing.
|
||||
- `window.panel.dock` with `left`, `right`, `top` or `bottom` and no `anchor` or `side` makes a
|
||||
new frame on that outer edge (`Placement::Edge`). A hidden panel is shown first.
|
||||
- Showing a panel (`activate`, `dock` without a destination, `ui.dock` `open`) restores a hidden
|
||||
panel's place; a panel never laid out opens as `Open` without an anchor does.
|
||||
- `window.panel.float` without a rect floats what it moves at the size a drag floats it at (see
|
||||
Floating size). It goes
|
||||
inside the dock area, 24 points from its top-left corner, so it stays clear of the menu bar.
|
||||
|
||||
Reference in new issue
Block a user