UI: stack floating panel groups into sets
Floating panel groups dropped on another group's top or bottom edge join it in a set: the groups stack top to bottom, move together by the top group's title bar (whose × docks them all), and a group dragged by its tab strip leaves the set. A double-click on a tab collapses a group to its tabs. Sets and collapsed groups are kept in preferences and workspaces, and window.panel.float takes below, above and collapsed. Behaviour from public documentation: https://helpx.adobe.com/illustrator/using/workspace-basics.html Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
parent
2ddbd9293f
commit
faa3678a3f
|
|
@ -9,9 +9,16 @@
|
|||
//! × of a group puts its panels back in the dock too. The Tools panel floats the same way by its
|
||||
//! title bar ([`crate::toolbar`]) and docks again on the window's left edge.
|
||||
//!
|
||||
//! `window.panel.float` / `window.panel.dock` do the same for agents. The groups and the Tools
|
||||
//! panel's position are kept with the preferences and in user workspaces, and a position saved on
|
||||
//! a bigger window is clamped into this one when drawn.
|
||||
//! Dropped on the top or bottom edge of another group (a line shows where), a group joins it in a
|
||||
//! set: the groups stack top to bottom, each showing one of its panels, and move together by the
|
||||
//! top group's title bar, whose × docks them all. A group dragged by its tab strip (a lone panel's
|
||||
//! tab, or the strip right of the tabs) leaves its set and floats on its own. A double-click on a
|
||||
//! tab collapses a group to its tabs, and another expands it.
|
||||
//!
|
||||
//! `window.panel.float` (`onto`, `below`, `above`, `collapsed`) / `window.panel.dock` do the same
|
||||
//! for agents. The groups, their sets and the Tools panel's position are kept with the preferences
|
||||
//! and in user workspaces, and a position saved on a bigger window is clamped into this one when
|
||||
//! drawn.
|
||||
|
||||
use egui::{CornerRadius, Id, Pos2, Rect, Sense, Stroke, Vec2, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -24,6 +31,10 @@ use crate::{VectorcraftApp, dock, icons, panels};
|
|||
pub const TITLE: f32 = 14.0;
|
||||
/// Height of a floating group's tab strip.
|
||||
pub const STRIP: f32 = 26.0;
|
||||
/// Height of the bar between two groups of a set.
|
||||
pub const GAP: f32 = 3.0;
|
||||
/// How far from a group's top or bottom edge a drop stacks it above or below the group.
|
||||
const EDGE: f32 = 7.0;
|
||||
/// Where `window.panel.float` floats a group given no position; each further group 24 points
|
||||
/// lower right.
|
||||
const FLOAT_AT: [f32; 2] = [420.0, 120.0];
|
||||
|
|
@ -58,6 +69,10 @@ enum Drop {
|
|||
Dock,
|
||||
/// Stacked with the floating group holding this panel.
|
||||
Stack(&'static str),
|
||||
/// In a set with the floating group holding this panel, below it.
|
||||
Below(&'static str),
|
||||
/// In a set with the floating group holding this panel, above it (the top group of its set).
|
||||
Above(&'static str),
|
||||
}
|
||||
|
||||
fn move_id() -> Id {
|
||||
|
|
@ -80,11 +95,17 @@ pub(crate) fn tools_zone_id() -> Id {
|
|||
Id::new("floating-tools-zone")
|
||||
}
|
||||
|
||||
/// The area of the floating group whose first panel is `first`.
|
||||
/// The area of the floating group whose first panel is `first` (of the set it heads).
|
||||
pub fn area_id(first: &str) -> Id {
|
||||
Id::new(("floating-panels", first))
|
||||
}
|
||||
|
||||
/// The floating group whose first panel is `first`, as last drawn (egui temp memory): in a set,
|
||||
/// from the bar above its tabs to its panel's bottom.
|
||||
pub(crate) fn group_rect_id(first: &str) -> Id {
|
||||
Id::new(("floating-group-rect", first))
|
||||
}
|
||||
|
||||
/// Panel `id` (`window.panel`) as a static string and its English label, if it is a panel.
|
||||
fn panel(id: &str) -> Option<(&'static str, &'static str)> {
|
||||
all_panels().find(|(p, _)| *p == id)
|
||||
|
|
@ -95,6 +116,71 @@ pub fn group_of(ui: &UiState, id: &str) -> Option<usize> {
|
|||
ui.floating_panels.iter().position(|g| g.panels.iter().any(|p| p == id))
|
||||
}
|
||||
|
||||
/// The floating groups drawn with group `gi`, top to bottom: the groups of its set, or it alone.
|
||||
pub fn set_of(ui: &UiState, gi: usize) -> Vec<usize> {
|
||||
match ui.floating_panels.get(gi).and_then(|g| g.column) {
|
||||
Some(c) => ui.floating_panels.iter().enumerate().filter(|(_, g)| g.column == Some(c)).map(|(i, _)| i).collect(),
|
||||
None => vec![gi],
|
||||
}
|
||||
}
|
||||
|
||||
/// Group `gi` heads its set (or floats alone): it draws the set's title bar.
|
||||
fn leads(ui: &UiState, gi: usize) -> bool {
|
||||
set_of(ui, gi).first() == Some(&gi)
|
||||
}
|
||||
|
||||
/// Move the set of group `gi` to `pos`.
|
||||
fn set_pos(ui: &mut UiState, gi: usize, pos: [f32; 2]) {
|
||||
for i in set_of(ui, gi) {
|
||||
if let Some(g) = ui.floating_panels.get_mut(i) {
|
||||
g.pos = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The panels of the groups `set`, in order.
|
||||
fn ids_in(ui: &UiState, set: &[usize]) -> Vec<&'static str> {
|
||||
set.iter().filter_map(|&i| ui.floating_panels.get(i)).flat_map(ids_of).collect()
|
||||
}
|
||||
|
||||
/// The panel shown by group `gi`.
|
||||
fn shown(ui: &UiState, gi: usize) -> Option<&'static str> {
|
||||
let g = ui.floating_panels.get(gi)?;
|
||||
let ids = ids_of(g);
|
||||
ids.get(g.active).or(ids.first()).copied()
|
||||
}
|
||||
|
||||
/// Put the groups `moved` in a set with the group holding `onto` (not one of them), right below or
|
||||
/// right above it, at its position.
|
||||
pub fn attach(ui: &mut UiState, moved: &[usize], onto: &str, below: bool) {
|
||||
let Some(target) = group_of(ui, onto).filter(|t| !moved.contains(t)) else { return };
|
||||
let column = match ui.floating_panels.get(target).and_then(|g| g.column) {
|
||||
Some(c) => c,
|
||||
None => ui.floating_panels.iter().filter_map(|g| g.column).max().map_or(1, |c| c.saturating_add(1)),
|
||||
};
|
||||
let Some(pos) = ui.floating_panels.get(target).map(|g| g.pos) else { return };
|
||||
let (mut taken, mut kept) = (vec![], vec![]);
|
||||
for (i, mut g) in ui.floating_panels.drain(..).enumerate() {
|
||||
if i == target {
|
||||
g.column = Some(column);
|
||||
}
|
||||
if moved.contains(&i) {
|
||||
g.column = Some(column);
|
||||
g.pos = pos;
|
||||
taken.push(g);
|
||||
} else {
|
||||
kept.push(g);
|
||||
}
|
||||
}
|
||||
let at = kept.iter().position(|g| g.panels.iter().any(|p| p == onto)).map_or(kept.len(), |i| if below { i + 1 } else { i });
|
||||
kept.splice(at..at, taken);
|
||||
ui.floating_panels = kept;
|
||||
FloatingPanels::tidy(&mut ui.floating_panels);
|
||||
if let Some(tab) = shown_tab(ui) {
|
||||
ui.dock_tab = tab;
|
||||
}
|
||||
}
|
||||
|
||||
/// The panels of group `g`, as static ids.
|
||||
fn ids_of(g: &FloatingPanels) -> Vec<&'static str> {
|
||||
g.panels.iter().filter_map(|p| panel(p)).map(|(id, _)| id).collect()
|
||||
|
|
@ -118,6 +204,7 @@ fn take(ui: &mut UiState, ids: &[&str]) {
|
|||
g.active = active.and_then(|a| g.panels.iter().position(|p| *p == a)).unwrap_or(0);
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
FloatingPanels::tidy(&mut ui.floating_panels);
|
||||
}
|
||||
|
||||
/// Float `ids` (out of the dock or of the group they were in) as a new group with its top-left
|
||||
|
|
@ -129,7 +216,7 @@ pub fn float(ui: &mut UiState, ids: &[&str], active: &str, pos: Pos2) {
|
|||
}
|
||||
let panels: Vec<String> = ids.iter().map(|id| id.to_string()).collect();
|
||||
let active = panels.iter().position(|p| p == active).unwrap_or(0);
|
||||
ui.floating_panels.push(FloatingPanels { panels, active, pos: [pos.x, pos.y], width: None });
|
||||
ui.floating_panels.push(FloatingPanels { panels, active, pos: [pos.x, pos.y], ..Default::default() });
|
||||
// The tabbed group shows another of its tabs when the one it showed floats.
|
||||
if let Some(tab) = shown_tab(ui) {
|
||||
ui.dock_tab = tab;
|
||||
|
|
@ -214,11 +301,22 @@ fn drop_target(app: &VectorcraftApp, ctx: &egui::Context, what: Moving, at: Pos2
|
|||
let zone = |id: Id| ctx.data(|d| d.get_temp::<Rect>(id)).filter(|r| r.contains(at));
|
||||
let Moving::Panel(id) = what else { return zone(tools_zone_id()).map(|r| (Drop::Dock, r)) };
|
||||
let own = group_of(&app.ui, id);
|
||||
// Another group's title bar and tabs stack with it.
|
||||
let moving = own.map(|i| set_of(&app.ui, i)).unwrap_or_default();
|
||||
// Another group's top or bottom edge puts the moved groups in a set with it; its title bar and
|
||||
// tabs stack them as tabs.
|
||||
for (i, g) in app.ui.floating_panels.iter().enumerate() {
|
||||
let Some((first, _)) = g.panels.first().and_then(|p| panel(p)).filter(|_| Some(i) != own) else { continue };
|
||||
let head = ctx.memory(|m| m.area_rect(area_id(first))).map(|r| Rect::from_min_size(r.min, vec2(r.width(), 1.0 + TITLE + STRIP)));
|
||||
if let Some(head) = head.filter(|h| h.contains(at)) {
|
||||
let Some((first, _)) = g.panels.first().and_then(|p| panel(p)).filter(|_| !moving.contains(&i)) else { continue };
|
||||
let Some(r) = ctx.data(|d| d.get_temp::<Rect>(group_rect_id(first))) else { continue };
|
||||
let line = |y: f32| Rect::from_x_y_ranges(r.x_range(), (y - 2.0)..=(y + 2.0));
|
||||
if Rect::from_x_y_ranges(r.x_range(), (r.bottom() - EDGE)..=(r.bottom() + EDGE)).contains(at) {
|
||||
return Some((Drop::Below(first), line(r.bottom())));
|
||||
}
|
||||
let lead = leads(&app.ui, i);
|
||||
if lead && Rect::from_x_y_ranges(r.x_range(), (r.top() - EDGE)..=r.top()).contains(at) {
|
||||
return Some((Drop::Above(first), line(r.top())));
|
||||
}
|
||||
let head = Rect::from_min_size(r.min, vec2(r.width(), if lead { 1.0 + TITLE + STRIP } else { GAP + STRIP }));
|
||||
if head.contains(at) {
|
||||
return Some((Drop::Stack(first), head));
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +336,13 @@ fn highlight(ctx: &egui::Context, zone: Rect) {
|
|||
painter.rect(zone.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.2), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Show where a drop puts the moved groups in a set: a line over the floating groups.
|
||||
fn insertion_line(ctx: &egui::Context, line: Rect) {
|
||||
let t = Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::new("floating-drop-line")));
|
||||
painter.rect_filled(line, CornerRadius::same(2), t.accent);
|
||||
}
|
||||
|
||||
/// Carry a move on (at the start of a frame, before the panels are drawn): the group or Tools panel
|
||||
/// follows the pointer and the zone it would drop on lights up; the release drops it there.
|
||||
pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
|
|
@ -251,16 +356,18 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
{
|
||||
match m.what {
|
||||
Moving::Panel(id) => {
|
||||
if let Some(g) = group_of(&app.ui, id).and_then(|i| app.ui.floating_panels.get_mut(i)) {
|
||||
g.pos = [p.x, p.y];
|
||||
if let Some(gi) = group_of(&app.ui, id) {
|
||||
set_pos(&mut app.ui, gi, [p.x, p.y]);
|
||||
}
|
||||
}
|
||||
Moving::Tools if app.ui.toolbar_pos.is_some() => app.ui.toolbar_pos = Some([p.x, p.y]),
|
||||
Moving::Tools => {}
|
||||
}
|
||||
}
|
||||
if let Some((_, zone)) = target {
|
||||
highlight(ctx, zone);
|
||||
match target {
|
||||
Some((Drop::Below(_) | Drop::Above(_), line)) => insertion_line(ctx, line),
|
||||
Some((_, zone)) => highlight(ctx, zone),
|
||||
None => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -270,9 +377,11 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
app.ui.toolbar_pos = None;
|
||||
return;
|
||||
};
|
||||
let Some(g) = group_of(&app.ui, id).and_then(|i| app.ui.floating_panels.get(i)) else { return };
|
||||
let ids = ids_of(g);
|
||||
let active = ids.get(g.active).copied().unwrap_or(id);
|
||||
// The moved set: its groups' panels, showing the top group's.
|
||||
let Some(gi) = group_of(&app.ui, id) else { return };
|
||||
let set = set_of(&app.ui, gi);
|
||||
let ids = ids_in(&app.ui, &set);
|
||||
let active = set.first().and_then(|&i| shown(&app.ui, i)).unwrap_or(id);
|
||||
match drop {
|
||||
Drop::Dock => {
|
||||
dock(&mut app.ui, &ids, active);
|
||||
|
|
@ -282,6 +391,8 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
Drop::Stack(onto) => stack(&mut app.ui, &ids, active, onto),
|
||||
Drop::Below(onto) => attach(&mut app.ui, &set, onto, true),
|
||||
Drop::Above(onto) => attach(&mut app.ui, &set, onto, false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,130 +400,217 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
enum Action {
|
||||
/// Show the group's `k`th tab.
|
||||
Activate(usize, usize),
|
||||
/// Its × puts the group's panels back in the dock.
|
||||
Close(usize),
|
||||
/// Move the group holding this panel, drawn at this corner.
|
||||
/// A double-click on a tab collapses the group to its tabs, or expands it.
|
||||
Collapse(usize),
|
||||
/// Its × puts the panels of the set's groups back in the dock.
|
||||
Close(Vec<usize>),
|
||||
/// Move the set of the group holding this panel, drawn at this corner.
|
||||
Move(&'static str, Pos2),
|
||||
/// Take this group out of its set and move it on its own; its tab strip began this far right.
|
||||
Detach(usize, f32),
|
||||
/// Float this panel out of its group; its tab began this far right.
|
||||
Tear(&'static str, f32),
|
||||
}
|
||||
|
||||
/// The floating groups, each a stack of tabs under a title bar. Hidden with the dock (Tab,
|
||||
/// Presentation Mode).
|
||||
/// A floating group as drawn: its index, panels, the one shown and whether it's collapsed.
|
||||
struct Shown {
|
||||
gi: usize,
|
||||
ids: Vec<&'static str>,
|
||||
active: &'static str,
|
||||
collapsed: bool,
|
||||
}
|
||||
|
||||
/// The floating groups, each a stack of tabs, stacked in their sets under a title bar. Hidden with
|
||||
/// the dock (Tab, Presentation Mode).
|
||||
pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if app.ui.floating_panels.is_empty() || !app.ui.dock || app.ui.screen_mode >= 3 {
|
||||
return;
|
||||
}
|
||||
let mut action = None;
|
||||
for gi in 0..app.ui.floating_panels.len() {
|
||||
if leads(&app.ui, gi) {
|
||||
let set = set_of(&app.ui, gi);
|
||||
show_set(app, ctx, &set, &mut action);
|
||||
}
|
||||
}
|
||||
match action {
|
||||
Some(Action::Activate(gi, k)) => {
|
||||
if let Some(g) = app.ui.floating_panels.get_mut(gi)
|
||||
&& g.active != k
|
||||
{
|
||||
g.active = k;
|
||||
g.collapsed = false;
|
||||
}
|
||||
}
|
||||
Some(Action::Collapse(gi)) => {
|
||||
if let Some(g) = app.ui.floating_panels.get_mut(gi) {
|
||||
g.collapsed = !g.collapsed;
|
||||
}
|
||||
}
|
||||
Some(Action::Close(set)) => {
|
||||
let ids = ids_in(&app.ui, &set);
|
||||
let active = set.first().and_then(|&i| shown(&app.ui, i)).unwrap_or_default();
|
||||
dock(&mut app.ui, &ids, active);
|
||||
}
|
||||
Some(Action::Move(id, pos)) => {
|
||||
// From where it is drawn (a position saved on a bigger window was clamped into this one).
|
||||
if let Some(gi) = group_of(&app.ui, id) {
|
||||
set_pos(&mut app.ui, gi, [pos.x, pos.y]);
|
||||
}
|
||||
if let Some(at) = ctx.input(|i| i.pointer.interact_pos()) {
|
||||
start(ctx, Moving::Panel(id), at - pos);
|
||||
}
|
||||
}
|
||||
Some(Action::Detach(gi, left)) => detach(app, ctx, gi, left),
|
||||
Some(Action::Tear(id, left)) => tear(app, ctx, &[id], id, strip_grab(ctx, left)),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take group `gi` out of its set under the pointer, its tab strip having begun `left` points from
|
||||
/// the window's left, and carry on moving it with the pointer.
|
||||
fn detach(app: &mut VectorcraftApp, ctx: &egui::Context, gi: usize, left: f32) {
|
||||
let Some(at) = ctx.input(|i| i.pointer.interact_pos()) else { return };
|
||||
let Some(id) = shown(&app.ui, gi) else { return };
|
||||
let grab = strip_grab(ctx, left);
|
||||
let p = at - grab;
|
||||
if let Some(g) = app.ui.floating_panels.get_mut(gi) {
|
||||
g.column = None;
|
||||
g.pos = [p.x, p.y];
|
||||
}
|
||||
FloatingPanels::tidy(&mut app.ui.floating_panels);
|
||||
start(ctx, Moving::Panel(id), grab);
|
||||
}
|
||||
|
||||
/// Draw the set of floating groups `set` (top to bottom) as one floating box.
|
||||
fn show_set(app: &mut VectorcraftApp, ctx: &egui::Context, set: &[usize], action: &mut Option<Action>) {
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
let pointer = ctx.input(|i| i.pointer.interact_pos());
|
||||
let mut action = None;
|
||||
for gi in 0..app.ui.floating_panels.len() {
|
||||
let Some(g) = app.ui.floating_panels.get(gi) else { break };
|
||||
let ids = ids_of(g);
|
||||
let Some(&first) = ids.first() else { continue };
|
||||
let active = ids.get(g.active).copied().unwrap_or(first);
|
||||
let given = g.width.map(|w| w.min(screen.width() - 16.0));
|
||||
let area = area_id(first);
|
||||
let size = ctx.memory(|m| m.area_rect(area)).map_or(vec2(dock::panel_width(active), 200.0), |r| r.size());
|
||||
let pos = clamp(g.pos, size, screen);
|
||||
// The tabbed group's panels fill (Layers) or scroll (Properties) to near the window's bottom,
|
||||
// leaving room for their margins (a group that only just fitted would creep up a little each
|
||||
// frame).
|
||||
let tall = (screen.bottom() - pos.y - TITLE - STRIP - 60.0).clamp(120.0, 560.0);
|
||||
let lone = ids.len() == 1;
|
||||
egui::Area::new(area).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
let frame = egui::Frame::popup(ui.style()).fill(t.panel).corner_radius(CornerRadius::same(4)).inner_margin(egui::Margin::ZERO);
|
||||
frame.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::ZERO;
|
||||
let tabs: Vec<_> = ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let label = panel(id).map_or(*id, |(_, label)| label);
|
||||
let color = if *id == active { t.text_strong } else { t.text_dim };
|
||||
ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), color)
|
||||
})
|
||||
.collect();
|
||||
let tabs_width: f32 = tabs.iter().map(|g| g.size().x + 24.0).sum();
|
||||
let width = ids.iter().map(|id| dock::panel_width(id)).fold(tabs_width + 32.0, f32::max).max(given.unwrap_or(0.0));
|
||||
ui.set_width(width);
|
||||
// Title bar: a grip that moves the group, and the × that docks it.
|
||||
let (bar, _) = ui.allocate_exact_size(vec2(width, TITLE), Sense::hover());
|
||||
ui.painter().rect_filled(bar, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.tab_strip);
|
||||
let close = Rect::from_center_size(bar.right_center() - vec2(10.0, 0.0), vec2(TITLE, TITLE));
|
||||
let grip = ui.interact(Rect::from_min_max(bar.min, pos2(close.left(), bar.bottom())), ui.id().with("title"), Sense::drag());
|
||||
for k in 0..2 {
|
||||
let y = bar.center().y - 1.5 + k as f32 * 3.0;
|
||||
ui.painter().line_segment([pos2(bar.center().x - 9.0, y), pos2(bar.center().x + 9.0, y)], Stroke::new(0.6, t.text_disabled));
|
||||
}
|
||||
if grip.drag_started() {
|
||||
action = Some(Action::Move(active, pos));
|
||||
}
|
||||
grip.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to move"));
|
||||
let x = ui.interact(close, ui.id().with("close"), Sense::click());
|
||||
icons::paint(ui, "x", close.shrink(3.0), if x.hovered() { t.text_strong } else { t.text_dim });
|
||||
if x.on_hover_text(tl!("Put back in the dock")).clicked() {
|
||||
action = Some(Action::Close(gi));
|
||||
}
|
||||
// Tab strip: a click shows a tab, a drag out of the strip floats it on its own.
|
||||
let groups: Vec<Shown> = set
|
||||
.iter()
|
||||
.filter_map(|&gi| {
|
||||
let g = app.ui.floating_panels.get(gi)?;
|
||||
let ids = ids_of(g);
|
||||
let active = ids.get(g.active).or(ids.first()).copied()?;
|
||||
Some(Shown { gi, ids, active, collapsed: g.collapsed })
|
||||
})
|
||||
.collect();
|
||||
let Some((lead, first)) = groups.first().and_then(|g| Some((g, *g.ids.first()?))) else { return };
|
||||
let Some(saved) = app.ui.floating_panels.get(lead.gi).map(|g| g.pos) else { return };
|
||||
let given = set.iter().filter_map(|&i| app.ui.floating_panels.get(i)?.width).reduce(f32::max).map(|w| w.min(screen.width() - 16.0));
|
||||
let area = area_id(first);
|
||||
let size = ctx.memory(|m| m.area_rect(area)).map_or(vec2(dock::panel_width(lead.active), 200.0), |r| r.size());
|
||||
let pos = clamp(saved, size, screen);
|
||||
let alone = groups.len() == 1;
|
||||
// The tabbed group's panels fill (Layers) or scroll (Properties) to near the window's bottom,
|
||||
// leaving room for their margins (a group that only just fitted would creep up a little each
|
||||
// frame); the open panels of a set share that height.
|
||||
let n = groups.len() as f32;
|
||||
let open = groups.iter().filter(|g| !g.collapsed).count().max(1) as f32;
|
||||
let tall = ((screen.bottom() - pos.y - TITLE - n * STRIP - (n - 1.0) * GAP - 60.0) / open).clamp(120.0, 560.0);
|
||||
let lead_active = lead.active;
|
||||
egui::Area::new(area).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
let frame = egui::Frame::popup(ui.style()).fill(t.panel).corner_radius(CornerRadius::same(4)).inner_margin(egui::Margin::ZERO);
|
||||
frame.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = Vec2::ZERO;
|
||||
let tabs: Vec<Vec<_>> = groups
|
||||
.iter()
|
||||
.map(|g| {
|
||||
g.ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let label = panel(id).map_or(*id, |(_, label)| label);
|
||||
let color = if *id == g.active { t.text_strong } else { t.text_dim };
|
||||
ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), color)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let width = groups
|
||||
.iter()
|
||||
.zip(&tabs)
|
||||
.map(|(g, tabs)| {
|
||||
let tabs_width: f32 = tabs.iter().map(|tab| tab.size().x + 24.0).sum();
|
||||
g.ids.iter().map(|id| dock::panel_width(id)).fold(tabs_width + 32.0, f32::max)
|
||||
})
|
||||
.fold(given.unwrap_or(0.0), f32::max);
|
||||
ui.set_width(width);
|
||||
// Title bar: a grip that moves the set, and the × that docks it.
|
||||
let (bar, _) = ui.allocate_exact_size(vec2(width, TITLE), Sense::hover());
|
||||
ui.painter().rect_filled(bar, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.tab_strip);
|
||||
let close = Rect::from_center_size(bar.right_center() - vec2(10.0, 0.0), vec2(TITLE, TITLE));
|
||||
let grip = ui.interact(Rect::from_min_max(bar.min, pos2(close.left(), bar.bottom())), ui.id().with("title"), Sense::drag());
|
||||
for k in 0..2 {
|
||||
let y = bar.center().y - 1.5 + k as f32 * 3.0;
|
||||
ui.painter().line_segment([pos2(bar.center().x - 9.0, y), pos2(bar.center().x + 9.0, y)], Stroke::new(0.6, t.text_disabled));
|
||||
}
|
||||
if grip.drag_started() {
|
||||
*action = Some(Action::Move(lead_active, pos));
|
||||
}
|
||||
grip.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to move"));
|
||||
let x = ui.interact(close, ui.id().with("close"), Sense::click());
|
||||
icons::paint(ui, "x", close.shrink(3.0), if x.hovered() { t.text_strong } else { t.text_dim });
|
||||
if x.on_hover_text(tl!("Put back in the dock")).clicked() {
|
||||
*action = Some(Action::Close(set.to_vec()));
|
||||
}
|
||||
for (k, (g, tabs)) in groups.iter().zip(tabs).enumerate() {
|
||||
let top = if k == 0 {
|
||||
bar.top()
|
||||
} else {
|
||||
let (gap, _) = ui.allocate_exact_size(vec2(width, GAP), Sense::hover());
|
||||
ui.painter().rect_filled(gap, 0.0, t.tab_strip);
|
||||
gap.top()
|
||||
};
|
||||
// Tab strip: a click shows a tab, a double-click collapses the group, a drag out of
|
||||
// the strip floats a tab on its own; a lone tab or the strip right of the tabs moves
|
||||
// the group (out of its set).
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(width, STRIP), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.panel_darker);
|
||||
let lone = g.ids.len() == 1;
|
||||
let mut left = strip.left();
|
||||
for (k, (&id, galley)) in ids.iter().zip(tabs).enumerate() {
|
||||
for (j, (&id, galley)) in g.ids.iter().zip(tabs).enumerate() {
|
||||
let r = Rect::from_min_size(pos2(left, strip.top()), vec2(galley.size().x + 24.0, STRIP));
|
||||
left = r.right();
|
||||
let resp = ui.interact(r, ui.id().with(("tab", id)), Sense::click_and_drag());
|
||||
if id == active {
|
||||
if id == g.active && !g.collapsed {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
}
|
||||
ui.painter().galley(pos2(r.left() + 12.0, r.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
if resp.clicked() {
|
||||
action = Some(Action::Activate(gi, k));
|
||||
if resp.double_clicked() {
|
||||
*action = Some(Action::Collapse(g.gi));
|
||||
} else if resp.clicked() {
|
||||
*action = Some(Action::Activate(g.gi, j));
|
||||
} else if lone && alone && resp.drag_started() {
|
||||
*action = Some(Action::Move(id, pos));
|
||||
} else if lone && resp.drag_started() {
|
||||
action = Some(Action::Move(id, pos));
|
||||
*action = Some(Action::Detach(g.gi, strip.left()));
|
||||
} else if !lone && resp.dragged() && pointer.is_some_and(|p| !strip.expand(4.0).contains(p)) {
|
||||
action = Some(Action::Tear(id, r.left()));
|
||||
*action = Some(Action::Tear(id, r.left()));
|
||||
}
|
||||
}
|
||||
let menu = Rect::from_center_size(strip.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0));
|
||||
let rest = Rect::from_min_max(pos2(left, strip.top()), pos2(menu.left() - 4.0, strip.bottom()));
|
||||
if rest.width() > 0.0 {
|
||||
let resp = ui.interact(rest, ui.id().with("strip"), Sense::drag());
|
||||
let resp = ui.interact(rest, ui.id().with(("strip", g.gi)), Sense::drag());
|
||||
if resp.drag_started() {
|
||||
action = Some(Action::Move(active, pos));
|
||||
*action = Some(if alone { Action::Move(g.active, pos) } else { Action::Detach(g.gi, strip.left()) });
|
||||
}
|
||||
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to move"));
|
||||
let tip = if alone { tl!("Drag to move") } else { tl!("Drag to move out of the set") };
|
||||
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tip);
|
||||
}
|
||||
panels::panel_menu(app, ui, active, menu);
|
||||
dock::panel_body(app, ui, active, width, tall);
|
||||
});
|
||||
panels::panel_menu(app, ui, g.active, menu);
|
||||
if !g.collapsed {
|
||||
dock::panel_body(app, ui, g.active, width, tall);
|
||||
}
|
||||
let bottom = ui.min_rect().bottom().max(strip.bottom());
|
||||
if let Some(&id) = g.ids.first() {
|
||||
let rect = Rect::from_min_max(pos2(strip.left(), top), pos2(strip.left() + width, bottom));
|
||||
ui.ctx().data_mut(|d| d.insert_temp(group_rect_id(id), rect));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
match action {
|
||||
Some(Action::Activate(gi, k)) => {
|
||||
if let Some(g) = app.ui.floating_panels.get_mut(gi) {
|
||||
g.active = k;
|
||||
}
|
||||
}
|
||||
Some(Action::Close(gi)) => {
|
||||
if let Some(g) = app.ui.floating_panels.get(gi) {
|
||||
let ids = ids_of(g);
|
||||
let active = ids.get(g.active).copied().unwrap_or_default();
|
||||
dock(&mut app.ui, &ids, active);
|
||||
}
|
||||
}
|
||||
Some(Action::Move(id, pos)) => {
|
||||
// From where it is drawn (a position saved on a bigger window was clamped into this one).
|
||||
if let Some(g) = group_of(&app.ui, id).and_then(|i| app.ui.floating_panels.get_mut(i)) {
|
||||
g.pos = [pos.x, pos.y];
|
||||
}
|
||||
if let Some(at) = pointer {
|
||||
start(ctx, Moving::Panel(id), at - pos);
|
||||
}
|
||||
}
|
||||
Some(Action::Tear(id, left)) => tear(app, ctx, &[id], id, strip_grab(ctx, left)),
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// `window.panel.float` and `window.panel.dock`; `None` for other commands.
|
||||
|
|
@ -455,35 +653,82 @@ fn float_or_dock(app: &mut VectorcraftApp, float_it: bool, p: &Value) -> Result<
|
|||
(true, None) if DockTab::from_id(id).is_some() => docked_tabs(&app.ui).map(|t| t.info().0).collect(),
|
||||
_ => vec![id],
|
||||
};
|
||||
let collapsed = crate::menus::opt_bool(p, "collapsed")?;
|
||||
let onto = p.get("onto").and_then(Value::as_str);
|
||||
let below = p.get("below").and_then(Value::as_str);
|
||||
let above = p.get("above").and_then(Value::as_str);
|
||||
if [onto, below, above].iter().filter(|o| o.is_some()).count() > 1 {
|
||||
return Err("give only one of onto, below and above".into());
|
||||
}
|
||||
app.ui.dock = true;
|
||||
if !float_it {
|
||||
dock(&mut app.ui, &ids, id);
|
||||
return Ok(json!({ "panel": id, "floating": false }));
|
||||
}
|
||||
match p.get("onto").and_then(Value::as_str) {
|
||||
if let Some((next, under)) = below.map(|b| (b, true)).or(above.map(|a| (a, false))) {
|
||||
let next = crate::menus::normalize_panel(next)
|
||||
.filter(|o| !ids.contains(o) && group_of(&app.ui, o).is_some())
|
||||
.ok_or("below and above must name a panel floating in another group")?;
|
||||
// The panels as a group of their own (their group already, or floated out), in the set.
|
||||
let own = own.filter(|&i| app.ui.floating_panels.get(i).is_some_and(|g| g.panels.len() == ids.len()));
|
||||
if own.is_none() {
|
||||
float(&mut app.ui, &ids, id, Pos2::ZERO);
|
||||
}
|
||||
if let Some(gi) = group_of(&app.ui, id) {
|
||||
attach(&mut app.ui, &[gi], next, under);
|
||||
}
|
||||
} else {
|
||||
float_onto(app, &ids, id, own, onto, at)?;
|
||||
}
|
||||
if let Some(g) = group_of(&app.ui, id).and_then(|i| app.ui.floating_panels.get_mut(i)) {
|
||||
g.active = g.panels.iter().position(|q| q == id).unwrap_or(g.active);
|
||||
if let Some(c) = collapsed {
|
||||
g.collapsed = c;
|
||||
}
|
||||
}
|
||||
let gi = group_of(&app.ui, id);
|
||||
let g = gi.and_then(|i| app.ui.floating_panels.get(i));
|
||||
let set: Vec<&Vec<String>> =
|
||||
gi.map(|i| set_of(&app.ui, i)).unwrap_or_default().iter().filter_map(|&i| app.ui.floating_panels.get(i)).map(|g| &g.panels).collect();
|
||||
let (group, pos, collapsed) = (g.map(|g| &g.panels), g.map(|g| g.pos), g.is_some_and(|g| g.collapsed));
|
||||
Ok(json!({ "panel": id, "floating": true, "group": group, "set": set, "pos": pos, "collapsed": collapsed }))
|
||||
}
|
||||
|
||||
/// Float `ids` (`id` among them; `own`, the group holding `id`) as their own group at `at`, or
|
||||
/// stacked as tabs `onto` another group.
|
||||
fn float_onto(
|
||||
app: &mut VectorcraftApp,
|
||||
ids: &[&'static str],
|
||||
id: &'static str,
|
||||
own: Option<usize>,
|
||||
onto: Option<&str>,
|
||||
at: Option<Pos2>,
|
||||
) -> Result<(), String> {
|
||||
match onto {
|
||||
Some(onto) => {
|
||||
let onto = crate::menus::normalize_panel(onto)
|
||||
.filter(|o| !ids.contains(o) && group_of(&app.ui, o).is_some_and(|g| Some(g) != own))
|
||||
.ok_or("onto must name a panel floating in another group")?;
|
||||
stack(&mut app.ui, &ids, id, onto);
|
||||
stack(&mut app.ui, ids, id, onto);
|
||||
}
|
||||
// Its own group already: moved there (or left where it is), showing the panel.
|
||||
None => match own.and_then(|i| app.ui.floating_panels.get_mut(i)).filter(|g| g.panels.len() == ids.len()) {
|
||||
// Moved to `at` takes it out of its set; else it stays where it is.
|
||||
Some(g) => {
|
||||
g.active = g.panels.iter().position(|p| p == id).unwrap_or(g.active);
|
||||
if let Some(at) = at {
|
||||
g.pos = [at.x, at.y];
|
||||
g.column = None;
|
||||
FloatingPanels::tidy(&mut app.ui.floating_panels);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let n = app.ui.floating_panels.len() as f32;
|
||||
let at = at.unwrap_or(pos2(FLOAT_AT[0] + 24.0 * n, FLOAT_AT[1] + 24.0 * n));
|
||||
float(&mut app.ui, &ids, id, at);
|
||||
float(&mut app.ui, ids, id, at);
|
||||
}
|
||||
},
|
||||
}
|
||||
let g = group_of(&app.ui, id).and_then(|i| app.ui.floating_panels.get(i));
|
||||
Ok(json!({ "panel": id, "floating": true, "group": g.map(|g| &g.panels), "pos": g.map(|g| g.pos) }))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -775,7 +1020,7 @@ mod tests {
|
|||
}))
|
||||
.unwrap();
|
||||
let edited = edited.sanitized();
|
||||
assert_eq!(edited.floating_panels, [FloatingPanels { panels: vec!["layers".into()], active: 0, pos: [1.0, 2.0], width: None }]);
|
||||
assert_eq!(edited.floating_panels, [FloatingPanels { panels: vec!["layers".into()], active: 0, pos: [1.0, 2.0], ..Default::default() }]);
|
||||
h.settle();
|
||||
let g = h.group("layers");
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
|
||||
|
|
@ -811,6 +1056,149 @@ mod tests {
|
|||
assert!(app.ui.toolbar && app.ui.toolbar_pos == Some(TOOLS_AT), "floating shows the Tools panel");
|
||||
}
|
||||
|
||||
/// The groups' sets: each group's panels and its column, in list order.
|
||||
fn sets(groups: &[FloatingPanels]) -> Vec<(Vec<&str>, Option<u32>)> {
|
||||
groups.iter().map(|g| (g.panels.iter().map(String::as_str).collect(), g.column)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn groups_dropped_on_an_edge_stack_into_a_set_that_moves_together_and_tears_apart() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.panel.float", json!({"panel": "color", "x": 500, "y": 150})).unwrap();
|
||||
h.app.run("window.panel.float", json!({"panel": "swatches", "x": 950, "y": 450})).unwrap();
|
||||
h.settle();
|
||||
// Swatches' title bar held over Color's bottom edge: a line shows it goes below.
|
||||
let color = h.temp_rect(group_rect_id("color"));
|
||||
let below = pos2(color.center().x, color.bottom() + 2.0);
|
||||
h.hold(h.title("swatches"), below);
|
||||
let target = drop_target(&h.app, &h.ctx, Moving::Panel("swatches"), below);
|
||||
assert_eq!(target.map(|t| t.0), Some(Drop::Below("color")));
|
||||
assert!(target.is_some_and(|t| t.1.height() <= 4.0), "a line, not a box: {target:?}");
|
||||
h.release(below);
|
||||
let s = sets(&h.app.ui.floating_panels);
|
||||
assert_eq!((s[0].0.clone(), s[1].0.clone()), (vec!["color"], vec!["swatches"]), "Color, then Swatches");
|
||||
assert!(s[0].1.is_some() && s[0].1 == s[1].1, "one set: {s:?}");
|
||||
assert_eq!(h.app.ui.floating_panels[0].pos, h.app.ui.floating_panels[1].pos, "at the set's corner");
|
||||
// Drawn as one box: Swatches right under Color.
|
||||
let (color, swatches) = (h.temp_rect(group_rect_id("color")), h.temp_rect(group_rect_id("swatches")));
|
||||
assert!((swatches.top() - color.bottom()).abs() < 1.0 && swatches.left() == color.left(), "{color:?} {swatches:?}");
|
||||
assert!(h.group("color").contains_rect(swatches.shrink(1.0)), "in Color's area");
|
||||
// The set moves by its title bar.
|
||||
let to = pos2(300.0, 120.0);
|
||||
let grab = h.title("color") - h.group("color").min;
|
||||
h.drag(h.title("color"), to);
|
||||
let moved = [to.x - grab.x, to.y - grab.y];
|
||||
assert!(h.app.ui.floating_panels.iter().all(|g| (g.pos[0] - moved[0]).abs() < 1.0 && (g.pos[1] - moved[1]).abs() < 1.0));
|
||||
assert_eq!(sets(&h.app.ui.floating_panels).len(), 2);
|
||||
// Swatches' lone tab dragged away takes it out of the set: both float on their own.
|
||||
let swatches = h.temp_rect(group_rect_id("swatches"));
|
||||
let tab = pos2(swatches.left() + 20.0, swatches.top() + GAP + STRIP / 2.0);
|
||||
let away = pos2(1000.0, 600.0);
|
||||
h.drag(tab, away);
|
||||
assert_eq!(sets(&h.app.ui.floating_panels), [(vec!["color"], None), (vec!["swatches"], None)]);
|
||||
assert!(h.group("swatches").contains(away), "under the pointer: {:?}", h.group("swatches"));
|
||||
// Dropped on Color's top edge, it goes above Color, which it now heads.
|
||||
let color = h.temp_rect(group_rect_id("color"));
|
||||
let above = pos2(color.center().x, color.top() - 3.0);
|
||||
h.hold(h.title("swatches"), above);
|
||||
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("swatches"), above).map(|t| t.0), Some(Drop::Above("color")));
|
||||
h.release(above);
|
||||
let s = sets(&h.app.ui.floating_panels);
|
||||
assert_eq!((s[0].0.clone(), s[1].0.clone()), (vec!["swatches"], vec!["color"]));
|
||||
assert!(s[0].1.is_some() && s[0].1 == s[1].1);
|
||||
// A group's tab strip still takes tabs: Stroke dropped on Color's tabs joins Color's group.
|
||||
h.app.run("window.panel.float", json!({"panel": "stroke", "x": 1000, "y": 650})).unwrap();
|
||||
h.settle();
|
||||
let color = h.temp_rect(group_rect_id("color"));
|
||||
let tabs = pos2(color.center().x, color.top() + GAP + STRIP / 2.0);
|
||||
h.drag(h.title("stroke"), tabs);
|
||||
assert_eq!(sets(&h.app.ui.floating_panels).iter().map(|s| s.0.clone()).collect::<Vec<_>>(), [vec!["swatches"], vec!["color", "stroke"]]);
|
||||
// The set's × docks every group.
|
||||
let g = h.group("swatches");
|
||||
h.click(pos2(g.right() - 11.0, g.top() + 1.0 + TITLE / 2.0));
|
||||
assert!(h.groups().is_empty(), "docked: {:?}", h.groups());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_click_on_a_tab_collapses_a_floating_group_and_another_expands_it() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.panel.float", json!({"panel": "layers", "x": 500, "y": 150})).unwrap();
|
||||
h.app.run("window.panel.float", json!({"panel": "stroke", "below": "layers"})).unwrap();
|
||||
h.settle();
|
||||
let open = h.temp_rect(group_rect_id("layers"));
|
||||
let tab = pos2(open.left() + 20.0, open.top() + 1.0 + TITLE + STRIP / 2.0);
|
||||
let double = |h: &mut Harness| {
|
||||
// Well after any earlier click, so the first click doesn't make a double-click with it.
|
||||
h.time += 1.0;
|
||||
h.frame(vec![Event::PointerMoved(tab), button(tab, true)]);
|
||||
h.frame(vec![button(tab, false)]);
|
||||
h.frame(vec![button(tab, true)]);
|
||||
h.frame(vec![button(tab, false)]);
|
||||
h.settle();
|
||||
};
|
||||
double(&mut h);
|
||||
assert!(h.app.ui.floating_panels[0].collapsed, "collapsed");
|
||||
let shut = h.temp_rect(group_rect_id("layers"));
|
||||
assert!(shut.height() < 1.0 + TITLE + STRIP + 2.0, "only its tabs: {shut:?}");
|
||||
assert!(h.temp_rect(group_rect_id("stroke")).top() < open.bottom(), "Stroke moved up under it");
|
||||
double(&mut h);
|
||||
assert!(!h.app.ui.floating_panels[0].collapsed, "expanded");
|
||||
// Collapsed by command, Window › Layers expands it.
|
||||
h.app.run("window.panel.float", json!({"panel": "layers", "collapsed": true})).unwrap();
|
||||
assert!(h.app.ui.floating_panels[0].collapsed);
|
||||
h.app.run("window.panel", json!({"panel": "layers"})).unwrap();
|
||||
assert!(!h.app.ui.floating_panels[0].collapsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_by_command_are_checked_and_kept_in_workspaces_and_preferences() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.panel.float", json!({"panel": "layers", "x": 100, "y": 50})).unwrap();
|
||||
assert!(app.run("window.panel.float", json!({"panel": "color", "below": "swatches"})).is_err(), "Swatches isn't floating");
|
||||
assert!(app.run("window.panel.float", json!({"panel": "color", "onto": "layers", "below": "layers"})).is_err(), "one of them");
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "below": "layers"})).is_err(), "not below itself");
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "collapsed": "yes"})).is_err());
|
||||
assert_eq!(app.ui.floating_panels.len(), 1, "nothing floated by the errors");
|
||||
// Color floats straight into the set, below Layers; Stroke above it.
|
||||
let r = app.run("window.panel.float", json!({"panel": "color", "below": "Layers"})).unwrap();
|
||||
assert_eq!(r["set"], json!([["layers"], ["color"]]));
|
||||
let r = app.run("window.panel.float", json!({"panel": "stroke", "above": "color"})).unwrap();
|
||||
assert_eq!(r["set"], json!([["layers"], ["stroke"], ["color"]]));
|
||||
assert!(app.ui.floating_panels.iter().all(|g| g.pos == [100.0, 50.0]), "at the set's corner");
|
||||
// Stroke's whole group moves to the set's bottom.
|
||||
app.run("window.panel.float", json!({"panel": "stroke", "below": "color"})).unwrap();
|
||||
assert_eq!(sets(&app.ui.floating_panels).iter().map(|s| s.0.clone()).collect::<Vec<_>>(), [vec!["layers"], vec!["color"], vec!["stroke"]]);
|
||||
// Kept in a workspace and the preferences.
|
||||
app.run("window.panel.float", json!({"panel": "color", "collapsed": true})).unwrap();
|
||||
app.run("window.workspace.new", json!({"name": "Sets"})).unwrap();
|
||||
let saved = app.ui.floating_panels.clone();
|
||||
app.run("window.workspace", json!({"name": "Essentials"})).unwrap();
|
||||
assert!(app.ui.floating_panels.is_empty());
|
||||
app.run("window.workspace", json!({"name": "Sets"})).unwrap();
|
||||
assert_eq!(app.ui.floating_panels, saved);
|
||||
let prefs: UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
assert_eq!(prefs.sanitized().floating_panels, saved);
|
||||
// Moved to x, y, a group leaves its set; the set left with one group floats on its own.
|
||||
let r = app.run("window.panel.float", json!({"panel": "color", "x": 600, "y": 400})).unwrap();
|
||||
assert_eq!((r["set"].clone(), r["pos"].clone()), (json!([["color"]]), json!([600.0, 400.0])));
|
||||
app.run("window.panel.dock", json!({"panel": "layers"})).unwrap();
|
||||
assert_eq!(sets(&app.ui.floating_panels), [(vec!["stroke"], None), (vec!["color"], None)]);
|
||||
// A hand-edited file: a set's groups kept together at its first group's corner, a lone one alone.
|
||||
let edited: UiState = serde_json::from_value(json!({
|
||||
"floating_panels": [
|
||||
{"panels": ["layers"], "pos": [1, 2], "column": 4},
|
||||
{"panels": ["color"], "pos": [9, 9]},
|
||||
{"panels": ["stroke"], "pos": [5, 6], "column": 4},
|
||||
{"panels": ["swatches"], "pos": [7, 8], "column": 9},
|
||||
],
|
||||
}))
|
||||
.unwrap();
|
||||
let edited = edited.sanitized();
|
||||
let s = sets(&edited.floating_panels);
|
||||
assert_eq!(s, [(vec!["layers"], Some(4)), (vec!["stroke"], Some(4)), (vec!["color"], None), (vec!["swatches"], None)]);
|
||||
assert_eq!(edited.floating_panels[1].pos, [1.0, 2.0]);
|
||||
}
|
||||
|
||||
fn floating_pos(app: &VectorcraftApp, id: &str) -> [f32; 2] {
|
||||
group_of(&app.ui, id).map(|i| app.ui.floating_panels[i].pos).unwrap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,6 +628,7 @@
|
|||
% of all commits % všech commitů
|
||||
Lines +/− Řádky +/−
|
||||
Drag to move Přetažením přesunete
|
||||
Drag to move out of the set Přetažením přesunete mimo sadu
|
||||
Drag to float as a toolbar Přetažením vytvoříte plovoucí panel nástrojů
|
||||
Put back in the toolbar Vrátit do panelu nástrojů
|
||||
Float Panel Plovoucí panel
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3317,6 +3317,7 @@ axis Horizontal Horizontal
|
|||
axis Vertical Vertical
|
||||
axis Both Ambos
|
||||
Drag to move Arrastrar para mover
|
||||
Drag to move out of the set Arrastrar para sacar del conjunto
|
||||
Drag to float as a toolbar Arrastrar para flotar como barra de herramientas
|
||||
Put back in the toolbar Devolver a la barra de herramientas
|
||||
Float Panel Hacer flotar el panel
|
||||
|
|
@ -3873,6 +3874,8 @@ axis Both Ambos
|
|||
@msg tool (a tool id) is required se requiere tool (un id de herramienta)
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} no está en un grupo de la barra de herramientas con varias herramientas
|
||||
@msg onto must name a panel floating in another group onto debe nombrar un panel que flote en otro grupo
|
||||
@msg below and above must name a panel floating in another group below y above deben nombrar un panel que flote en otro grupo
|
||||
@msg give only one of onto, below and above indica solo uno de onto, below y above
|
||||
@msg x and y must both be numbers x e y deben ser números
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}" no es un color: escríbelo como #rrggbb
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead esto no es un logotipo de colores planos ({_1}% de sus zonas sólidas coinciden con unos pocos colores planos), así que el modo Logotipo plano convertiría sus degradados en manchas: usa el modo Color
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3314,6 +3314,7 @@ axis Horizontal Horizontal
|
|||
axis Vertical Vertical
|
||||
axis Both Les deux
|
||||
Drag to move Faire glisser pour déplacer
|
||||
Drag to move out of the set Faire glisser pour sortir de l’ensemble
|
||||
Drag to float as a toolbar Faire glisser pour détacher en barre d’outils flottante
|
||||
Put back in the toolbar Remettre dans la barre d’outils
|
||||
Float Panel Rendre le panneau flottant
|
||||
|
|
@ -3870,6 +3871,8 @@ axis Both Les deux
|
|||
@msg tool (a tool id) is required tool (un identifiant d’outil) est obligatoire
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} ne fait pas partie d’un groupe de plusieurs outils dans la barre d’outils
|
||||
@msg onto must name a panel floating in another group onto doit désigner un panneau flottant dans un autre groupe
|
||||
@msg below and above must name a panel floating in another group below et above doivent désigner un panneau flottant dans un autre groupe
|
||||
@msg give only one of onto, below and above indiquez un seul de onto, below et above
|
||||
@msg x and y must both be numbers x et y doivent tous deux être des nombres
|
||||
@msg "{0}" is not a colour: write it as #rrggbb « {0} » n’est pas une couleur : écrivez-la sous la forme #rrggbb
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead ce n’est pas un logo en aplats ({_1} % de ses zones pleines correspondent à quelques couleurs unies), donc le mode Logo en aplats transformerait ses dégradés en taches : utilisez le mode Couleur
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3317,6 +3317,7 @@ axis Horizontal Orizzontale
|
|||
axis Vertical Verticale
|
||||
axis Both Entrambi
|
||||
Drag to move Trascina per spostare
|
||||
Drag to move out of the set Trascina per spostare fuori dall’insieme
|
||||
Drag to float as a toolbar Trascina per rendere mobile come barra degli strumenti
|
||||
Put back in the toolbar Riporta nella barra degli strumenti
|
||||
Float Panel Rendi mobile pannello
|
||||
|
|
@ -3873,6 +3874,8 @@ axis Both Entrambi
|
|||
@msg tool (a tool id) is required tool (un id di strumento) è obbligatorio
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} non fa parte di un gruppo di più strumenti nella barra degli strumenti
|
||||
@msg onto must name a panel floating in another group onto deve indicare un pannello mobile in un altro gruppo
|
||||
@msg below and above must name a panel floating in another group below e above devono indicare un pannello mobile in un altro gruppo
|
||||
@msg give only one of onto, below and above indica solo uno tra onto, below e above
|
||||
@msg x and y must both be numbers x e y devono essere entrambi numeri
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}" non è un colore: scrivilo come #rrggbb
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead questo non è un logo a colori piatti ({_1}% delle sue aree piene corrisponde a pochi colori piatti), quindi la modalità Logo piatto trasformerebbe i suoi sfumati in macchie: usa la modalità Colore
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3265,6 +3265,7 @@ axis Horizontal 水平方向
|
|||
axis Vertical 垂直方向
|
||||
axis Both 両方
|
||||
Drag to move 移動するにはドラッグ
|
||||
Drag to move out of the set ドラッグしてセットから出す
|
||||
Drag to float as a toolbar ドラッグしてツールバーとして浮かせる
|
||||
Put back in the toolbar ツールバーに戻す
|
||||
Float Panel パネルをフローティング
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2697,6 +2697,7 @@
|
|||
% of all commits % de todos os commits
|
||||
Lines +/− Linhas +/−
|
||||
Drag to move Arraste para mover
|
||||
Drag to move out of the set Arraste para tirar do conjunto
|
||||
Drag to float as a toolbar Arraste para flutuar como barra de ferramentas
|
||||
Put back in the toolbar Devolver à barra de ferramentas
|
||||
Float Panel Flutuar Painel
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3313,6 +3313,7 @@ axis Horizontal Горизонтально
|
|||
axis Vertical Вертикально
|
||||
axis Both Оба
|
||||
Drag to move Перетащите для перемещения
|
||||
Drag to move out of the set Перетащите, чтобы вынести из набора
|
||||
Drag to float as a toolbar Перетащите, чтобы сделать плавающей панелью инструментов
|
||||
Put back in the toolbar Вернуть на панель инструментов
|
||||
Float Panel Сделать панель плавающей
|
||||
|
|
@ -3869,6 +3870,8 @@ axis Both Оба
|
|||
@msg tool (a tool id) is required требуется tool (идентификатор инструмента)
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} не входит в группу панели инструментов из нескольких инструментов
|
||||
@msg onto must name a panel floating in another group onto должен называть панель, плавающую в другой группе
|
||||
@msg below and above must name a panel floating in another group below и above должны называть панель, плавающую в другой группе
|
||||
@msg give only one of onto, below and above укажите только один из onto, below и above
|
||||
@msg x and y must both be numbers x и y должны быть числами
|
||||
@msg "{0}" is not a colour: write it as #rrggbb «{0}» не является цветом: запишите его как #rrggbb
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead это не логотип из плоских цветов ({_1}% его сплошных областей совпадает с несколькими плоскими цветами), поэтому режим «Плоский логотип» превратил бы градиенты в пятна: используйте режим «Цвет»
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3305,6 +3305,7 @@ axis Horizontal Горизонтальна
|
|||
axis Vertical Вертикальна
|
||||
axis Both Обидві
|
||||
Drag to move Перетягніть для переміщення
|
||||
Drag to move out of the set Перетягніть, щоб винести з набору
|
||||
Drag to float as a toolbar Перетягніть, щоб від’єднати панель інструментів
|
||||
Put back in the toolbar Повернути на панель інструментів
|
||||
Float Panel Від’єднати панель
|
||||
|
|
@ -3856,6 +3857,8 @@ axis Both Обидві
|
|||
@msg tool (a tool id) is required потрібно tool (ідентифікатор інструменту)
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} не входить до групи панелі інструментів з кількох інструментів
|
||||
@msg onto must name a panel floating in another group onto має називати панель, що плаває в іншій групі
|
||||
@msg below and above must name a panel floating in another group below і above мають називати панель, що плаває в іншій групі
|
||||
@msg give only one of onto, below and above вкажіть лише один із onto, below і above
|
||||
@msg x and y must both be numbers x і y мають бути числами
|
||||
@msg "{0}" is not a colour: write it as #rrggbb "{0}" не є кольором: запишіть його як #rrggbb
|
||||
@msg this isn't a flat-colour logo ({_1}% of its solid areas match a few flat colours), so Flat Logo mode would turn its gradients into patches: use Color mode instead це не логотип із плоских кольорів ({_1}% його суцільних областей збігається з кількома плоскими кольорами), тому режим «Плоский логотип» перетворив би градієнти на плями: використовуйте режим «Колір»
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -3517,6 +3517,7 @@ axis Horizontal 水平
|
|||
axis Vertical 垂直
|
||||
axis Both 两者
|
||||
Drag to move 拖动以移动
|
||||
Drag to move out of the set 拖动以移出该组合
|
||||
Drag to float as a toolbar 拖动使其浮动为工具栏
|
||||
Put back in the toolbar 放回工具栏
|
||||
Float Panel 浮动面板
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3295,6 +3295,7 @@ axis Horizontal 水平
|
|||
axis Vertical 垂直
|
||||
axis Both 兩者
|
||||
Drag to move 拖曳以移動
|
||||
Drag to move out of the set 拖曳以移出該組合
|
||||
Drag to float as a toolbar 拖曳使其浮動為工具列
|
||||
Put back in the toolbar 放回工具列
|
||||
Float Panel 浮動面板
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -260,7 +260,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"window.panel.float",
|
||||
"Float Panel",
|
||||
"",
|
||||
"{panel: id or \"tools\", x?, y?, onto?: id, group?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it with the floating group holding that panel instead; `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved. \"tools\" floats the Tools panel. Returns {panel, floating, group, pos}",
|
||||
"{panel: id or \"tools\", x?, y?, onto?: id, below?: id, above?: id, group?: bool, collapsed?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it as a tab with the floating group holding that panel instead; `below` / `above` puts its group in a set with that group, right below or above it, as dropping it on that group's bottom or top edge does (the set's groups stack top to bottom and move together); `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved (out of its set, given x, y); `collapsed` shows its group's tabs only, as a double-click on a tab does. \"tools\" floats the Tools panel. Returns {panel, floating, group, set, pos, collapsed}",
|
||||
),
|
||||
(
|
||||
"window.panel.dock",
|
||||
|
|
@ -1126,6 +1126,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
&& let Some(g) = crate::floating::group_of(&app.ui, p).and_then(|i| app.ui.floating_panels.get_mut(i))
|
||||
{
|
||||
g.active = g.panels.iter().position(|q| q == p).unwrap_or(g.active);
|
||||
g.collapsed = false;
|
||||
let out = json!({ "floating": g.panels });
|
||||
app.ui.dock = true;
|
||||
return Some(Ok(out));
|
||||
|
|
|
|||
|
|
@ -434,7 +434,12 @@ pub struct TaskBarPlace {
|
|||
}
|
||||
|
||||
/// A group of panels dragged out of the dock: it floats as a stack of tabs, one panel shown.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
///
|
||||
/// Groups sharing a `column` float as one set, stacked top to bottom in list order, like the panel
|
||||
/// groups of a dock in Illustrator's workspace
|
||||
/// (<https://helpx.adobe.com/illustrator/using/workspace-basics.html>): the set moves by the top
|
||||
/// group's title bar, and a group dragged by its tab strip leaves it.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FloatingPanels {
|
||||
/// Panel ids (`window.panel`), in tab order.
|
||||
pub panels: Vec<String>,
|
||||
|
|
@ -446,6 +451,13 @@ pub struct FloatingPanels {
|
|||
/// A width the group was given (the Tabs panel sized over its text), else its panels' own.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub width: Option<f32>,
|
||||
/// The set this group is stacked in with others (the same number), top to bottom in list
|
||||
/// order; none when it floats on its own.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column: Option<u32>,
|
||||
/// Collapsed to its tabs (a double-click on a tab), its panel hidden.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub collapsed: bool,
|
||||
}
|
||||
|
||||
impl FloatingPanels {
|
||||
|
|
@ -458,10 +470,35 @@ impl FloatingPanels {
|
|||
g.active = g.active.min(g.panels.len().saturating_sub(1));
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
Self::tidy(groups);
|
||||
if toolbar_pos.is_some_and(|p| !p.iter().all(|v| v.is_finite())) {
|
||||
*toolbar_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep each set's groups together in the list, after its first one and in the order they
|
||||
/// come, at its first one's position; a set left with one group floats on its own.
|
||||
pub fn tidy(groups: &mut Vec<FloatingPanels>) {
|
||||
let mut out: Vec<FloatingPanels> = Vec::with_capacity(groups.len());
|
||||
for mut g in groups.drain(..) {
|
||||
match g.column.and_then(|c| out.iter().rposition(|o| o.column == Some(c))) {
|
||||
Some(i) => {
|
||||
if let Some(first) = out.iter().find(|o| o.column == g.column) {
|
||||
g.pos = first.pos;
|
||||
}
|
||||
out.insert(i + 1, g);
|
||||
}
|
||||
None => out.push(g),
|
||||
}
|
||||
}
|
||||
let alone: Vec<u32> = out.iter().filter_map(|g| g.column).filter(|c| out.iter().filter(|g| g.column == Some(*c)).count() < 2).collect();
|
||||
for g in &mut out {
|
||||
if g.column.is_some_and(|c| alone.contains(&c)) {
|
||||
g.column = None;
|
||||
}
|
||||
}
|
||||
*groups = out;
|
||||
}
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
|
|
|
|||
|
|
@ -260,13 +260,20 @@ Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title
|
|||
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
|
||||
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;
|
||||
dropped on another group's title bar or tabs it stacks with that group, and dropped on the dock (lit up while the
|
||||
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column.
|
||||
`window.panel.float {panel, x?, y?, onto?, group?}` floats a panel (with `group`, its whole group) with its top-left
|
||||
corner at `x`, `y` (window points; default cascaded), or stacks it with the floating group holding `onto`;
|
||||
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on another
|
||||
group's bottom or top edge (a line shows where), a group joins it in a set: the groups stack top to bottom, each
|
||||
showing one of its panels, and move together by the top group's title bar, whose × docks them all; a group dragged by
|
||||
its tab strip (a lone panel's tab or the strip right of the tabs) leaves the set. A double-click on a tab collapses a
|
||||
group to its tabs, and another expands it.
|
||||
`window.panel.float {panel, x?, y?, onto?, below?, above?, group?, collapsed?}` floats a panel (with `group`, its whole
|
||||
group) with its top-left corner at `x`, `y` (window points; default cascaded), or stacks it as a tab with the floating
|
||||
group holding `onto`, or puts its group in a set right `below` or `above` the group holding that panel; `collapsed`
|
||||
collapses or expands its group;
|
||||
`window.panel.dock {panel, group?}` puts it back. `panel` takes the `window.panel` ids and labels, and `"tools"` for
|
||||
the Tools panel, which floats by its title bar and docks on the window's left edge. `window.panel` on a floating
|
||||
panel shows its tab. `ui.floating_panels` in `ui.inspect` lists the groups (panel ids, the index of the tab shown,
|
||||
the top-left corner) and `ui.toolbar_pos` the Tools panel's corner (null: docked); both are saved with the
|
||||
panel shows its tab (expanding its group). `ui.floating_panels` in `ui.inspect` lists the groups (panel ids, the
|
||||
index of the tab shown, the top-left corner, `column`: the set it is stacked in, top to bottom in list order, and
|
||||
`collapsed`) and `ui.toolbar_pos` the Tools panel's corner (null: docked); both are saved with the
|
||||
preferences and in user workspaces (the built-in workspaces dock everything), and a corner saved on a bigger window
|
||||
is clamped into this one when drawn. Floating panels stay inside the app window (no separate OS windows).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue