mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
Merge branch 'mac-b1-edges' into dock-result-preview
This commit is contained in:
commit
cdc6713130
13 files changed
+347
-91
No files matched your search
@@ -340,7 +340,24 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
/// [`super::float_space`] of the area that draws `workspace`. Given `x`, `y`, `width` and
|
||||
/// `height` replace the matching values, and a group or column that is already a whole
|
||||
/// floating window keeps its rect.
|
||||
///
|
||||
/// An outer edge dock wraps the part between the docks. [`Self::to_actions_with`] also
|
||||
/// knows which panels are protected, so it wraps the canvas column where a drag would.
|
||||
pub fn to_actions(&self, workspace: &Workspace<P>, right_to_left: bool, space: &FloatSpace<P>) -> Result<Vec<Action<P>>, DockError> {
|
||||
self.to_actions_with(workspace, right_to_left, space, |_| Permissions::default())
|
||||
}
|
||||
|
||||
/// [`Self::to_actions`] with the `permissions` the actions are applied with. An outer edge
|
||||
/// dock wraps what a drag to that edge would: the canvas column where the area narrows its
|
||||
/// edge band to it, else the part between the docks. `space` says whether the area draws
|
||||
/// with [`super::DockStyle::side_edge_zones`], which never narrows.
|
||||
pub fn to_actions_with(
|
||||
&self,
|
||||
workspace: &Workspace<P>,
|
||||
right_to_left: bool,
|
||||
space: &FloatSpace<P>,
|
||||
permissions: impl Fn(&P) -> Permissions,
|
||||
) -> Result<Vec<Action<P>>, DockError> {
|
||||
let layout = &workspace.layout;
|
||||
let zoned = Zoned { layout, right_to_left };
|
||||
let shown = |panel: &P| if layout.contains(panel) { Ok(()) } else { Err(DockError::MissingPanel) };
|
||||
@@ -418,12 +435,13 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
if before.is_some() {
|
||||
return Err(DockError::InvalidPlacement);
|
||||
}
|
||||
let anchor = layout
|
||||
.panels()
|
||||
.into_iter()
|
||||
.find(|p| !members.contains(p) && matches!(layout.place(p), Some(Place::Dock { .. } | Place::Node(_))))
|
||||
.cloned()
|
||||
.ok_or(DockError::MissingTarget)?;
|
||||
// The model narrows the edge on a protected anchor, as `Workspace::apply` checks it.
|
||||
let protected = |p: &P| {
|
||||
let policy = if workspace.locked { permissions(p).locked() } else { permissions(p) };
|
||||
policy.is_protected()
|
||||
};
|
||||
let narrowed = !space.side_edge_zones && layout.edge_narrows(zone, &protected);
|
||||
let anchor = layout.edge_anchor(narrowed, &protected, &|p| members.contains(p)).ok_or(DockError::MissingTarget)?;
|
||||
return one(unit.to(layout, panel, Target::At { anchor, placement: Placement::Edge(zone) })?);
|
||||
}
|
||||
if !matches!(layout.place(panel), Some(Place::Floating { .. })) {
|
||||
@@ -504,7 +522,7 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
/// 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`].
|
||||
/// `space` is where `window.panel.float` floats, as for [`Self::to_actions_with`].
|
||||
pub fn apply(
|
||||
&self,
|
||||
workspace: &mut Workspace<P>,
|
||||
@@ -526,7 +544,7 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
// Shown, then moved to the edge, all or nothing.
|
||||
let mut shown = workspace.clone();
|
||||
shown.set_visible(panel.clone(), true, None, &permissions)?;
|
||||
let closed = shown.apply_all(self.to_actions(&shown, right_to_left, space)?, permissions)?;
|
||||
let closed = shown.apply_all(self.to_actions_with(&shown, right_to_left, space, &permissions)?, &permissions)?;
|
||||
*workspace = shown;
|
||||
return Ok(closed);
|
||||
}
|
||||
@@ -534,7 +552,7 @@ impl<P: Clone + Eq + Hash> DockCommand<P> {
|
||||
workspace.locked = locked.unwrap_or(!workspace.locked);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let actions = self.to_actions(workspace, right_to_left, space)?;
|
||||
let actions = self.to_actions_with(workspace, right_to_left, space, &permissions)?;
|
||||
workspace.apply_all(actions, permissions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ fn parse(params: Value) -> Result<DockCommand<P>, serde_json::Error> {
|
||||
/// The actions `params` makes on a fresh workspace.
|
||||
fn actions(params: Value) -> Vec<Action<P>> {
|
||||
let command = parse(params.clone()).unwrap_or_else(|e| panic!("{params}: {e}"));
|
||||
command.to_actions(&workspace(), false, &space()).unwrap_or_else(|e| panic!("{params}: {e}"))
|
||||
command.to_actions_with(&workspace(), false, &space(), permissions).unwrap_or_else(|e| panic!("{params}: {e}"))
|
||||
}
|
||||
|
||||
/// Apply `params` to a fresh workspace; return it and the panels closed.
|
||||
@@ -294,7 +294,7 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
|
||||
fn side_zones_map_to_the_placement_the_anchor_container_takes_in_either_direction() {
|
||||
let command =
|
||||
|zone: &str, anchor: &str| parse(json!({"command": "window.panel.move", "panel": "history", "anchor": anchor, "zone": zone})).unwrap();
|
||||
let placed = |zone: &str, anchor: &str, rtl: bool| match command(zone, anchor).to_actions(&workspace(), rtl, &space()) {
|
||||
let placed = |zone: &str, anchor: &str, rtl: bool| match command(zone, anchor).to_actions_with(&workspace(), rtl, &space(), permissions) {
|
||||
Ok(actions) => match actions.as_slice() {
|
||||
[Action::Move { placement, .. }] => Ok(placement.clone()),
|
||||
other => panic!("{other:?}"),
|
||||
@@ -338,7 +338,7 @@ fn float_without_a_rect_takes_the_drag_size_inside_the_dock_area() {
|
||||
let d = testing::DockDriver::new(workspace().layout, Sized).with_area(area);
|
||||
let space = float_space::<P>(&d.ctx, d.id).expect("the area records its float space");
|
||||
assert_eq!(space.area, area);
|
||||
let float = |params: Value| parse(params).unwrap().to_actions(&workspace(), false, &space).unwrap();
|
||||
let float = |params: Value| parse(params).unwrap().to_actions_with(&workspace(), false, &space, permissions).unwrap();
|
||||
|
||||
let panel = float(json!({"command": "window.panel.float", "panel": "layers"}));
|
||||
// The floating_size width, and an empty body's least fitted height under its 150 points.
|
||||
@@ -359,19 +359,25 @@ fn float_without_a_rect_takes_the_drag_size_inside_the_dock_area() {
|
||||
|
||||
// Without recorded sizes a unit floats at the layout's size, shrunk into a small area.
|
||||
let small = FloatSpace::new(egui::Rect::from_min_size(egui::pos2(0.0, 30.0), egui::vec2(200.0, 200.0)));
|
||||
let applied = parse(json!({"command": "window.panel.float", "panel": "layers", "group": true})).unwrap().to_actions(&workspace(), false, &small);
|
||||
let applied = parse(json!({"command": "window.panel.float", "panel": "layers", "group": true})).unwrap().to_actions_with(
|
||||
&workspace(),
|
||||
false,
|
||||
&small,
|
||||
permissions,
|
||||
);
|
||||
assert_eq!(applied, Ok(vec![Action::MoveGroup { panel: s("layers"), to: Target::Float { rect: Some([0.0, 30.0, 200.0, 200.0]) } }]));
|
||||
|
||||
// A host seeds the size of a panel the area never drew; a recorded size still wins.
|
||||
let seeded = FloatSpace::new(area).with_panel_size(s("layers"), egui::vec2(180.0, 240.0));
|
||||
let applied = parse(json!({"command": "window.panel.float", "panel": "layers"})).unwrap().to_actions(&workspace(), false, &seeded);
|
||||
let applied =
|
||||
parse(json!({"command": "window.panel.float", "panel": "layers"})).unwrap().to_actions_with(&workspace(), false, &seeded, permissions);
|
||||
assert_eq!(applied, Ok(vec![Action::Float { panel: s("layers"), rect: [24.0, 54.0, 180.0, 240.0] }]));
|
||||
let recorded = space.clone().with_panel_size(s("layers"), egui::vec2(180.0, 240.0));
|
||||
assert_eq!(float_with(&recorded, "layers"), vec![Action::Float { panel: s("layers"), rect: [24.0, 54.0, 200.0, 120.0] }]);
|
||||
}
|
||||
|
||||
fn float_with(space: &FloatSpace<P>, panel: &str) -> Vec<Action<P>> {
|
||||
parse(json!({"command": "window.panel.float", "panel": panel})).unwrap().to_actions(&workspace(), false, space).unwrap()
|
||||
parse(json!({"command": "window.panel.float", "panel": panel})).unwrap().to_actions_with(&workspace(), false, space, permissions).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -411,6 +417,35 @@ fn dock_with_a_side_zone_and_no_anchor_makes_an_outer_edge_frame() {
|
||||
rejected(json!({"command": "window.panel.dock", "panel": "color", "zone": "left", "before": null}), DockError::InvalidPlacement);
|
||||
}
|
||||
|
||||
/// The frame beside `panel`'s own: what an edge split with `panel` wrapped.
|
||||
fn wrapped(layout: &Layout<P>, panel: &str) -> Option<Node<P>> {
|
||||
let Some(dock_model::Place::Node(path)) = layout.place(&s(panel)) else { return None };
|
||||
let (last, parent) = path.split_last()?;
|
||||
layout.root.as_ref().and_then(|root| dock_model::node_at(root, &[parent, &[!last]].concat())).cloned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outer_edge_dock_wraps_the_canvas_column_whatever_the_tree_order() {
|
||||
let tabs = |panel: &str| Node::Tabs { panels: vec![s(panel)], active: 0 };
|
||||
let side =
|
||||
|first, second| Node::Split { axis: SplitAxis::Horizontal, size: SplitSize::Ratio(0.5), first: Box::new(first), second: Box::new(second) };
|
||||
let canvas_first = side(tabs("canvas"), side(tabs("layers"), tabs("history")));
|
||||
let canvas_later = side(side(tabs("layers"), tabs("canvas")), tabs("history"));
|
||||
for root in [canvas_first, canvas_later] {
|
||||
let layout = Layout { root: Some(root), floating: vec![Floating::single(vec![s("color")], 0, [300.0, 200.0, 200.0, 150.0])] };
|
||||
let command = parse(json!({"command": "window.panel.dock", "panel": "color", "zone": "top"})).unwrap();
|
||||
let mut workspace = Workspace::new(layout.clone());
|
||||
command.apply(&mut workspace, false, &space(), permissions).unwrap();
|
||||
assert_eq!(wrapped(&workspace.layout, "color"), Some(tabs("canvas")), "{:?}", workspace.layout);
|
||||
// An area drawn with side edge zones never narrows its edge to the canvas column.
|
||||
let mut workspace = Workspace::new(layout.clone());
|
||||
command.apply(&mut workspace, false, &space().with_side_edge_zones(true), permissions).unwrap();
|
||||
let mut whole = layout;
|
||||
whole.floating.clear();
|
||||
assert_eq!(wrapped(&workspace.layout, "color"), whole.root, "{:?}", workspace.layout);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dock_to_the_side_a_panel_already_leads_the_last_column_of() {
|
||||
// Layers leads the dock's only column: it moves out to a new column after it.
|
||||
|
||||
@@ -543,20 +543,28 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The node an edge split wraps: the root, or the part between the docks. Where neither side
|
||||
/// of a side-by-side split is a dock and only one holds a `canvas` panel, that side: the
|
||||
/// canvas column, without the side frames.
|
||||
pub(in crate::docking) fn edge_path(&self, canvas: &dyn Fn(&P) -> bool) -> Vec<bool> {
|
||||
/// The node an edge split at `zone` wraps: the root, or the part between the docks. Where
|
||||
/// neither side of a side-by-side split is a dock and only one holds a `canvas` panel,
|
||||
/// that side: the canvas column, without the side frames. A top (bottom) edge also
|
||||
/// descends a split of rows into its first (last) row when only that row holds a `canvas`
|
||||
/// panel, so a full-width row below (above) the canvas keeps its edge.
|
||||
pub(in crate::docking) fn edge_path(&self, zone: Zone, canvas: &dyn Fn(&P) -> bool) -> Vec<bool> {
|
||||
let holds = |node: &Node<P>| tree_panels(node).into_iter().any(canvas);
|
||||
let mut path = Vec::new();
|
||||
let mut node = self.root.as_ref();
|
||||
while let Some(Node::Split { axis: SplitAxis::Horizontal, first, second, .. }) = node {
|
||||
while let Some(Node::Split { axis, first, second, .. }) = node {
|
||||
if path.len() > MAX_DEPTH {
|
||||
break;
|
||||
}
|
||||
let second_side = match (&**first, &**second) {
|
||||
(Node::Dock { .. }, _) => true,
|
||||
(_, Node::Dock { .. }) => false,
|
||||
_ => match (tree_panels(first).into_iter().any(canvas), tree_panels(second).into_iter().any(canvas)) {
|
||||
let second_side = match (axis, &**first, &**second) {
|
||||
(SplitAxis::Horizontal, Node::Dock { .. }, _) => true,
|
||||
(SplitAxis::Horizontal, _, Node::Dock { .. }) => false,
|
||||
(SplitAxis::Vertical, ..) => match (zone, holds(first), holds(second)) {
|
||||
(Zone::Top, true, false) => false,
|
||||
(Zone::Bottom, false, true) => true,
|
||||
_ => break,
|
||||
},
|
||||
_ => match (holds(first), holds(second)) {
|
||||
(true, false) => false,
|
||||
(false, true) => true,
|
||||
_ => break,
|
||||
@@ -568,6 +576,22 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
path
|
||||
}
|
||||
|
||||
/// Whether an edge drop at `zone` that narrows by `canvas` wraps less than one that does
|
||||
/// not: the edge area is narrower than the part between the docks.
|
||||
pub(in crate::docking) fn edge_narrows(&self, zone: Zone, canvas: &dyn Fn(&P) -> bool) -> bool {
|
||||
self.edge_path(zone, canvas) != self.edge_path(zone, &|_| false)
|
||||
}
|
||||
|
||||
/// The docked panel, other than `skip`, that a [`Placement::Edge`] anchors on so the drop
|
||||
/// wraps the edge area it shows: a `protected` one where that area is narrowed to the
|
||||
/// canvas column (`narrowed`), else one that is not protected, since a protected anchor
|
||||
/// narrows the model's edge. Any docked panel when every one is protected.
|
||||
pub(in crate::docking) fn edge_anchor(&self, narrowed: bool, protected: &dyn Fn(&P) -> bool, skip: &dyn Fn(&P) -> bool) -> Option<P> {
|
||||
let docked: Vec<&P> =
|
||||
self.panels().into_iter().filter(|p| !skip(p) && matches!(self.place(p), Some(Place::Dock { .. } | Place::Node(_)))).collect();
|
||||
docked.iter().find(|p| protected(p) == narrowed).or(docked.first()).map(|p| (*p).clone())
|
||||
}
|
||||
|
||||
pub(super) fn columns_mut(&mut self, path: &[bool]) -> Option<&mut Vec<Column<P>>> {
|
||||
match self.root.as_mut().and_then(|root| node_at_mut(root, path)) {
|
||||
Some(Node::Dock { columns, .. }) => Some(columns),
|
||||
@@ -866,7 +890,7 @@ impl<P: Clone + Eq + Hash> Layout<P> {
|
||||
return Err(DockError::InvalidPlacement);
|
||||
}
|
||||
// A protected anchor narrows the edge to the canvas column.
|
||||
let path = if canvas(&anchor) { self.edge_path(canvas) } else { self.edge_path(&|_| false) };
|
||||
let path = if canvas(&anchor) { self.edge_path(zone, canvas) } else { self.edge_path(zone, &|_| false) };
|
||||
let node = self.root.as_mut().and_then(|root| node_at_mut(root, &path)).ok_or(DockError::MissingTarget)?;
|
||||
if matches!(node, Node::Dock { .. }) {
|
||||
return Err(DockError::InvalidPlacement);
|
||||
|
||||
@@ -262,6 +262,59 @@ fn between_docks_the_outer_edge_band_covers_the_part_between_them() {
|
||||
assert_eq!(**first, split(SplitAxis::Vertical, SplitSize::Ratio(0.75), middle, tabs(&[3])));
|
||||
}
|
||||
|
||||
/// The frame beside `panel`'s own: what an edge split with `panel` wrapped.
|
||||
fn wrapped(layout: &Layout<u32>, panel: u32) -> Option<Node<u32>> {
|
||||
let Some(dock_model::Place::Node(path)) = layout.place(&panel) else { return None };
|
||||
let (last, parent) = path.split_last()?;
|
||||
layout.root.as_ref().and_then(|root| dock_model::node_at(root, &[parent, &[!last]].concat())).cloned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_a_full_width_row_the_top_edge_band_covers_only_the_canvas_column_above_it() {
|
||||
// SoundCraft's Edit window: side frame 1, canvas 0 and side frame [2, 3] over a full-width
|
||||
// editor 4.
|
||||
let Some(upper) = canvas_columns().root else { panic!() };
|
||||
let root = split(SplitAxis::Vertical, SplitSize::Ratio(0.7), upper.clone(), tabs(&[4]));
|
||||
let layout = Layout { root: Some(root), floating: Vec::new() };
|
||||
let driver = || DockDriver::new(layout.clone(), Bodies { rules: [(0, Permissions::PROTECTED)].into(), ..Bodies::default() });
|
||||
let d = driver();
|
||||
let (canvas, _) = group(&d, 0);
|
||||
let at = pos2(canvas.center().x, 5.0);
|
||||
let mut d = driver();
|
||||
d.hold(tab(&d, 3), at);
|
||||
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
|
||||
assert_eq!(preview.x_range(), canvas.shrink(1.0).x_range(), "the canvas column's width, not the window's");
|
||||
let out = d.release(at);
|
||||
d.settle();
|
||||
assert_eq!(out.actions, vec![Action::Move { panel: 3, anchor: 0, placement: Placement::Edge(Zone::Top) }]);
|
||||
assert_eq!(wrapped(&d.layout, 3), Some(tabs(&[0])), "{:?}", d.layout);
|
||||
assert_eq!(preview, group(&d, 3).1.shrink(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_outer_edge_drop_wraps_the_area_it_previews_when_the_app_applies_other_permissions() {
|
||||
// Frame [2, 3] and canvas 0 beside frame 1, which is protected only while drawing (PDFCraft's
|
||||
// bare tools mode): drawn, the edge band is not narrowed to the canvas.
|
||||
let inner = split(SplitAxis::Horizontal, SplitSize::Ratio(0.5), Node::Tabs { panels: vec![2, 3], active: 1 }, tabs(&[0]));
|
||||
let layout = Layout { root: Some(split(SplitAxis::Horizontal, SplitSize::Ratio(0.7), inner, tabs(&[1]))), floating: Vec::new() };
|
||||
let drawn = [(0, Permissions::PROTECTED), (1, Permissions::PROTECTED)];
|
||||
let driver = || DockDriver::new(layout.clone(), Bodies { rules: drawn.into(), ..Bodies::default() });
|
||||
let d = driver();
|
||||
let (canvas, _) = group(&d, 0);
|
||||
let at = pos2(canvas.center().x, 5.0);
|
||||
let mut d = driver();
|
||||
d.hold(tab(&d, 3), at);
|
||||
let preview = result_preview(&d).unwrap_or_else(|| panic!("no result preview"));
|
||||
assert_eq!(preview.x_range(), Rect::from_x_y_ranges(0.0..=800.0, 0.0..=1.0).shrink2(egui::vec2(1.0, 0.0)).x_range(), "the window's width");
|
||||
let out = d.release(at);
|
||||
let mut applied = layout.clone();
|
||||
let apply = |p: &u32| if *p == 0 { Permissions::PROTECTED } else { Permissions::default() };
|
||||
applied.apply_all(out.actions.clone(), apply).unwrap_or_else(|e| panic!("{e}: {:?}", out.actions));
|
||||
let mut rest = layout;
|
||||
rest.apply(Action::Close { panel: 3 }).unwrap();
|
||||
assert_eq!(wrapped(&applied, 3), rest.root, "{:?}", out.actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_edges_limit_the_outer_edges_and_the_canvas_sides() {
|
||||
let left_right = |style: &mut DockStyle| style.drop_edges = Sides::LEFT_RIGHT;
|
||||
|
||||
@@ -328,8 +328,9 @@ pub enum Placement<P> {
|
||||
/// root is split, or in a layout with docks, the part between them. The anchor names any
|
||||
/// docked panel. A protected anchor ([`Permissions::is_protected`], applied through
|
||||
/// [`Layout::apply_with_permissions`]) narrows the part to the canvas column: where neither
|
||||
/// side of a side-by-side split is a dock and only one holds a protected panel, that side.
|
||||
/// `Center` is invalid.
|
||||
/// side of a side-by-side split is a dock and only one holds a protected panel, that side;
|
||||
/// for `Top` (`Bottom`), also the first (last) row of a split of rows when only that row
|
||||
/// holds one. `Center` is invalid.
|
||||
Edge(Zone),
|
||||
}
|
||||
|
||||
|
||||
@@ -2205,10 +2205,13 @@ fn a_tab_drag_onto_a_native_window_is_no_app_drag() {
|
||||
#[test]
|
||||
fn a_native_window_fades_only_while_its_drag_can_dock() {
|
||||
let drag = |docks| WindowDrag { window: 1, grab: vec2(10.0, 10.0), to: pos2(100.0, 100.0), start: pos2(100.0, 100.0), docks };
|
||||
assert_eq!(drag_opacity(None, true), 1.0);
|
||||
assert_eq!(drag_opacity(None, false, true), 1.0);
|
||||
// A bar drag that only moves the window stays opaque.
|
||||
assert_eq!(drag_opacity(Some(&drag(false)), false), 1.0);
|
||||
assert_eq!(drag_opacity(Some(&drag(false)), true), 0.75);
|
||||
assert_eq!(drag_opacity(Some(&drag(false)), false, false), 1.0);
|
||||
assert_eq!(drag_opacity(Some(&drag(false)), false, true), 0.75);
|
||||
// A drag by the window's only tab or group docks either way.
|
||||
assert_eq!(drag_opacity(Some(&drag(true)), false), 0.75);
|
||||
assert_eq!(drag_opacity(Some(&drag(true)), false, false), 0.75);
|
||||
// With Cmd/Ctrl held the drag only moves the window.
|
||||
assert_eq!(drag_opacity(Some(&drag(false)), true, true), 1.0);
|
||||
assert_eq!(drag_opacity(Some(&drag(true)), true, false), 1.0);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ use egui::{Context, Pos2, Rect, pos2, vec2};
|
||||
|
||||
use super::{GroupKind, Renderer, floating::WindowHit, group::TabGroup};
|
||||
use crate::docking::{
|
||||
Action, Column, DockContent, DockEvent, Floating, Group, HidePanels, Layout, Node, Zone,
|
||||
dock_model::{Place, node_at},
|
||||
Action, Column, DockContent, DockEvent, Floating, Group, HidePanels, Layout, Node, Placement, Target, Zone,
|
||||
dock_model::{Place, node_at, tree_panels},
|
||||
};
|
||||
|
||||
/// How far below the pointer the float hint sits, clear of the cursor.
|
||||
@@ -20,14 +20,40 @@ pub(in crate::docking) struct FrameState<P> {
|
||||
paths: Option<HashMap<Vec<bool>, Vec<bool>>>,
|
||||
/// The same for each floating window's splits, by the window's first shown panel.
|
||||
window_paths: HashMap<P, HashMap<Vec<bool>, Vec<bool>>>,
|
||||
/// The view node whose rect is the outer edge area; none while a frame is maximized.
|
||||
edge_path: Option<Vec<bool>>,
|
||||
pub(super) edge: Option<Rect>,
|
||||
/// The outer edge areas of top, bottom, left and right drops; none while a frame is
|
||||
/// maximized.
|
||||
edges: Option<[EdgeArea<P>; 4]>,
|
||||
/// The panels the view leaves out.
|
||||
hidden: Vec<P>,
|
||||
pub(super) maximized: Option<P>,
|
||||
/// Floating windows as drawn, back to front.
|
||||
pub(super) windows: Vec<WindowHit<P>>,
|
||||
}
|
||||
|
||||
/// The view node an outer edge drop wraps ([`Layout::edge_path`]), and its rect once drawn.
|
||||
struct EdgeArea<P> {
|
||||
path: Vec<bool>,
|
||||
/// The node is narrower than the part between the docks: the canvas column.
|
||||
narrowed: bool,
|
||||
panels: Vec<P>,
|
||||
rect: Option<Rect>,
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash> EdgeArea<P> {
|
||||
fn of(view: &Layout<P>, zone: Zone, canvas: &dyn Fn(&P) -> bool) -> Self {
|
||||
let path = view.edge_path(zone, canvas);
|
||||
let panels =
|
||||
view.root.as_ref().and_then(|root| node_at(root, &path)).map_or(Vec::new(), |node| tree_panels(node).into_iter().cloned().collect());
|
||||
Self { narrowed: view.edge_narrows(zone, canvas), path, panels, rect: None }
|
||||
}
|
||||
}
|
||||
|
||||
const EDGES: [Zone; 4] = [Zone::Top, Zone::Bottom, Zone::Left, Zone::Right];
|
||||
|
||||
fn edge_areas<P: Clone + Eq + Hash>(view: &Layout<P>, canvas: &dyn Fn(&P) -> bool) -> [EdgeArea<P>; 4] {
|
||||
EDGES.map(|zone| EdgeArea::of(view, zone, canvas))
|
||||
}
|
||||
|
||||
/// The layout as shown: a maximized frame alone, and hidden panels left out. `narrow` narrows
|
||||
/// the outer edge area to the canvas column ([`Layout::edge_path`]).
|
||||
pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
|
||||
@@ -54,11 +80,12 @@ pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
|
||||
}
|
||||
Place::Floating { .. } => None,
|
||||
});
|
||||
let mut state = FrameState { paths: None, window_paths: HashMap::new(), edge_path: None, edge: None, maximized: None, windows: Vec::new() };
|
||||
let mut state = FrameState { paths: None, window_paths: HashMap::new(), edges: None, hidden: Vec::new(), maximized: None, windows: Vec::new() };
|
||||
if maximized.is_none() && matches!(hide, HidePanels::None) {
|
||||
state.edge_path = Some(layout.edge_path(&canvas));
|
||||
state.edges = Some(edge_areas(layout, &canvas));
|
||||
return (Cow::Borrowed(layout), state);
|
||||
}
|
||||
state.hidden = layout.panels().into_iter().filter(|p| hidden(p)).cloned().collect();
|
||||
let mut paths = HashMap::new();
|
||||
let view = match maximized {
|
||||
Some((panel, path, node)) => {
|
||||
@@ -81,7 +108,7 @@ pub(in crate::docking) fn view<'a, P: Clone + Eq + Hash>(
|
||||
}
|
||||
};
|
||||
if state.maximized.is_none() {
|
||||
state.edge_path = Some(view.edge_path(&canvas));
|
||||
state.edges = Some(edge_areas(&view, &canvas));
|
||||
}
|
||||
state.paths = Some(paths);
|
||||
(Cow::Owned(view), state)
|
||||
@@ -156,8 +183,11 @@ fn prune_group<P: Clone + PartialEq>(group: &Group<P>, hidden: &dyn Fn(&P) -> bo
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
pub(super) fn note_edge(&mut self, path: &[bool], rect: Rect) {
|
||||
if self.tree.is_none() && self.frame.edge_path.as_deref() == Some(path) {
|
||||
self.frame.edge = Some(rect);
|
||||
if self.tree.is_some() {
|
||||
return;
|
||||
}
|
||||
for area in self.frame.edges.iter_mut().flatten().filter(|area| area.path == path) {
|
||||
area.rect = Some(rect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,38 +207,33 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// edge zones the quarter the new frame takes. Without them, on a tab strip that takes tabs,
|
||||
/// the band reaches only [`crate::docking::targets::STRIP_REACH`] from the line.
|
||||
pub(super) fn edge_proposal(&self, members: &[P], pointer: Pos2) -> Option<(P, Zone, Rect)> {
|
||||
let area = self.frame.edge.filter(|area| area.contains(pointer))?;
|
||||
let band = if self.style.side_edge_zones { crate::docking::targets::SIDE_EDGE_BAND } else { self.style.edge_band };
|
||||
let band = band.min(area.width() / 4.0).min(area.height() / 4.0);
|
||||
let line = |zone| crate::docking::targets::edge_line(area, zone, self.style.drop_line_width);
|
||||
let sides = self.style.outer_edges();
|
||||
let edges = [
|
||||
(pointer.y - area.top(), Zone::Top),
|
||||
(area.bottom() - pointer.y, Zone::Bottom),
|
||||
(pointer.x - area.left(), Zone::Left),
|
||||
(area.right() - pointer.x, Zone::Right),
|
||||
];
|
||||
let (distance, zone) = edges.into_iter().filter(|(_, zone)| sides.contains(*zone)).min_by(|a, b| a.0.total_cmp(&b.0))?;
|
||||
if distance > band {
|
||||
return None;
|
||||
}
|
||||
let reach = |line: Rect| line.distance_to_pos(pointer) <= crate::docking::targets::STRIP_REACH;
|
||||
if !self.style.side_edge_zones && self.in_tab_strip(pointer) && !line(zone).is_some_and(reach) {
|
||||
return None;
|
||||
}
|
||||
// A protected anchor narrows the model's edge to the canvas column, as `edge_path`
|
||||
// narrows the band without side edge zones.
|
||||
let narrow = !self.style.side_edge_zones;
|
||||
let docked: Vec<&P> = self
|
||||
.layout
|
||||
.panels()
|
||||
let (distance, zone, area) = EDGES
|
||||
.into_iter()
|
||||
.filter(|p| !members.contains(p) && !matches!(self.place(p), Some(Place::Floating { .. }) | None))
|
||||
.collect();
|
||||
let anchor = docked.iter().find(|p| self.permissions(p).is_protected() == narrow).or(docked.first())?;
|
||||
let anchor = (*anchor).clone();
|
||||
.filter(|zone| sides.contains(*zone))
|
||||
.filter_map(|zone| {
|
||||
let area = self.edge_area(zone)?.rect.filter(|area| area.contains(pointer))?;
|
||||
let distance = match zone {
|
||||
Zone::Top => pointer.y - area.top(),
|
||||
Zone::Bottom => area.bottom() - pointer.y,
|
||||
Zone::Left => pointer.x - area.left(),
|
||||
Zone::Right | Zone::Center => area.right() - pointer.x,
|
||||
};
|
||||
Some((distance, zone, area))
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))?;
|
||||
let band = if self.style.side_edge_zones { crate::docking::targets::SIDE_EDGE_BAND } else { self.style.edge_band };
|
||||
if distance > band.min(area.width() / 4.0).min(area.height() / 4.0) {
|
||||
return None;
|
||||
}
|
||||
let line = crate::docking::targets::edge_line(area, zone, self.style.drop_line_width);
|
||||
let reach = |line: Rect| line.distance_to_pos(pointer) <= crate::docking::targets::STRIP_REACH;
|
||||
if !self.style.side_edge_zones && self.in_tab_strip(pointer) && !line.is_some_and(reach) {
|
||||
return None;
|
||||
}
|
||||
let anchor = self.edge_anchor(members, zone)?;
|
||||
if !self.style.side_edge_zones {
|
||||
return Some((anchor, zone, line(zone)?));
|
||||
return Some((anchor, zone, line?));
|
||||
}
|
||||
let size = area.size() * 0.25;
|
||||
let preview = match zone {
|
||||
@@ -220,6 +245,48 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
Some((anchor, zone, preview))
|
||||
}
|
||||
|
||||
fn edge_area(&self, zone: Zone) -> Option<&EdgeArea<P>> {
|
||||
self.frame.edges.as_ref()?.get(EDGES.iter().position(|edge| *edge == zone)?)
|
||||
}
|
||||
|
||||
/// The docked anchor, not one of `members`, of an outer edge drop at `zone` that wraps the
|
||||
/// edge area drawn: protected where that area is the canvas column, as the model narrows on
|
||||
/// a protected anchor, else not. The drawn area and the model's can still differ (hidden
|
||||
/// panels, a canvas test that differs from these permissions), so the drop is tried: an
|
||||
/// anchor whose drop wraps other panels than the area shows gives way to one that is not
|
||||
/// protected, and then to no edge target.
|
||||
fn edge_anchor(&self, members: &[P], zone: Zone) -> Option<P> {
|
||||
let area = self.edge_area(zone)?;
|
||||
let protected = |p: &P| self.permissions(p).is_protected();
|
||||
let skip = |p: &P| members.contains(p);
|
||||
let lead = members.first()?;
|
||||
let wraps_area = |anchor: &P| {
|
||||
let to = Target::At { anchor: anchor.clone(), placement: Placement::Edge(zone) };
|
||||
let action = match members {
|
||||
[_] => Action::Move { panel: lead.clone(), anchor: anchor.clone(), placement: Placement::Edge(zone) },
|
||||
_ => Action::MoveGroup { panel: lead.clone(), to },
|
||||
};
|
||||
let mut candidate = self.layout.clone();
|
||||
if candidate.apply_with_permissions(action, |p| self.permissions(p)).is_err() {
|
||||
// Left to the model's check.
|
||||
return true;
|
||||
}
|
||||
let Some(Place::Node(path)) = candidate.place(lead) else { return false };
|
||||
let Some((last, parent)) = path.split_last() else { return false };
|
||||
let beside = [parent, &[!last]].concat();
|
||||
let Some(wrapped) = candidate.root.as_ref().and_then(|root| node_at(root, &beside)) else { return false };
|
||||
let shown = |p: &&P| !self.frame.hidden.contains(p) && !members.contains(p);
|
||||
let wrapped: Vec<&P> = tree_panels(wrapped).into_iter().filter(shown).collect();
|
||||
let drawn: Vec<&P> = area.panels.iter().filter(shown).collect();
|
||||
wrapped.len() == drawn.len() && wrapped.iter().all(|p| drawn.contains(p))
|
||||
};
|
||||
let anchor = self.layout.edge_anchor(area.narrowed, &protected, &skip)?;
|
||||
if wraps_area(&anchor) {
|
||||
return Some(anchor);
|
||||
}
|
||||
self.layout.edge_anchor(false, &protected, &skip).filter(|other| *other != anchor && !protected(other) && wraps_area(other))
|
||||
}
|
||||
|
||||
/// Under the pointer while a drag offers a docking target: which modifier floats the drop
|
||||
/// instead. With native windows, the window under the pointer draws it, beside its preview.
|
||||
pub(super) fn paint_float_hint(&self, ctx: &Context, pointer: Pos2) {
|
||||
|
||||
@@ -95,11 +95,12 @@ pub(in crate::docking) const OTHER_CLOSE: CloseLayout =
|
||||
const DRAG_OPACITY: f64 = 0.75;
|
||||
|
||||
/// Opacity of the window `drag` moves: faded while the drag can dock (its only tab or group, or
|
||||
/// a bar with [`crate::docking::DockStyle::title_bar_docks`]), opaque for a plain move.
|
||||
/// a bar with [`crate::docking::DockStyle::title_bar_docks`]), opaque for a plain move and while
|
||||
/// Ctrl/Cmd keeps the drop floating (`float_only`).
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
pub(in crate::docking) fn drag_opacity(drag: Option<&WindowDrag>, title_bar_docks: bool) -> f64 {
|
||||
pub(in crate::docking) fn drag_opacity(drag: Option<&WindowDrag>, float_only: bool, title_bar_docks: bool) -> f64 {
|
||||
match drag {
|
||||
Some(drag) if drag.docks || title_bar_docks => DRAG_OPACITY,
|
||||
Some(drag) if !float_only && (drag.docks || title_bar_docks) => DRAG_OPACITY,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
@@ -594,7 +595,8 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let drag = ctx.data(|d| d.get_temp::<WindowDrag>(drag_key));
|
||||
let dragged = drag.as_ref().and_then(|drag| self.layout.floating.iter().find(|w| w.id == drag.window));
|
||||
let this = dragged.and_then(|w| crate::docking::dock_model::first_active(&w.root)).is_some_and(|p| self.label_of(p) == title);
|
||||
let opacity = if this { drag_opacity(drag.as_ref(), self.style.title_bar_docks) } else { 1.0 };
|
||||
let float_only = drag.is_some_and(|drag| self.cross.pointer.is_some_and(|p| p.window == Some(drag.window) && p.command));
|
||||
let opacity = if this { drag_opacity(drag.as_ref(), float_only, self.style.title_bar_docks) } else { 1.0 };
|
||||
set_window_opacity(&title, main.as_deref(), opacity);
|
||||
}
|
||||
if let Some(placed) = report.placed.filter(|_| !moving) {
|
||||
|
||||
@@ -226,10 +226,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
|
||||
/// The side split of `proposal`'s frame when halving it leaves no room
|
||||
/// ([`Placement::SplitSized`]): the frame keeps its size, the new frame takes the size the
|
||||
/// moved panels have now (at least its minimum and [`DROP_MARGIN`]), and the frames beyond
|
||||
/// the divider on that side give the room. `None` without such a divider, or when those
|
||||
/// frames cannot give the new frame its minimum.
|
||||
/// ([`Placement::SplitSized`]): the frame keeps its size, and the frames beyond the divider
|
||||
/// on that side give the new frame its minimum (with [`DROP_MARGIN`]). A floating panel
|
||||
/// keeps its size along the split instead, and one in a dock column its column's width or
|
||||
/// its group's height, when that fits. `None` without such a divider, or when those frames
|
||||
/// cannot give the new frame its minimum.
|
||||
fn sized_split(&self, panel: &P, proposal: &DropProposal<P>, action: &impl Fn(Target<P>) -> Action<P>) -> Option<Placement<P>> {
|
||||
let Placement::Split(zone) = proposal.placement else { return None };
|
||||
let axis = if matches!(zone, Zone::Top | Zone::Bottom) { SplitAxis::Vertical } else { SplitAxis::Horizontal };
|
||||
@@ -277,11 +278,18 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let room = sizer.travel(far.0, far.1, axis, start).0 - DROP_MARGIN;
|
||||
let least = along(super::node_limits(new, self.style, &*self.content).min) + DROP_MARGIN;
|
||||
let gap = self.style.gap;
|
||||
let wanted = now.iter().find(|(node, _)| holds(node, panel)).map_or(least, |(_, rect)| along(rect.size()));
|
||||
let size = wanted.max(least).min(along(halved.size()) + room - frame - gap);
|
||||
if size < least {
|
||||
// A split frame's width is its share of the room, not one of its own.
|
||||
let saved = match self.place(panel) {
|
||||
Some(Place::Floating { .. }) => now.iter().find(|(node, _)| holds(node, panel)).map(|(_, rect)| along(rect.size())),
|
||||
Some(place @ Place::Dock { .. }) if axis == SplitAxis::Horizontal => self.layout.column(place).map(|column| column.width),
|
||||
Some(place @ Place::Dock { .. }) => self.layout.group(place).and_then(|group| group.height),
|
||||
_ => None,
|
||||
};
|
||||
let most = along(halved.size()) + room - frame - gap;
|
||||
if least > most {
|
||||
return None;
|
||||
}
|
||||
let size = saved.filter(|width| (least..=most).contains(width)).unwrap_or(least);
|
||||
// The anchor's side of the outer split grows by what the halves did not take already.
|
||||
let grow = frame + gap + size - along(halved.size());
|
||||
let first = along(a.1.size()) + if start { -grow } else { grow };
|
||||
|
||||
@@ -397,6 +397,32 @@ fn a_drop_beside_a_frame_too_narrow_to_halve_takes_room_from_its_neighbour() {
|
||||
assert!(new_properties.width() < properties.width() - 200.0, "Properties gave the room");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_docked_panel_dropped_beside_a_frame_too_narrow_to_halve_takes_its_minimum_width() {
|
||||
// Info docked below Properties, as wide as it, beside a narrow Tools frame.
|
||||
let root = Node::Split {
|
||||
axis: SplitAxis::Horizontal,
|
||||
size: SplitSize::FixedSecond(60.0),
|
||||
first: Box::new(split(SplitAxis::Vertical, 0.6, tabs(&PROPERTIES), tabs(&[INFO]))),
|
||||
second: Box::new(tabs(&[TOOLS])),
|
||||
};
|
||||
let mut d = driver(Layout { root: Some(root), floating: Vec::new() }, Film::default());
|
||||
d.area = Rect::from_min_size(Pos2::ZERO, vec2(1000.0, SCREEN.y));
|
||||
d.settle();
|
||||
let (tools, properties) = (group(&d, TOOLS), group(&d, PROPERTIES[0]));
|
||||
let out = d.drag(tab(&d, INFO), pos2(tools.left() + 4.0, tools.center().y + 13.0));
|
||||
assert!(
|
||||
matches!(&out.actions[..], [Action::Move { panel: INFO, anchor: TOOLS, placement: Placement::SplitSized { zone: Zone::Left, .. } }]),
|
||||
"{:?}",
|
||||
out.actions
|
||||
);
|
||||
d.settle();
|
||||
let (new_tools, new_info, new_properties) = (group(&d, TOOLS), group(&d, INFO), group(&d, PROPERTIES[0]));
|
||||
assert!((new_tools.width() - tools.width()).abs() < 1.0, "Tools keeps its width: {new_tools:?}");
|
||||
assert!(new_info.width() < 60.0, "Info takes its minimum, not its docked width: {new_info:?}");
|
||||
assert!(new_properties.width() > properties.width() - 60.0, "Properties gives only that: {new_properties:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_beside_a_narrow_frame_joins_it_when_no_neighbour_has_room() {
|
||||
let mut d = beside_tools(130.0, 60.0);
|
||||
|
||||
@@ -366,9 +366,9 @@ const FLOAT_INSET: f32 = 24.0;
|
||||
#[cfg_attr(not(feature = "serde"), allow(dead_code))]
|
||||
pub(super) const DEFAULT_FLOAT: Vec2 = Vec2::new(260.0, 320.0);
|
||||
|
||||
/// Where a docking command floats a panel, group or column when it gives no rect (see
|
||||
/// `DockCommand::to_actions`, feature `serde`). [`DockArea`] records one each frame; read it
|
||||
/// with [`float_space`].
|
||||
/// Where a docking command floats a panel, group or column when it gives no rect, and how the
|
||||
/// area draws its outer edge (see `DockCommand::to_actions`, feature `serde`). [`DockArea`]
|
||||
/// records one each frame; read it with [`float_space`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FloatSpace<P> {
|
||||
/// The rect the area drew in. A floated unit goes inside it, 24 points from its top-left
|
||||
@@ -381,13 +381,23 @@ pub struct FloatSpace<P> {
|
||||
/// The height each panel's content used when it was last drawn, which fits a floated group
|
||||
/// to its content; `None` for content that filled its body.
|
||||
pub(super) contents: Vec<(P, Option<f32>)>,
|
||||
/// The area draws with [`DockStyle::side_edge_zones`]: an outer edge drop wraps the part
|
||||
/// between the docks, never only the canvas column.
|
||||
pub(super) side_edge_zones: bool,
|
||||
}
|
||||
|
||||
impl<P: PartialEq> FloatSpace<P> {
|
||||
/// An area with no recorded sizes: commands float a panel at 260x320, and a group or
|
||||
/// column at its dock column's width (else 260) by 320.
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self { area, panels: Vec::new(), groups: Vec::new(), columns: Vec::new(), contents: Vec::new() }
|
||||
Self { area, panels: Vec::new(), groups: Vec::new(), columns: Vec::new(), contents: Vec::new(), side_edge_zones: false }
|
||||
}
|
||||
|
||||
/// For an area drawn with [`DockStyle::side_edge_zones`] when the area has not drawn yet
|
||||
/// (a headless run): an outer edge command then wraps what that profile's edge band wraps.
|
||||
pub fn with_side_edge_zones(mut self, on: bool) -> Self {
|
||||
self.side_edge_zones = on;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds the size a command floats `panel` at alone when the area has not drawn it, such as
|
||||
@@ -1189,7 +1199,7 @@ impl<'a, P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<'a, P> {
|
||||
output: DockOutput::default(),
|
||||
frame,
|
||||
iconic: super::render::Iconic::new(self.auto_collapse),
|
||||
float_space: FloatSpace { contents, ..FloatSpace::new(rect) },
|
||||
float_space: FloatSpace { contents, side_edge_zones: style.side_edge_zones, ..FloatSpace::new(rect) },
|
||||
places,
|
||||
contents: content_index,
|
||||
dividers: Vec::new(),
|
||||
|
||||
@@ -417,7 +417,8 @@ mod v1_fixtures {
|
||||
let float: DockCommand<String> =
|
||||
serde_json::from_value(json!({"command": "window.panel.float", "panel": bars[0], "column": true})).unwrap();
|
||||
let space = craft_ui::docking::float_space(&d.ctx, d.id).unwrap();
|
||||
let actions = float.to_actions(&craft_ui::docking::Workspace::new(d.layout.clone()), false, &space).unwrap();
|
||||
let actions =
|
||||
float.to_actions_with(&craft_ui::docking::Workspace::new(d.layout.clone()), false, &space, |p| d.content.permissions(p)).unwrap();
|
||||
d.layout.apply_all(actions, |p| d.content.permissions(p)).unwrap_or_else(|e| panic!("{at}: {e}"));
|
||||
d.settle();
|
||||
assert!(start_columns(&d.layout).is_empty(), "{at}: floated");
|
||||
|
||||
+12
-4
@@ -266,9 +266,16 @@ A flyout fits its content the same way, without the title bar.
|
||||
edge band is offered only when every frame keeps its minimum.
|
||||
- The edge area is the root, or the part between the docks. Without `side_edge_zones` it also
|
||||
narrows to the canvas column: where neither side of a side-by-side split is a dock and only
|
||||
one side holds a protected panel, that side. Side panels in split frames then get no band
|
||||
across the whole window, and the drop wraps only the canvas column. A drop on the band
|
||||
anchors on the canvas, so `Placement::Edge` narrows the same way when applied.
|
||||
one side holds a protected panel, that side. The top (bottom) band also narrows a split of
|
||||
rows to its first (last) row when only that row holds the canvas, so a full-width row below
|
||||
the canvas keeps the bottom band. Side panels in split frames then get no band across the
|
||||
whole window, and the drop wraps only the canvas column. A drop on a narrowed band anchors
|
||||
on the canvas, so `Placement::Edge` narrows the same way when applied; on a band that is not
|
||||
narrowed it anchors on a panel that is not protected. The renderer tries the drop with its
|
||||
own permissions and offers the band only when the drop wraps the panels the band shows.
|
||||
`window.panel.dock` to an outer edge picks its anchor the same way, from the permissions
|
||||
`apply` (or `to_actions`) gets and the area's `FloatSpace` (`with_side_edge_zones` before it
|
||||
has drawn).
|
||||
- `DockStyle::drop_edges` (a `Sides`, all four by default) lists which sides of a group that
|
||||
takes no tabs (a canvas), and of the layout's outer edge, take a drop. A canvas's whole body
|
||||
goes to the nearest listed side; with none listed it takes nothing. In the docking profile
|
||||
@@ -400,7 +407,8 @@ let closed = command.apply(&mut app.workspace, style.right_to_left, &space, |pan
|
||||
it floats. `side` is `Start` or `End`: what moves becomes a new last column of that dock, or
|
||||
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.
|
||||
new frame on that outer edge (`Placement::Edge`), wrapping what a drag to that edge would
|
||||
(see the edge area above). 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
|
||||
|
||||
Reference in new issue
Block a user