mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
docking: dock profile with columns, column bars, seams and dock targets
Node::Dock renders as side docks: columns from the window edge inwards, each with a column bar (drag, »/« button, double-click and menu toggle iconic) above stacked groups. A split beside a dock gives the dock its columns' width. Every column's inner edge resizes it; dividers between open groups save both heights; minimized groups keep theirs. A column whose groups' minimum heights do not fit scrolls. Drops resolve dock targets before group targets: column edges (new column), seams and the top or bottom quarter of a group body (new group), the room under a minimized column (below its last group) and the left or right edge of the docked area without a dock (new dock, offered once the layout has a dock). They show a horizontal or full-height vertical line. Floating windows and host overlays occlude them. Tab drags use the same targets with Move. A column bar drag moves the column with a ghost and floats its groups away from a dock; Escape, focus loss, pointer exit and cancel_drag cancel it like a group drag. Side::Start maps to the left edge, or the right with DockStyle::right_to_left. New DockStyle fields: column_bar_height, seam_band, edge_band, right_to_left and the column labels. Behaviour follows vectorcraft crates/ui-egui/src/floating.rs (drop geometry, column layout and scrolling) and photocraft crates/ui-egui/src/dock.rs (group heights).
This commit is contained in:
1 parent
4bf11647ad
commit
f1a3683cc9
6 files changed
+656
-27
No files matched your search
@@ -0,0 +1,505 @@
|
||||
//! [D] The dock profile: side docks of columns, each a column bar above stacked groups.
|
||||
//!
|
||||
//! Columns run from the window edge inwards. Every column's inner edge resizes it; the last
|
||||
//! column's inner edge is the dock's inner edge. Behaviour follows VectorCraft `floating.rs`
|
||||
//! (column layout, drop geometry) and PhotoCraft `dock.rs` (group heights).
|
||||
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use egui::{Context, Key, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, pos2, vec2};
|
||||
|
||||
use super::{GroupKind, Renderer};
|
||||
use crate::docking::{
|
||||
Action, Column, Group, GroupDrag, GroupGesture, Node, Side, Target,
|
||||
targets::{DockDrop, DockTarget, column_edge, horizontal_line, resolve_dock, usable, vertical_line},
|
||||
};
|
||||
use crate::layout::{ResizeHandle, ResizeHandleStyle, SplitAxis};
|
||||
|
||||
/// Width of an iconic column without a saved width.
|
||||
const ICONIC_WIDTH: f32 = 36.0;
|
||||
/// Widest an iconic column gets by dragging its inner edge.
|
||||
const ICONIC_MAX: f32 = 240.0;
|
||||
/// Width of the »/« button at the outer end of a column bar.
|
||||
const COLLAPSE_WIDTH: f32 = 18.0;
|
||||
|
||||
/// The physical window edge a dock sits on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(in crate::docking) enum Edge {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// `Start` is the left edge in a left-to-right layout and the right edge in a right-to-left one.
|
||||
pub(in crate::docking) fn edge_of(side: Side, right_to_left: bool) -> Edge {
|
||||
match (side, right_to_left) {
|
||||
(Side::Start, false) | (Side::End, true) => Edge::Left,
|
||||
(Side::Start, true) | (Side::End, false) => Edge::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn side_at(edge: Edge, right_to_left: bool) -> Side {
|
||||
match (edge, right_to_left) {
|
||||
(Edge::Left, false) | (Edge::Right, true) => Side::Start,
|
||||
(Edge::Left, true) | (Edge::Right, false) => Side::End,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::docking) fn column_width<P>(column: &Column<P>) -> f32 {
|
||||
if column.iconic { column.iconic_width.unwrap_or(ICONIC_WIDTH) } else { column.width }
|
||||
}
|
||||
|
||||
/// Heights of stacked groups in a column body `room` high. Each entry is (minimized, saved
|
||||
/// height, minimum). Minimized groups take `strip`, groups with a saved height keep it, and the
|
||||
/// rest share what is left; when every open group has a height, the last one takes the rest.
|
||||
/// Saved heights shrink towards their minimums before the column overflows and scrolls.
|
||||
pub(in crate::docking) fn group_heights(groups: &[(bool, Option<f32>, f32)], strip: f32, room: f32) -> Vec<f32> {
|
||||
let open: Vec<usize> = (0..groups.len()).filter(|i| groups.get(*i).is_some_and(|g| !g.0)).collect();
|
||||
let mut shared: Vec<usize> = open.iter().copied().filter(|i| groups.get(*i).is_some_and(|g| g.1.is_none_or(|h| !h.is_finite()))).collect();
|
||||
if shared.is_empty() {
|
||||
shared.extend(open.last());
|
||||
}
|
||||
let mut heights: Vec<f32> = groups
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (minimized, height, min))| {
|
||||
if *minimized {
|
||||
strip
|
||||
} else if shared.contains(&i) {
|
||||
0.0
|
||||
} else {
|
||||
height.unwrap_or(*min).max(*min)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mins: Vec<f32> = groups.iter().map(|g| if g.0 { strip } else { g.2 }).collect();
|
||||
let shared_min: f32 = shared.iter().filter_map(|i| mins.get(*i)).sum();
|
||||
let deficit = heights.iter().sum::<f32>() + shared_min - room;
|
||||
let slack: f32 = open.iter().filter(|i| !shared.contains(i)).filter_map(|i| Some(heights.get(*i)? - mins.get(*i)?)).sum();
|
||||
if deficit > 0.0 && slack > 0.0 {
|
||||
let take = deficit.min(slack) / slack;
|
||||
for i in open.iter().filter(|i| !shared.contains(i)) {
|
||||
if let (Some(height), Some(min)) = (heights.get_mut(*i), mins.get(*i)) {
|
||||
*height -= (*height - min) * take;
|
||||
}
|
||||
}
|
||||
}
|
||||
let free = (room - heights.iter().sum::<f32>()).max(0.0);
|
||||
let share = free / shared.len().max(1) as f32;
|
||||
for i in &shared {
|
||||
if let (Some(height), Some(min)) = (heights.get_mut(*i), mins.get(*i)) {
|
||||
*height = share.max(*min);
|
||||
}
|
||||
}
|
||||
heights
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// A horizontal split with a dock child: the dock takes its columns' width on its physical
|
||||
/// edge, the sibling the rest. False when neither child is a dock.
|
||||
pub(super) fn dock_split(&mut self, ui: &mut Ui, first: &Node<P>, second: &Node<P>, rect: Rect, path: &mut Vec<bool>) -> bool {
|
||||
let (dock_first, side, columns, other) = match (first, second) {
|
||||
(Node::Dock { side, columns }, other) => (true, *side, columns, other),
|
||||
(other, Node::Dock { side, columns }) => (false, *side, columns, other),
|
||||
_ => return false,
|
||||
};
|
||||
let gap = self.style.gap;
|
||||
let room = (rect.width() - gap - self.node_limits(other).min.x).clamp(0.0, rect.width());
|
||||
let width = columns.iter().map(column_width).sum::<f32>().clamp(0.0, room);
|
||||
let (dock, rest) = match edge_of(side, self.style.right_to_left) {
|
||||
Edge::Left => (
|
||||
Rect::from_min_max(rect.min, pos2(rect.left() + width, rect.bottom())),
|
||||
Rect::from_min_max(pos2((rect.left() + width + gap).min(rect.right()), rect.top()), rect.max),
|
||||
),
|
||||
Edge::Right => (
|
||||
Rect::from_min_max(pos2(rect.right() - width, rect.top()), rect.max),
|
||||
Rect::from_min_max(rect.min, pos2((rect.right() - width - gap).max(rect.left()), rect.bottom())),
|
||||
),
|
||||
};
|
||||
self.dock(ui, side, columns, dock, room);
|
||||
path.push(dock_first);
|
||||
self.node(ui, other, rest, path);
|
||||
path.pop();
|
||||
true
|
||||
}
|
||||
|
||||
/// One dock in `rect`, columns from its outer edge. `room` is the widest the dock may get.
|
||||
pub(super) fn dock(&mut self, ui: &mut Ui, side: Side, columns: &[Column<P>], rect: Rect, room: f32) {
|
||||
let edge = edge_of(side, self.style.right_to_left);
|
||||
let (outer, inward) = match edge {
|
||||
Edge::Left => (rect.left(), 1.0),
|
||||
Edge::Right => (rect.right(), -1.0),
|
||||
};
|
||||
if columns.is_empty() {
|
||||
let (band, line) = column_edge(outer, inward, rect.y_range());
|
||||
let band = band.union(Rect::from_x_y_ranges(
|
||||
outer.min(outer + inward * self.style.edge_band)..=outer.max(outer + inward * self.style.edge_band),
|
||||
rect.y_range(),
|
||||
));
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::NewDock { side }, band, line });
|
||||
return;
|
||||
}
|
||||
if usable(rect) {
|
||||
ui.painter().rect_filled(rect, 0.0, self.style.background);
|
||||
}
|
||||
let total: f32 = columns.iter().map(column_width).sum();
|
||||
let mut x = outer;
|
||||
let mut drawn = Vec::new();
|
||||
for (index, column) in columns.iter().enumerate() {
|
||||
let Some(first) = column.groups.first().and_then(|g| g.panels.first()) else { continue };
|
||||
let width = column_width(column).min((rect.width() - (x - outer).abs()).max(0.0));
|
||||
let inner = x + inward * width;
|
||||
let column_rect = Rect::from_x_y_ranges(x.min(inner)..=x.max(inner), rect.y_range());
|
||||
self.column(ui, column, first, column_rect, edge);
|
||||
if index == 0 {
|
||||
let (band, line) = column_edge(x, inward, rect.y_range());
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::ColumnEdge { anchor: first.clone(), after: false }, band, line });
|
||||
}
|
||||
let (band, line) = column_edge(inner, -inward, rect.y_range());
|
||||
self.dock_targets.push(DockDrop { target: DockTarget::ColumnEdge { anchor: first.clone(), after: true }, band, line });
|
||||
drawn.push((column, first, width, inner));
|
||||
x = inner;
|
||||
}
|
||||
// Registered after the columns, so the narrow hit areas win at the edges.
|
||||
for (column, first, width, inner) in drawn {
|
||||
let others = total - column_width(column);
|
||||
let (lo, hi) = if column.iconic { (ICONIC_WIDTH, ICONIC_MAX) } else { self.column_bounds(column) };
|
||||
let hi = hi.min(room - others).max(lo.min(room - others).max(0.0));
|
||||
let lo = lo.min(hi);
|
||||
let divider = Rect::from_x_y_ranges((inner - 1.0)..=(inner + 1.0), rect.y_range());
|
||||
let response = ResizeHandle::new(
|
||||
self.id.with(("column-resize", Some(first))),
|
||||
SplitAxis::Horizontal,
|
||||
divider,
|
||||
&self.style.resize_label,
|
||||
self.handle_style(),
|
||||
)
|
||||
.value(width, lo..=hi)
|
||||
.show(ui);
|
||||
self.register_interaction(ui.ctx(), response.response.id);
|
||||
let next = (width + inward * response.delta).clamp(lo, hi);
|
||||
if response.delta != 0.0 && next != width {
|
||||
self.output.actions.push(if column.iconic {
|
||||
Action::ResizeIconic { panel: first.clone(), width: Some(next) }
|
||||
} else {
|
||||
Action::ResizeColumn { panel: first.clone(), width: next }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_style(&self) -> ResizeHandleStyle {
|
||||
ResizeHandleStyle { hovered: self.style.accent, dragged: self.style.accent, focused: self.style.accent }
|
||||
}
|
||||
|
||||
/// Width bounds of an expanded column from its panels' limits.
|
||||
fn column_bounds(&self, column: &Column<P>) -> (f32, f32) {
|
||||
let mut lo = self.style.min_pane;
|
||||
let mut hi = f32::INFINITY;
|
||||
for panel in column.groups.iter().flat_map(|g| g.panels.iter()) {
|
||||
let limits = self.limits(panel);
|
||||
lo = lo.max(limits.min.x);
|
||||
hi = hi.min(limits.max.x);
|
||||
}
|
||||
(lo, hi.max(lo))
|
||||
}
|
||||
|
||||
/// Least height of an open group: its strip and the tallest minimum of its panels.
|
||||
fn group_min(&self, group: &Group<P>) -> f32 {
|
||||
let body = group.panels.iter().map(|p| self.limits(p).min.y).fold(self.style.min_pane, f32::max);
|
||||
self.style.tab_height + body
|
||||
}
|
||||
|
||||
fn column(&mut self, ui: &mut Ui, column: &Column<P>, first: &P, rect: Rect, edge: Edge) {
|
||||
if !usable(rect) {
|
||||
return;
|
||||
}
|
||||
let bar = Rect::from_min_max(rect.min, pos2(rect.right(), (rect.top() + self.style.column_bar_height).min(rect.bottom())));
|
||||
self.column_bar(ui, column, first, bar, rect, edge);
|
||||
let body = Rect::from_min_max(pos2(rect.left(), bar.bottom()), rect.max);
|
||||
if column.iconic || !usable(body) {
|
||||
ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
|
||||
return;
|
||||
}
|
||||
let sizes: Vec<_> = column.groups.iter().map(|g| (g.minimized, g.height, self.group_min(g))).collect();
|
||||
let heights = group_heights(&sizes, self.style.tab_height, body.height());
|
||||
let total: f32 = heights.iter().sum();
|
||||
let start = self.targets.len();
|
||||
if total > body.height() + 0.5 {
|
||||
let mut child =
|
||||
ui.new_child(UiBuilder::new().id_salt(("dock-column", first)).max_rect(body).layout(egui::Layout::top_down(egui::Align::Min)));
|
||||
child.set_clip_rect(body.intersect(ui.clip_rect()));
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt(self.id.with(("column-scroll", first)))
|
||||
.auto_shrink([false, false])
|
||||
.scroll_source(egui::scroll_area::ScrollSource::SCROLL_BAR | egui::scroll_area::ScrollSource::MOUSE_WHEEL)
|
||||
.show(&mut child, |ui| {
|
||||
let (content, _) = ui.allocate_exact_size(vec2(ui.available_width(), total), Sense::hover());
|
||||
self.column_groups(ui, column, &heights, content, body);
|
||||
});
|
||||
} else {
|
||||
self.column_groups(ui, column, &heights, body, body);
|
||||
}
|
||||
// A group scrolled out of view takes nothing.
|
||||
for target in self.targets.iter_mut().skip(start) {
|
||||
target.rect = target.rect.intersect(body);
|
||||
target.tab_strip = target.tab_strip.map(|strip| strip.intersect(body));
|
||||
for (_, tab) in &mut target.tabs {
|
||||
*tab = tab.intersect(body);
|
||||
}
|
||||
}
|
||||
ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Groups top to bottom in `area` (the scrolled content when the column scrolls), their
|
||||
/// seams and bands clipped to the visible `view`, and the height dividers.
|
||||
fn column_groups(&mut self, ui: &mut Ui, column: &Column<P>, heights: &[f32], area: Rect, view: Rect) {
|
||||
let mut top = area.top();
|
||||
let mut rects = Vec::new();
|
||||
for (group, height) in column.groups.iter().zip(heights) {
|
||||
let rect = Rect::from_min_max(pos2(area.left(), top), pos2(area.right(), top + height));
|
||||
self.group(ui, &group.panels, group.active, group.minimized, rect, GroupKind::Dock);
|
||||
rects.push(rect);
|
||||
top += height;
|
||||
}
|
||||
let xs = view.x_range();
|
||||
let half = self.style.seam_band * 0.5;
|
||||
let push = |targets: &mut Vec<DockDrop<P>>, target: DockTarget<P>, band: Rect, line: Rect| {
|
||||
let band = band.intersect(view);
|
||||
if usable(band) {
|
||||
targets.push(DockDrop { target, band, line: line.intersect(view.expand(2.0)) });
|
||||
}
|
||||
};
|
||||
let mut previous: Option<&P> = None;
|
||||
for (group, rect) in column.groups.iter().zip(&rects) {
|
||||
let Some(anchor) = group.panels.first() else { continue };
|
||||
if let Some(above) = previous {
|
||||
let band = Rect::from_x_y_ranges(xs, (rect.top() - half)..=(rect.top() + half));
|
||||
push(&mut self.dock_targets, DockTarget::Seam { anchor: above.clone() }, band, horizontal_line(rect.top(), xs));
|
||||
}
|
||||
if !group.minimized {
|
||||
let body = Rect::from_min_max(pos2(rect.left(), (rect.top() + self.style.tab_height).min(rect.bottom())), rect.max);
|
||||
let quarter = body.height() * 0.25;
|
||||
let upper = Rect::from_x_y_ranges(xs, body.top()..=(body.top() + quarter));
|
||||
let lower = Rect::from_x_y_ranges(xs, (body.bottom() - quarter)..=body.bottom());
|
||||
push(&mut self.dock_targets, DockTarget::GroupBand { anchor: anchor.clone(), below: false }, upper, horizontal_line(rect.top(), xs));
|
||||
push(
|
||||
&mut self.dock_targets,
|
||||
DockTarget::GroupBand { anchor: anchor.clone(), below: true },
|
||||
lower,
|
||||
horizontal_line(rect.bottom(), xs),
|
||||
);
|
||||
}
|
||||
previous = Some(anchor);
|
||||
}
|
||||
// The room under the last group, when every group is minimized, stacks below it.
|
||||
if let Some(last) = previous
|
||||
&& top < view.bottom()
|
||||
{
|
||||
let band = Rect::from_x_y_ranges(xs, top..=view.bottom());
|
||||
push(&mut self.dock_targets, DockTarget::GroupBand { anchor: last.clone(), below: true }, band, horizontal_line(top, xs));
|
||||
}
|
||||
// Height dividers between an open group and the next open one, after the groups so
|
||||
// their hit areas win at the boundary. Both groups' heights are saved.
|
||||
let open: Vec<usize> = (0..column.groups.len()).filter(|i| column.groups.get(*i).is_some_and(|g| !g.minimized)).collect();
|
||||
for pair in open.windows(2) {
|
||||
let &[i, j] = pair else { continue };
|
||||
let (Some(upper), Some(lower), Some(rect)) = (column.groups.get(i), column.groups.get(j), rects.get(i)) else { continue };
|
||||
let (Some(a), Some(b), Some(first), Some(second)) = (heights.get(i), heights.get(j), upper.panels.first(), lower.panels.first()) else {
|
||||
continue;
|
||||
};
|
||||
let (lo, hi) = (self.group_min(upper), a + b - self.group_min(lower));
|
||||
let hi = hi.max(lo);
|
||||
let divider = Rect::from_x_y_ranges(rect.x_range(), (rect.bottom() - 1.0)..=(rect.bottom() + 1.0));
|
||||
let response = ResizeHandle::new(
|
||||
self.id.with(("group-resize", Some(first))),
|
||||
SplitAxis::Vertical,
|
||||
divider,
|
||||
&self.style.resize_label,
|
||||
self.handle_style(),
|
||||
)
|
||||
.value(*a, lo..=hi)
|
||||
.show(ui);
|
||||
self.register_interaction(ui.ctx(), response.response.id);
|
||||
let next = (a + response.delta).clamp(lo, hi);
|
||||
if response.delta != 0.0 && next != *a {
|
||||
self.output.actions.push(Action::ResizeGroup { panel: first.clone(), height: Some(next) });
|
||||
self.output.actions.push(Action::ResizeGroup { panel: second.clone(), height: Some(a + b - next) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The column bar: the »/« button at its outer end, the rest drags the column (S6),
|
||||
/// toggles iconic on double-click (M2) and opens the column menu (U1).
|
||||
fn column_bar(&mut self, ui: &mut Ui, column: &Column<P>, first: &P, bar: Rect, rect: Rect, edge: Edge) {
|
||||
ui.painter().rect_filled(bar, 0.0, self.style.tab_background);
|
||||
let width = COLLAPSE_WIDTH.min(bar.width());
|
||||
let (button, grip) = match edge {
|
||||
Edge::Left => (
|
||||
Rect::from_min_max(bar.min, pos2(bar.left() + width, bar.bottom())),
|
||||
Rect::from_min_max(pos2(bar.left() + width, bar.top()), bar.max),
|
||||
),
|
||||
Edge::Right => (
|
||||
Rect::from_min_max(pos2(bar.right() - width, bar.top()), bar.max),
|
||||
Rect::from_min_max(bar.min, pos2(bar.right() - width, bar.bottom())),
|
||||
),
|
||||
};
|
||||
let label = if column.iconic { self.style.expand_panels_label.clone() } else { self.style.collapse_to_icons_label.clone() };
|
||||
if usable(button) {
|
||||
let response = ui.interact(button, self.id.with(("column-collapse", Some(first))), Sense::click());
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &label));
|
||||
// The arrows point where the column goes: towards the window edge when it collapses.
|
||||
let glyph = if (edge == Edge::Left) != column.iconic { "«" } else { "»" };
|
||||
let color = if response.hovered() || response.has_focus() { self.style.text } else { self.style.inactive_text };
|
||||
ui.painter().with_clip_rect(button).text(button.center(), egui::Align2::CENTER_CENTER, glyph, self.style.font.clone(), color);
|
||||
if response.clicked() {
|
||||
self.output.actions.push(Action::SetIconic { panel: first.clone(), iconic: !column.iconic });
|
||||
}
|
||||
}
|
||||
if !usable(grip) {
|
||||
return;
|
||||
}
|
||||
let movable = column.groups.iter().flat_map(|g| g.panels.iter()).all(|p| self.permissions(p).movable);
|
||||
let response = ui.interact(grip, self.id.with(("column-bar", Some(first))), Sense::click_and_drag());
|
||||
let response = if movable { response.on_hover_cursor(egui::CursorIcon::Grab) } else { response };
|
||||
self.register_interaction(ui.ctx(), response.id);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), &self.style.move_column_label));
|
||||
if movable && response.drag_started() {
|
||||
let origin = ui.input(|i| i.pointer.press_origin()).or_else(|| ui.ctx().pointer_interact_pos()).unwrap_or(grip.center());
|
||||
let drag = GroupDrag {
|
||||
panel: first.clone(),
|
||||
original: [rect.left(), rect.top(), rect.width(), rect.height()],
|
||||
gesture: GroupGesture::Column,
|
||||
members: column.groups.iter().flat_map(|g| g.panels.iter().cloned()).collect(),
|
||||
origin,
|
||||
source: rect,
|
||||
};
|
||||
ui.ctx().data_mut(|d| d.insert_temp(self.id.with("group-drag"), drag));
|
||||
}
|
||||
if response.double_clicked() {
|
||||
self.output.actions.push(Action::SetIconic { panel: first.clone(), iconic: !column.iconic });
|
||||
}
|
||||
for k in 0..2 {
|
||||
let y = grip.center().y - 1.5 + k as f32 * 3.0;
|
||||
let x = grip.center().x;
|
||||
ui.painter().with_clip_rect(grip).line_segment([pos2(x - 9.0, y), pos2(x + 9.0, y)], Stroke::new(1.0, self.style.inactive_text));
|
||||
}
|
||||
if response.has_focus() || response.hovered() {
|
||||
ui.painter().with_clip_rect(grip).rect_stroke(
|
||||
grip.shrink(1.0),
|
||||
0.0,
|
||||
Stroke::new(1.0, self.style.accent.gamma_multiply(0.5)),
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
let keyboard = response.has_focus() && ui.input(|i| i.key_pressed(Key::Enter) || i.key_pressed(Key::Space));
|
||||
let command = if response.secondary_clicked() || keyboard {
|
||||
Some(egui::SetOpenCommand::Bool(true))
|
||||
} else if response.clicked() {
|
||||
Some(egui::SetOpenCommand::Bool(false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
egui::Popup::menu(&response).open_memory(command).show(|ui| {
|
||||
if self.menu_action(ui, &label) {
|
||||
self.output.actions.push(Action::SetIconic { panel: first.clone(), iconic: !column.iconic });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether a floating window (other than the one following the pointer) or a host overlay
|
||||
/// covers `pointer`. Covered dock targets take nothing.
|
||||
fn dock_covered(&self, ctx: &Context, pointer: Pos2, moving: Option<&P>) -> bool {
|
||||
self.blockers.iter().any(|r| r.contains(pointer))
|
||||
|| self.layout.floating.iter().any(|window| {
|
||||
let Some(first) = window.panels().next() else { return false };
|
||||
!moving.is_some_and(|p| window.contains(p))
|
||||
&& ctx.memory(|m| m.area_rect(self.id.with(("floating", first)))).is_some_and(|r| r.contains(pointer))
|
||||
})
|
||||
}
|
||||
|
||||
/// New docks are offered only where the dock profile is in use: in a layout with a dock.
|
||||
fn has_dock(&self) -> bool {
|
||||
self.layout.dock_path(Side::Start).is_some() || self.layout.dock_path(Side::End).is_some()
|
||||
}
|
||||
|
||||
/// Dock targets, with the new-dock bands along the docked area's left and right edges that
|
||||
/// have no dock yet.
|
||||
fn dock_candidates(&self) -> Vec<DockDrop<P>> {
|
||||
let mut candidates = self.dock_targets.clone();
|
||||
if !self.has_dock() || !usable(self.workspace) {
|
||||
return candidates;
|
||||
}
|
||||
let rtl = self.style.right_to_left;
|
||||
let ys = self.workspace.y_range();
|
||||
for edge in [Edge::Left, Edge::Right] {
|
||||
let side = side_at(edge, rtl);
|
||||
if self.layout.dock_path(side).is_some() {
|
||||
continue;
|
||||
}
|
||||
let (x, inward) = match edge {
|
||||
Edge::Left => (self.workspace.left(), self.style.edge_band),
|
||||
Edge::Right => (self.workspace.right(), -self.style.edge_band),
|
||||
};
|
||||
let band = Rect::from_x_y_ranges(x.min(x + inward)..=x.max(x + inward), ys);
|
||||
let line = vertical_line(x + inward.signum() * 2.0, ys);
|
||||
candidates.push(DockDrop { target: DockTarget::NewDock { side }, band, line });
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
/// The dock target a whole group or column drag would take at `pointer`, with the action
|
||||
/// and its line. A target that leaves the layout unchanged is a no-op: `None` action.
|
||||
pub(super) fn dock_group_proposal(&self, ctx: &Context, drag: &GroupDrag<P>, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
|
||||
let moving = (drag.gesture == GroupGesture::Window).then_some(&drag.panel);
|
||||
if self.dock_covered(ctx, pointer, moving) {
|
||||
return None;
|
||||
}
|
||||
let candidates = self.dock_candidates();
|
||||
let drop = resolve_dock(pointer, &candidates)?;
|
||||
let action = group_action(drag, drop.target.to());
|
||||
self.checked(action).map(|action| (action, drop.line))
|
||||
}
|
||||
|
||||
/// The dock target a single tab drag would take: a new group or column of that panel. A new
|
||||
/// dock only takes a panel that is alone in its group.
|
||||
pub(super) fn dock_tab_proposal(&self, ctx: &Context, panel: &P, pointer: Pos2) -> Option<(Option<Action<P>>, Rect)> {
|
||||
if self.dock_covered(ctx, pointer, None) {
|
||||
return None;
|
||||
}
|
||||
let candidates = self.dock_candidates();
|
||||
let drop = resolve_dock(pointer, &candidates)?;
|
||||
let action = match drop.target.to() {
|
||||
Target::At { anchor, placement } => Action::Move { panel: panel.clone(), anchor, placement },
|
||||
to @ Target::NewDock { .. }
|
||||
if self.layout.place(panel).and_then(|place| self.layout.group(&place)).is_some_and(|g| g.panels.len() == 1) =>
|
||||
{
|
||||
Action::MoveGroup { panel: panel.clone(), to }
|
||||
}
|
||||
Target::NewDock { .. } | Target::Float { .. } => return None,
|
||||
};
|
||||
self.checked(action).map(|action| (action, drop.line))
|
||||
}
|
||||
|
||||
/// `Some(None)` when the action is valid but changes nothing, `None` when it is invalid.
|
||||
fn checked(&self, action: Action<P>) -> Option<Option<Action<P>>> {
|
||||
let mut candidate = self.layout.clone();
|
||||
candidate.apply_with_permissions(action.clone(), |p| self.permissions(p)).ok()?;
|
||||
Some((candidate != *self.layout).then_some(action))
|
||||
}
|
||||
|
||||
/// The insertion line of a dock target.
|
||||
pub(super) fn paint_dock_line(&self, ctx: &Context, line: Rect) {
|
||||
if usable(line) {
|
||||
self.preview_painter(ctx).rect_filled(line, 1.0, self.style.accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The move a group gesture makes: the column for a column bar, else the group.
|
||||
pub(super) fn group_action<P: Clone>(drag: &GroupDrag<P>, to: Target<P>) -> Action<P> {
|
||||
match drag.gesture {
|
||||
GroupGesture::Column => Action::MoveColumn { panel: drag.panel.clone(), to },
|
||||
GroupGesture::Window | GroupGesture::Group => Action::MoveGroup { panel: drag.panel.clone(), to },
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.output.actions.push(Action::SetMinimized { panel: panel.clone(), minimized: !owner.minimized });
|
||||
}
|
||||
}
|
||||
if owner.kind == GroupKind::Dock && self.menu_action(ui, &self.style.collapse_to_icons_label.clone()) {
|
||||
self.output.actions.push(Action::SetIconic { panel: panel.clone(), iconic: true });
|
||||
}
|
||||
if floatable && !matches!(owner.kind, GroupKind::Floating { .. }) && self.menu_action(ui, &self.style.float_group_label.clone()) {
|
||||
let rect = clamp_float(owner.rect.translate(vec2(24.0, 24.0)), ui.ctx().content_rect());
|
||||
self.output.actions.push(Action::MoveGroup {
|
||||
@@ -281,7 +284,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
|
||||
/// A stock menu button with shaped text and its logical accessibility label; true when
|
||||
/// clicked, after closing the menu.
|
||||
fn menu_action(&mut self, ui: &mut Ui, logical: &str) -> bool {
|
||||
pub(super) fn menu_action(&mut self, ui: &mut Ui, logical: &str) -> bool {
|
||||
let text = self.content.action_label(ui, logical, &self.style.font, self.style.text);
|
||||
let response = ui.button(text);
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), logical));
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::layout::{ResizeHandle, ResizeHandleStyle, SplitAxis, SplitSize, split
|
||||
|
||||
use super::{Action, DockContent, DockOutput, DockStyle, DropTarget, Layout, Node, PanelLimits, Permissions};
|
||||
|
||||
mod dock;
|
||||
mod floating;
|
||||
|
||||
pub(in crate::docking) use group::GroupKind;
|
||||
@@ -24,6 +25,8 @@ pub(super) struct Renderer<'a, P> {
|
||||
/// The docked area, for host drop requests.
|
||||
pub(super) workspace: Rect,
|
||||
pub(super) targets: Vec<DropTarget<P>>,
|
||||
/// [D] Seams, group bands, column edges and new-dock bands, see [`crate::docking::targets::DockDrop`].
|
||||
pub(super) dock_targets: Vec<crate::docking::targets::DockDrop<P>>,
|
||||
pub(super) owned_interactions: Vec<Id>,
|
||||
pub(super) output: DockOutput<P>,
|
||||
}
|
||||
@@ -63,6 +66,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
return;
|
||||
}
|
||||
match node {
|
||||
Node::Split { axis: SplitAxis::Horizontal, first, second, .. } if self.dock_split(ui, first, second, rect, path) => {}
|
||||
Node::Split { axis, size, first, second } => {
|
||||
let Some(wanted) = split_rect(rect, *axis, *size, self.style.gap, 0.0) else { return };
|
||||
let (lo, hi) = split_bounds(wanted.available(), *axis, self.node_limits(first), self.node_limits(second));
|
||||
@@ -93,17 +97,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
}
|
||||
Node::Tabs { panels, active } => self.group(ui, panels, *active, false, rect, GroupKind::Frame),
|
||||
Node::Dock { columns, .. } => {
|
||||
// Interim rendering: columns side by side, groups stacked evenly.
|
||||
let total: f32 = columns.iter().map(|c| c.width).sum();
|
||||
let mut left = rect.left();
|
||||
for column in columns {
|
||||
let width = if total > 0.0 { rect.width() * column.width / total } else { 0.0 };
|
||||
let column_rect = Rect::from_min_max(pos2(left, rect.top()), pos2(left + width, rect.bottom()));
|
||||
left += width;
|
||||
self.stacked_groups(ui, &column.groups, column_rect, GroupKind::Dock);
|
||||
}
|
||||
}
|
||||
Node::Dock { side, columns } => self.dock(ui, *side, columns, rect, rect.width()),
|
||||
Node::Stack { entries } => {
|
||||
let total_header = (entries.len() as f32 * self.style.tab_height).min(rect.height());
|
||||
let remaining = (rect.height() - total_header).max(0.0);
|
||||
@@ -220,7 +214,10 @@ pub(in crate::docking) fn stripless<P>(panels: &[P], content: &dyn DockContent<P
|
||||
/// Size policy of a subtree, shared by the renderer and its measurements.
|
||||
pub(in crate::docking) fn node_limits<P>(node: &Node<P>, style: &DockStyle, content: &dyn DockContent<P>) -> PanelLimits {
|
||||
match node {
|
||||
Node::Dock { .. } => PanelLimits { min: Vec2::splat(style.min_pane), ..PanelLimits::default() },
|
||||
Node::Dock { columns, .. } => {
|
||||
let width: f32 = columns.iter().map(dock::column_width).sum();
|
||||
PanelLimits { min: vec2(width.clamp(style.min_pane, 1_000_000.0), style.min_pane), ..PanelLimits::default() }
|
||||
}
|
||||
Node::Tabs { panels, .. } => {
|
||||
let mut limits = PanelLimits::default();
|
||||
for panel in panels {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use egui::{Color32, Context, Key, Pos2, Rect, Stroke, StrokeKind, Vec2, vec2};
|
||||
|
||||
use super::{Renderer, clamp_float};
|
||||
use super::{Renderer, clamp_float, dock::group_action};
|
||||
use crate::docking::{
|
||||
Action, Drag, DropProposal, DropRequest, GroupDrag, GroupGesture, Placement, Target, Zone, cancel_drag, dock_model::Place, resolve_drop,
|
||||
};
|
||||
@@ -32,7 +32,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
&& (down || released)
|
||||
&& let Some(pointer) = pointer
|
||||
&& !self.blockers.iter().any(|r| r.contains(pointer))
|
||||
&& self.host_drop(ctx, &drag.panel, pointer, true, released, ghost.filter(|_| drag.gesture == GroupGesture::Group))
|
||||
&& self.host_drop(ctx, &drag.panel, pointer, true, released, ghost.filter(|_| drag.gesture != GroupGesture::Window))
|
||||
{
|
||||
if released {
|
||||
ctx.data_mut(|d| d.remove::<GroupDrag<P>>(key));
|
||||
@@ -59,11 +59,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) else { return };
|
||||
match drag.gesture {
|
||||
GroupGesture::Window => self.output.group_drop = Some((drag.panel, pointer)),
|
||||
GroupGesture::Group => self.drop_group(ctx, &drag, pointer, float_only),
|
||||
GroupGesture::Group | GroupGesture::Column => self.drop_group(ctx, &drag, pointer, float_only),
|
||||
}
|
||||
} else if let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) {
|
||||
self.output.group_drag = Some(drag.panel.clone());
|
||||
if drag.gesture == GroupGesture::Group {
|
||||
if drag.gesture != GroupGesture::Window {
|
||||
self.paint_group_drag(ctx, &drag, pointer, float_only);
|
||||
}
|
||||
}
|
||||
@@ -83,10 +83,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
});
|
||||
}
|
||||
let proposal = resolve_drop(&drag.panel, pointer, std::slice::from_ref(target))?;
|
||||
let action = Action::MoveGroup {
|
||||
panel: drag.panel.clone(),
|
||||
to: Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() },
|
||||
};
|
||||
let action = group_action(drag, Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() });
|
||||
self.layout.clone().apply_with_permissions(action, |p| self.permissions(p)).is_ok().then_some(proposal)
|
||||
}
|
||||
|
||||
@@ -97,11 +94,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if pointer.distance(drag.origin) < 2.0 {
|
||||
return;
|
||||
}
|
||||
if !float_only && let Some((action, _)) = self.dock_group_proposal(ctx, drag, pointer) {
|
||||
self.output.actions.extend(action);
|
||||
return;
|
||||
}
|
||||
let floating = matches!(self.layout.place(&drag.panel), Some(Place::Floating { .. }));
|
||||
let proposal = if float_only { None } else { self.group_proposal(drag, pointer) };
|
||||
if let Some(proposal) = proposal.as_ref().filter(|p| !p.noop) {
|
||||
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
|
||||
self.output.actions.push(Action::MoveGroup { panel: drag.panel.clone(), to });
|
||||
self.output.actions.push(group_action(drag, to));
|
||||
return;
|
||||
}
|
||||
if proposal.is_some() && !floating {
|
||||
@@ -110,8 +111,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
// No target: hosts that still apply their own policy for a whole-group release get it.
|
||||
self.output.group_drop = Some((drag.panel.clone(), pointer));
|
||||
let rect = clamp_float(drag.source.translate(pointer - drag.origin), ctx.content_rect());
|
||||
let action =
|
||||
Action::MoveGroup { panel: drag.panel.clone(), to: Target::Float { rect: Some([rect.left(), rect.top(), rect.width(), rect.height()]) } };
|
||||
let action = group_action(drag, Target::Float { rect: Some([rect.left(), rect.top(), rect.width(), rect.height()]) });
|
||||
if self.layout.clone().apply_with_permissions(action.clone(), |p| self.permissions(p)).is_ok() {
|
||||
self.output.actions.push(action);
|
||||
}
|
||||
@@ -125,6 +125,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
if float_only {
|
||||
return;
|
||||
}
|
||||
if let Some((action, line)) = self.dock_group_proposal(ctx, drag, pointer) {
|
||||
if action.is_some() {
|
||||
self.paint_dock_line(ctx, line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(proposal) = self.group_proposal(drag, pointer).filter(|p| !p.noop) {
|
||||
let split = matches!(proposal.placement, Placement::Split(zone) if zone != Zone::Center);
|
||||
let fill = if split { self.style.accent.gamma_multiply(0.15) } else { Color32::TRANSPARENT };
|
||||
@@ -170,6 +176,19 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let dock = if float_only || gone { None } else { self.dock_tab_proposal(ctx, &drag.panel, pointer) };
|
||||
if let Some((action, line)) = dock {
|
||||
if released {
|
||||
cancel_drag::<P>(ctx, self.id);
|
||||
if let Some(action) = action.filter(|_| pointer.distance(drag.origin) >= 2.0) {
|
||||
self.output.actions.push(action);
|
||||
self.output.focused = Some(drag.panel);
|
||||
}
|
||||
} else if action.is_some() {
|
||||
self.paint_dock_line(ctx, line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let proposal = if float_only { None } else { resolve_drop(&drag.panel, pointer, &self.targets) };
|
||||
if released {
|
||||
cancel_drag::<P>(ctx, self.id);
|
||||
|
||||
@@ -45,6 +45,19 @@ pub struct DockStyle {
|
||||
/// Painted under every floating window, so it stays opaque when the panel colours are not.
|
||||
pub floating_background: Color32,
|
||||
pub floating_stroke: Stroke,
|
||||
/// [D] Height of the bar above each dock column. It drags the column and holds its »/« button.
|
||||
pub column_bar_height: f32,
|
||||
/// [D] Height of the band around the seam between two stacked groups.
|
||||
pub seam_band: f32,
|
||||
/// [D] Width of the band along the window's left and right edges that makes a new dock.
|
||||
pub edge_band: f32,
|
||||
/// [D] Lay docks out right to left: `Side::Start` is the right edge. egui has no app-wide
|
||||
/// layout direction, so the host sets this from its locale. Frame splits keep their sides.
|
||||
pub right_to_left: bool,
|
||||
/// [D] Accessibility label of a column bar.
|
||||
pub move_column_label: String,
|
||||
pub collapse_to_icons_label: String,
|
||||
pub expand_panels_label: String,
|
||||
}
|
||||
|
||||
impl DockStyle {
|
||||
@@ -77,12 +90,20 @@ impl DockStyle {
|
||||
drag_bar_width: 40.0,
|
||||
floating_background: visuals.window_fill,
|
||||
floating_stroke: visuals.window_stroke,
|
||||
column_bar_height: 14.0,
|
||||
seam_band: 8.0,
|
||||
edge_band: 12.0,
|
||||
right_to_left: false,
|
||||
move_column_label: "Move panel column".into(),
|
||||
collapse_to_icons_label: "Collapse to Icons".into(),
|
||||
expand_panels_label: "Expand Panels".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn valid(&self) -> bool {
|
||||
[self.tab_height, self.gap, self.min_pane, self.border.width, self.font.size, self.drag_bar_width, self.floating_stroke.width]
|
||||
.iter()
|
||||
.chain(&[self.column_bar_height, self.seam_band, self.edge_band])
|
||||
.all(|v| v.is_finite() && (0.0..=4096.0).contains(v))
|
||||
&& self.tab_height > 0.0
|
||||
&& self.font.size > 0.0
|
||||
@@ -148,6 +169,8 @@ pub(super) enum GroupGesture {
|
||||
Window,
|
||||
/// A drag bar or top border: a ghost follows the pointer, the release picks the target.
|
||||
Group,
|
||||
/// [D] A column bar: like `Group`, for the whole dock column.
|
||||
Column,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -378,7 +401,16 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
while let Some((node, path)) = nodes.pop() {
|
||||
match node {
|
||||
Node::Tabs { panels, .. } if group(panels) => return true,
|
||||
Node::Dock { columns, .. } if columns.iter().flat_map(|c| c.groups.iter()).any(|g| group(&g.panels)) => return true,
|
||||
Node::Dock { columns, .. }
|
||||
if columns.iter().flat_map(|c| c.groups.iter()).any(|g| {
|
||||
group(&g.panels)
|
||||
|| ["column-bar", "column-collapse", "column-resize", "group-resize"]
|
||||
.iter()
|
||||
.any(|name| matches(self.id.with((*name, g.panels.first()))))
|
||||
}) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Node::Stack { entries } => {
|
||||
if entries.iter().any(|entry| {
|
||||
matches(self.id.with(("tab", &entry.panel)))
|
||||
@@ -485,6 +517,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> DockArea<P> {
|
||||
layout,
|
||||
workspace: rect,
|
||||
targets: Vec::new(),
|
||||
dock_targets: Vec::new(),
|
||||
owned_interactions: Vec::new(),
|
||||
output: DockOutput::default(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::{Pos2, Rect, pos2};
|
||||
use egui::{Pos2, Rangef, Rect, pos2};
|
||||
|
||||
use super::{Placement, Zone};
|
||||
use super::{Placement, Side, Target, Zone};
|
||||
|
||||
/// One visible group, supplied in back-to-front order to [`resolve_drop`]. An empty tab strip
|
||||
/// or a group that rejects tabs may still accept side splits, e.g. a protected document canvas.
|
||||
@@ -149,3 +149,75 @@ pub fn resolve_drop<P: Clone + Eq>(panel: &P, pointer: Pos2, targets: &[DropTarg
|
||||
noop: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// [D] A dock drop target, built once per frame next to the [`DropTarget`]s. The pointer selects
|
||||
/// it inside `band`; the preview is the line `line`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) struct DockDrop<P> {
|
||||
pub(super) target: DockTarget<P>,
|
||||
pub(super) band: Rect,
|
||||
pub(super) line: Rect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum DockTarget<P> {
|
||||
/// Column edge: a new column beside the column of `anchor`, further from the window edge
|
||||
/// when `after` (P5).
|
||||
ColumnEdge { anchor: P, after: bool },
|
||||
/// The seam between two stacked groups: a new group below the group of `anchor` (P4).
|
||||
Seam { anchor: P },
|
||||
/// The top or bottom quarter of a group body, or the free room under a column's last group (P3).
|
||||
GroupBand { anchor: P, below: bool },
|
||||
/// A window edge without a dock, or an empty dock (P5, P6).
|
||||
NewDock { side: Side },
|
||||
}
|
||||
|
||||
impl<P: Clone> DockTarget<P> {
|
||||
pub(super) fn to(&self) -> Target<P> {
|
||||
match self {
|
||||
Self::ColumnEdge { anchor, after } => Target::At { anchor: anchor.clone(), placement: Placement::Column { after: *after } },
|
||||
Self::Seam { anchor } => Target::At { anchor: anchor.clone(), placement: Placement::Group { below: true } },
|
||||
Self::GroupBand { anchor, below } => Target::At { anchor: anchor.clone(), placement: Placement::Group { below: *below } },
|
||||
Self::NewDock { side } => Target::NewDock { side: *side },
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank in resolution order: column edges reach past their columns, so they come first.
|
||||
fn rank(&self) -> u8 {
|
||||
match self {
|
||||
Self::ColumnEdge { .. } => 0,
|
||||
Self::Seam { .. } => 1,
|
||||
Self::GroupBand { .. } => 2,
|
||||
Self::NewDock { .. } => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The dock target under the pointer: column edges, then seams, then group bands, then new docks.
|
||||
pub(super) fn resolve_dock<P: Clone>(pointer: Pos2, targets: &[DockDrop<P>]) -> Option<&DockDrop<P>> {
|
||||
if !pointer.is_finite() {
|
||||
return None;
|
||||
}
|
||||
targets.iter().filter(|t| usable(t.band) && t.band.contains(pointer)).min_by_key(|t| t.target.rank())
|
||||
}
|
||||
|
||||
/// Width of the insertion lines dock targets preview with.
|
||||
const LINE: f32 = 4.0;
|
||||
/// How far a column edge band reaches into the column, and past the edge out of it.
|
||||
const EDGE_IN: f32 = 6.0;
|
||||
const EDGE_OUT: f32 = 10.0;
|
||||
|
||||
/// Band and line of a column edge at `x`. `inward` is +1 when the column lies right of the edge,
|
||||
/// -1 when it lies left of it. The band reaches further out of the column than into it.
|
||||
pub(super) fn column_edge(x: f32, inward: f32, ys: Rangef) -> (Rect, Rect) {
|
||||
let (a, b) = (x + inward * EDGE_IN, x - inward * EDGE_OUT);
|
||||
(Rect::from_x_y_ranges(a.min(b)..=a.max(b), ys), vertical_line(x, ys))
|
||||
}
|
||||
|
||||
pub(super) fn vertical_line(x: f32, ys: Rangef) -> Rect {
|
||||
Rect::from_x_y_ranges((x - LINE * 0.5)..=(x + LINE * 0.5), ys)
|
||||
}
|
||||
|
||||
pub(super) fn horizontal_line(y: f32, xs: Rangef) -> Rect {
|
||||
Rect::from_x_y_ranges(xs, (y - LINE * 0.5)..=(y + LINE * 0.5))
|
||||
}
|
||||
Reference in new issue
Block a user