Merge pull request #513 from storytold/fix/issue-495
Fix #495: panels, panel groups and the Tools panel undock into floating panels
This commit is contained in:
commit
0e59e0a38d
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@ use serde_json::json;
|
|||
|
||||
use crate::state::{DockTab, ICON_PANEL_GROUPS, ICON_PANELS};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, icons, panels, widgets};
|
||||
use crate::{VectorcraftApp, floating, icons, panels, widgets};
|
||||
|
||||
const ICON_COL: f32 = 38.0;
|
||||
/// Height of the strip along the top of a dock column that carries its double arrow.
|
||||
|
|
@ -57,18 +57,26 @@ fn toggle(app: &mut VectorcraftApp, collapsed: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
/// One icon of the column: a click pops its panel out (or puts it away).
|
||||
fn panel_icon(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, label: &str, icon: &str) {
|
||||
/// One icon of the column: a click pops its panel out (or puts it away); dragged out of the
|
||||
/// column (`column`), the panel floats.
|
||||
fn panel_icon(app: &mut VectorcraftApp, ui: &mut Ui, id: &'static str, label: &str, icon: &str, column: Rect) {
|
||||
let open = app.ui.open_panel.as_deref() == Some(id);
|
||||
if widgets::icon_button(ui, icon, label, open, 30.0).clicked() {
|
||||
// Its own id: the icons below it move up when it floats, and mustn't take over its drag.
|
||||
let (_, rect) = ui.allocate_space(vec2(30.0, 30.0));
|
||||
let resp = widgets::paint_icon_button(ui, ui.interact(rect, ui.id().with(("panel-icon", id)), Sense::click_and_drag()), icon, label, open);
|
||||
if resp.clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
if resp.dragged() && ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| !column.contains(p)) {
|
||||
floating::tear(app, ui.ctx(), &[id], id, floating::strip_grab(ui.ctx(), resp.rect.left()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
// Main tabbed group, unless it is collapsed to icons.
|
||||
if !app.ui.dock_collapsed {
|
||||
let shown = floating::shown_tab(&app.ui);
|
||||
// Main tabbed group, unless it is collapsed to icons or all its panels float.
|
||||
let group = shown.filter(|_| !app.ui.dock_collapsed).map(|active| {
|
||||
egui::Panel::right("dock")
|
||||
.resizable(true)
|
||||
.default_size(DOCK_WIDTH)
|
||||
|
|
@ -78,47 +86,31 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
if collapse_header(ui, false) {
|
||||
toggle(app, true);
|
||||
}
|
||||
// Tab strip.
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
|
||||
let mut x = strip.left();
|
||||
for tab in DockTab::ALL {
|
||||
let (_, label, _) = tab.info();
|
||||
let active = app.ui.dock_tab == tab;
|
||||
let galley =
|
||||
ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.5), if active { t.text_strong } else { t.text_dim });
|
||||
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(galley.size().x + 24.0, strip.height() - 1.0));
|
||||
let resp = ui.interact(r, ui.id().with(("docktab", label)), Sense::click());
|
||||
if active {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
}
|
||||
ui.painter().galley(egui::pos2(r.left() + 12.0, r.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
ui.painter().line_segment([r.right_top(), r.right_bottom()], Stroke::new(1.0, t.border));
|
||||
if resp.clicked() {
|
||||
app.ui.dock_tab = tab;
|
||||
}
|
||||
x = r.right();
|
||||
if tab_strip(app, ui, active) {
|
||||
// It floats from this frame on.
|
||||
return;
|
||||
}
|
||||
let menu_id = app.ui.dock_tab.info().0;
|
||||
panels::panel_menu(app, ui, menu_id, egui::Rect::from_center_size(strip.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0)));
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 12, right: 10, top: 10, bottom: 8 }).show(ui, |ui| match app.ui.dock_tab {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 12, right: 10, top: 10, bottom: 8 }).show(ui, |ui| match active {
|
||||
DockTab::Properties => {
|
||||
egui::ScrollArea::vertical().id_salt("props").auto_shrink([false, false]).show(ui, |ui| panels::properties::show(app, ui));
|
||||
}
|
||||
DockTab::Layers => panels::layers::show(app, ui),
|
||||
DockTab::Libraries => panels::libraries(app, ui),
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.response
|
||||
.rect
|
||||
});
|
||||
// Collapsed icon-panel strip, left of the expanded panel group (like Illustrator's dock). A
|
||||
// collapsed group's panels head it, under the « that expands them again.
|
||||
let collapsed = app.ui.dock_collapsed;
|
||||
// collapsed group's panels head it, under the « that expands them again. Floating panels leave
|
||||
// it.
|
||||
let collapsed = app.ui.dock_collapsed && shown.is_some();
|
||||
let column = egui::Panel::right("icon_column")
|
||||
.resizable(false)
|
||||
.exact_size(ICON_COL)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.5, t.border)))
|
||||
.show(ui, |ui| {
|
||||
let bounds = ui.max_rect();
|
||||
if collapsed && collapse_header(ui, true) {
|
||||
toggle(app, false);
|
||||
}
|
||||
|
|
@ -129,41 +121,98 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
if collapsed {
|
||||
for tab in DockTab::ALL {
|
||||
let (id, label, icon) = tab.info();
|
||||
panel_icon(app, ui, id, label, icon);
|
||||
if floating::group_of(&app.ui, id).is_none() {
|
||||
panel_icon(app, ui, id, label, icon, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (gi, group) in ICON_PANEL_GROUPS.iter().enumerate() {
|
||||
if gi > 0 || collapsed {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 7.0), Sense::hover());
|
||||
ui.painter()
|
||||
.line_segment([r.left_center() + vec2(4.0, 0.0), r.right_center() - vec2(4.0, 0.0)], Stroke::new(1.0, t.divider));
|
||||
}
|
||||
// A divider above each group with icons left, but the first.
|
||||
let mut divider = collapsed;
|
||||
for group in ICON_PANEL_GROUPS {
|
||||
let mut first = true;
|
||||
for id in group.iter() {
|
||||
let Some((_, label, icon)) = ICON_PANELS.iter().find(|p| p.0 == *id) else { continue };
|
||||
panel_icon(app, ui, id, label, icon);
|
||||
if floating::group_of(&app.ui, id).is_some() {
|
||||
continue;
|
||||
}
|
||||
if std::mem::take(&mut first) && std::mem::replace(&mut divider, true) {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 7.0), Sense::hover());
|
||||
ui.painter()
|
||||
.line_segment([r.left_center() + vec2(4.0, 0.0), r.right_center() - vec2(4.0, 0.0)], Stroke::new(1.0, t.divider));
|
||||
}
|
||||
panel_icon(app, ui, id, label, icon, bounds);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let left = column.response.rect.left();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(column_left_id(), left));
|
||||
let column = column.response.rect;
|
||||
let dock = group.map_or(column, |g| g.union(column));
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(column_left_id(), column.left());
|
||||
d.insert_temp(floating::dock_rect_id(), dock);
|
||||
});
|
||||
}
|
||||
|
||||
/// The tabbed group's tab strip, showing `active`: a click on a tab shows it, a tab dragged out of
|
||||
/// the strip floats on its own, and the strip right of the tabs floats the whole group. True when
|
||||
/// the shown panel floats now.
|
||||
fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
|
||||
let out = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| !strip.contains(p));
|
||||
// What a drag out of the strip floats: the panels, the one shown, where its tab began.
|
||||
let mut tear: Option<(Vec<&'static str>, &'static str, f32)> = None;
|
||||
let mut x = strip.left();
|
||||
for tab in DockTab::ALL {
|
||||
let (id, label, _) = tab.info();
|
||||
if floating::group_of(&app.ui, id).is_some() {
|
||||
continue;
|
||||
}
|
||||
let galley =
|
||||
ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.5), if tab == active { t.text_strong } else { t.text_dim });
|
||||
let r = Rect::from_min_size(egui::pos2(x, strip.top()), vec2(galley.size().x + 24.0, strip.height() - 1.0));
|
||||
let resp = ui.interact(r, ui.id().with(("docktab", label)), Sense::click_and_drag());
|
||||
if tab == active {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
}
|
||||
ui.painter().galley(egui::pos2(r.left() + 12.0, r.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
ui.painter().line_segment([r.right_top(), r.right_bottom()], Stroke::new(1.0, t.border));
|
||||
if resp.clicked() {
|
||||
app.ui.dock_tab = tab;
|
||||
} else if resp.dragged() && out {
|
||||
tear = Some((vec![id], id, r.left()));
|
||||
}
|
||||
x = r.right();
|
||||
}
|
||||
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(egui::pos2(x, strip.top()), egui::pos2(menu.left() - 4.0, strip.bottom()));
|
||||
if rest.width() > 0.0 {
|
||||
let resp = ui.interact(rest, ui.id().with("dock-group-bar"), Sense::drag());
|
||||
if resp.dragged() && out {
|
||||
tear = Some((floating::docked_tabs(&app.ui).map(|t| t.info().0).collect(), active.info().0, strip.left()));
|
||||
}
|
||||
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to float the panel group"));
|
||||
}
|
||||
panels::panel_menu(app, ui, active.info().0, menu);
|
||||
if let Some((ids, id, left)) = tear {
|
||||
floating::tear(app, ui.ctx(), &ids, id, floating::strip_grab(ui.ctx(), left));
|
||||
}
|
||||
// Torn off, or floated from its menu.
|
||||
floating::group_of(&app.ui, active.info().0).is_some()
|
||||
}
|
||||
|
||||
/// A panel popped out next to the icon column: an icon panel, or one of the tabbed group's panels
|
||||
/// while the dock is collapsed.
|
||||
/// while the dock is collapsed. Dragging its title floats it.
|
||||
pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(id) = app.ui.open_panel.clone() else { return };
|
||||
let tab = DockTab::from_id(&id);
|
||||
let label = match tab {
|
||||
Some(tab) if app.ui.dock_collapsed => tab.info().1,
|
||||
// Expanded, the group shows its panels in the dock itself.
|
||||
Some(_) => return,
|
||||
None => match ICON_PANELS.iter().find(|p| p.0 == id) {
|
||||
Some((_, label, _)) => *label,
|
||||
None => return,
|
||||
},
|
||||
};
|
||||
let Some((id, label)) = app.ui.open_panel.as_deref().and_then(|p| crate::state::all_panels().find(|(q, _)| *q == p)) else { return };
|
||||
let tab = DockTab::from_id(id);
|
||||
// Expanded, the group shows its panels in the dock itself; a floating panel shows in its group.
|
||||
if tab.is_some() && !app.ui.dock_collapsed || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
// Pinned by its right edge, 6 points left of the icon column wherever the dock's width (or its
|
||||
|
|
@ -179,52 +228,39 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
let right = column - 6.0;
|
||||
// The tabbed group's panels fill their height (Layers) or scroll (Properties): give them one
|
||||
// that keeps the flyout on screen.
|
||||
// The tabbed group's panels keep the width the dock gives them; icon panels are 256 points.
|
||||
let width = if tab.is_some() { DOCK_WIDTH } else { 256.0 };
|
||||
let width = panel_width(id);
|
||||
let tall = (screen.height() - FLYOUT_TOP - 26.0 - 20.0 - 40.0).clamp(160.0, 560.0);
|
||||
let mut open = true;
|
||||
let mut torn = None;
|
||||
let area = egui::Area::new(egui::Id::new("icon-panel")).order(egui::Order::Foreground).pivot(egui::Align2::RIGHT_TOP);
|
||||
area.fixed_pos(egui::pos2(right, FLYOUT_TOP)).show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).fill(t.panel).corner_radius(CornerRadius::same(4)).inner_margin(egui::Margin::ZERO).show(ui, |ui| {
|
||||
ui.set_width(width);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(width, 26.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker);
|
||||
let title = egui::Rect::from_min_size(
|
||||
let title = Rect::from_min_size(
|
||||
strip.min,
|
||||
vec2(ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0),
|
||||
);
|
||||
ui.painter().rect_filled(title, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel);
|
||||
ui.painter().text(title.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, tl!(label), theme::semibold(12.0), t.text);
|
||||
let close = egui::Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0));
|
||||
let close = Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0));
|
||||
let cr = ui.interact(close, ui.id().with("close-panel"), Sense::click());
|
||||
icons::paint(ui, "chevrons-right", close, if cr.hovered() { t.text } else { t.text_dim });
|
||||
if cr.clicked() {
|
||||
open = false;
|
||||
}
|
||||
let menu_r = egui::Rect::from_center_size(strip.right_center() - vec2(34.0, 0.0), vec2(16.0, 16.0));
|
||||
panels::panel_menu(app, ui, &id, menu_r);
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(width - 20.0);
|
||||
match tab {
|
||||
Some(DockTab::Properties) => {
|
||||
// At least `tall` when the sections are taller: the flyout's area keeps the
|
||||
// size its content needs, and egui sizes a new area at most 400 points high,
|
||||
// so a scroll area that only took the room it was given never grew past it.
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("props-flyout")
|
||||
.max_height(tall)
|
||||
.min_scrolled_height(tall)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| panels::properties::show(app, ui));
|
||||
}
|
||||
Some(DockTab::Layers) => {
|
||||
ui.set_max_height(tall);
|
||||
panels::layers::show(app, ui);
|
||||
}
|
||||
Some(DockTab::Libraries) => panels::libraries(app, ui),
|
||||
None => panels::show_icon_panel(app, ui, &id),
|
||||
}
|
||||
});
|
||||
let menu_r = Rect::from_center_size(strip.right_center() - vec2(34.0, 0.0), vec2(16.0, 16.0));
|
||||
panels::panel_menu(app, ui, id, menu_r);
|
||||
let drag = ui.interact(title, ui.id().with("title"), Sense::drag()).on_hover_cursor(egui::CursorIcon::Grab);
|
||||
if drag.dragged() && ui.input(|i| i.pointer.is_decidedly_dragging()) {
|
||||
torn = Some(title.left());
|
||||
}
|
||||
// Torn off, or floated from its menu: it floats from this frame on.
|
||||
if torn.is_some() || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
panel_body(app, ui, id, width, tall);
|
||||
});
|
||||
});
|
||||
// User Interface › Auto-Collapse Iconic Panels (#394): a press away from the flyout puts it
|
||||
|
|
@ -240,11 +276,46 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
open = false;
|
||||
}
|
||||
}
|
||||
if !open {
|
||||
if let Some(left) = torn {
|
||||
floating::tear(app, ctx, &[id], id, floating::strip_grab(ctx, left));
|
||||
} else if !open {
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The width of a popped-out or floating panel: the tabbed group's panels keep the width the dock
|
||||
/// gives them; icon panels are 256 points.
|
||||
pub(crate) fn panel_width(id: &str) -> f32 {
|
||||
if DockTab::from_id(id).is_some() { DOCK_WIDTH } else { 256.0 }
|
||||
}
|
||||
|
||||
/// Panel `id`'s contents in a popped-out or floating panel `width` wide; the tabbed group's panels
|
||||
/// fill (Layers) or scroll (Properties) `tall` points.
|
||||
pub(crate) fn panel_body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: f32, tall: f32) {
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(width - 20.0);
|
||||
match DockTab::from_id(id) {
|
||||
Some(DockTab::Properties) => {
|
||||
// At least `tall` when the sections are taller: the flyout's area keeps the size its
|
||||
// content needs, and egui sizes a new area at most 400 points high, so a scroll area
|
||||
// that only took the room it was given never grew past it.
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("props-flyout")
|
||||
.max_height(tall)
|
||||
.min_scrolled_height(tall)
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| panels::properties::show(app, ui));
|
||||
}
|
||||
Some(DockTab::Layers) => {
|
||||
ui.set_max_height(tall);
|
||||
panels::layers::show(app, ui);
|
||||
}
|
||||
Some(DockTab::Libraries) => panels::libraries(app, ui),
|
||||
None => panels::show_icon_panel(app, ui, id),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,769 @@
|
|||
//! Floating panels: panels dragged out of the dock float inside the window as groups of tabs.
|
||||
//!
|
||||
//! A dock tab, an icon of the icon column or a popped-out panel's title dragged out of the dock
|
||||
//! floats that panel; the strip right of the dock's tabs floats the whole tabbed group. A floating
|
||||
//! group moves by its title bar (or the strip right of its tabs, or the tab of a lone panel), and a
|
||||
//! tab dragged out of a group floats on its own. Dropped on the dock, which lights up while the
|
||||
//! pointer is over it, a group's panels go back to where they live in the dock (the tabbed group or
|
||||
//! the icon column); dropped on another group's title bar or tabs, they stack with that group. The
|
||||
//! × 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.
|
||||
|
||||
use egui::{CornerRadius, Id, Pos2, Rect, Sense, Stroke, Vec2, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::state::{DockTab, FloatingPanels, UiState, all_panels};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, dock, icons, panels};
|
||||
|
||||
/// Height of a floating group's title bar (its grip and ×).
|
||||
pub const TITLE: f32 = 14.0;
|
||||
/// Height of a floating group's tab strip.
|
||||
pub const STRIP: f32 = 26.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];
|
||||
/// Where `window.panel.float {panel: "tools"}` floats the Tools panel given no position.
|
||||
const TOOLS_AT: [f32; 2] = [60.0, 120.0];
|
||||
/// Points the pointer travels from its press before a drop counts (a click on a title bar that
|
||||
/// overlaps the dock doesn't dock its group).
|
||||
const TRAVEL: f32 = 4.0;
|
||||
|
||||
/// What a move carries.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Moving {
|
||||
/// The floating group holding this panel.
|
||||
Panel(&'static str),
|
||||
/// The floating Tools panel.
|
||||
Tools,
|
||||
}
|
||||
|
||||
/// A move in progress: what moves, where the pointer holds it from its top-left corner, and where
|
||||
/// the press began.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Move {
|
||||
what: Moving,
|
||||
grab: Vec2,
|
||||
from: Pos2,
|
||||
}
|
||||
|
||||
/// Where a moved group or Tools panel lands.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Drop {
|
||||
/// Back in the dock (the Tools panel: at the window's left edge).
|
||||
Dock,
|
||||
/// Stacked with the floating group holding this panel.
|
||||
Stack(&'static str),
|
||||
}
|
||||
|
||||
fn move_id() -> Id {
|
||||
Id::new("floating-move")
|
||||
}
|
||||
|
||||
/// The dock (tabbed group and icon column) as last drawn: a group dropped there docks.
|
||||
pub(crate) fn dock_rect_id() -> Id {
|
||||
Id::new("floating-dock-rect")
|
||||
}
|
||||
|
||||
/// The strip along the window's left edge where the Tools panel docks, as last laid out.
|
||||
pub(crate) fn tools_zone_id() -> Id {
|
||||
Id::new("floating-tools-zone")
|
||||
}
|
||||
|
||||
/// The area of the floating group whose first panel is `first`.
|
||||
pub fn area_id(first: &str) -> Id {
|
||||
Id::new(("floating-panels", 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)
|
||||
}
|
||||
|
||||
/// The floating group holding panel `id`.
|
||||
pub fn group_of(ui: &UiState, id: &str) -> Option<usize> {
|
||||
ui.floating_panels.iter().position(|g| g.panels.iter().any(|p| p == id))
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// The dock tabs left in the dock, in tab order.
|
||||
pub fn docked_tabs(ui: &UiState) -> impl Iterator<Item = DockTab> + '_ {
|
||||
DockTab::ALL.into_iter().filter(|t| group_of(ui, t.info().0).is_none())
|
||||
}
|
||||
|
||||
/// The dock tab the tabbed group shows: the chosen one, or the first left when it floats.
|
||||
pub fn shown_tab(ui: &UiState) -> Option<DockTab> {
|
||||
docked_tabs(ui).find(|t| *t == ui.dock_tab).or_else(|| docked_tabs(ui).next())
|
||||
}
|
||||
|
||||
/// Take `ids` out of the floating groups, dropping the groups left empty.
|
||||
fn take(ui: &mut UiState, ids: &[&str]) {
|
||||
ui.floating_panels.retain_mut(|g| {
|
||||
let active = g.panels.get(g.active).filter(|p| !ids.contains(&p.as_str())).cloned();
|
||||
g.panels.retain(|p| !ids.contains(&p.as_str()));
|
||||
g.active = active.and_then(|a| g.panels.iter().position(|p| *p == a)).unwrap_or(0);
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
/// Float `ids` (out of the dock or of the group they were in) as a new group with its top-left
|
||||
/// corner at `pos`, showing `active`.
|
||||
pub fn float(ui: &mut UiState, ids: &[&str], active: &str, pos: Pos2) {
|
||||
take(ui, ids);
|
||||
if ui.open_panel.as_deref().is_some_and(|p| ids.contains(&p)) {
|
||||
ui.open_panel = None;
|
||||
}
|
||||
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] });
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stack `ids` with the floating group holding `onto` (not one of them), showing `active`.
|
||||
pub fn stack(ui: &mut UiState, ids: &[&str], active: &str, onto: &str) {
|
||||
take(ui, ids);
|
||||
if ui.open_panel.as_deref().is_some_and(|p| ids.contains(&p)) {
|
||||
ui.open_panel = None;
|
||||
}
|
||||
if let Some(g) = group_of(ui, onto).and_then(|i| ui.floating_panels.get_mut(i)) {
|
||||
g.panels.extend(ids.iter().map(|id| id.to_string()));
|
||||
g.active = g.panels.iter().position(|p| p == active).unwrap_or(g.active);
|
||||
}
|
||||
if let Some(tab) = shown_tab(ui) {
|
||||
ui.dock_tab = tab;
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `ids` back in the dock: the dock tabs in the tabbed group (showing `active`, or the first of
|
||||
/// them), the other panels in the icon column.
|
||||
pub fn dock(ui: &mut UiState, ids: &[&str], active: &str) {
|
||||
take(ui, ids);
|
||||
if let Some(tab) = DockTab::from_id(active).or_else(|| ids.iter().find_map(|id| DockTab::from_id(id))) {
|
||||
ui.dock_tab = tab;
|
||||
}
|
||||
ui.dock = true;
|
||||
}
|
||||
|
||||
/// `pos`, a saved top-left corner, moved so that a `size` box there is inside `screen` (its
|
||||
/// top-left corner first when it doesn't fit). Pulled in by whole points: egui rounds an area's
|
||||
/// position, which would push a box of fractional size half a point past the edge.
|
||||
pub fn clamp(pos: [f32; 2], size: Vec2, screen: Rect) -> Pos2 {
|
||||
let [x, y] = pos;
|
||||
let (x, y) = if x.is_finite() && y.is_finite() { (x, y) } else { (screen.left() + 60.0, screen.top() + 100.0) };
|
||||
let fit = |v: f32, max: f32| if v > max { max.floor() } else { v };
|
||||
pos2(fit(x, screen.right() - size.x).max(screen.left()), fit(y, screen.bottom() - size.y).max(screen.top()))
|
||||
}
|
||||
|
||||
/// Start moving `what` with the pointer, held `grab` from its top-left corner.
|
||||
fn start(ctx: &egui::Context, what: Moving, grab: Vec2) {
|
||||
let Some(from) = ctx.input(|i| i.pointer.press_origin().or(i.pointer.interact_pos())) else { return };
|
||||
ctx.data_mut(|d| d.insert_temp(move_id(), Move { what, grab, from }));
|
||||
}
|
||||
|
||||
/// Where the pointer holds a group torn off by a press `left` points right of where the group's
|
||||
/// tab strip will begin (under its title bar, past its 1-point border).
|
||||
pub(crate) fn strip_grab(ctx: &egui::Context, left: f32) -> Vec2 {
|
||||
let x = ctx.input(|i| i.pointer.press_origin()).map_or(12.0, |p| p.x - left);
|
||||
vec2(1.0 + x.max(0.0), 1.0 + TITLE + STRIP / 2.0)
|
||||
}
|
||||
|
||||
/// Float `ids` under the pointer, held `grab` from the new group's top-left corner, and carry on
|
||||
/// moving them with the pointer (a tab or group dragged out of the dock or out of its group).
|
||||
pub(crate) fn tear(app: &mut VectorcraftApp, ctx: &egui::Context, ids: &[&'static str], active: &'static str, grab: Vec2) {
|
||||
let Some(at) = ctx.input(|i| i.pointer.interact_pos()) else { return };
|
||||
float(&mut app.ui, ids, active, at - grab);
|
||||
start(ctx, Moving::Panel(active), grab);
|
||||
}
|
||||
|
||||
/// The Tools panel's title bar was pressed (`title`, the panel's bounds `bounds`): docked, dragging
|
||||
/// it out of the panel floats the panel under the pointer; floating at `floating`, a drag moves it.
|
||||
pub(crate) fn drag_tools(app: &mut VectorcraftApp, ctx: &egui::Context, title: &egui::Response, bounds: Rect, floating: Option<Pos2>) {
|
||||
let Some(at) = ctx.input(|i| i.pointer.interact_pos()) else { return };
|
||||
match floating {
|
||||
Some(pos) if title.drag_started() => start(ctx, Moving::Tools, at - pos),
|
||||
None if title.dragged() && !bounds.contains(at) => {
|
||||
// Held where the press grabbed the title bar.
|
||||
let grab = ctx.input(|i| i.pointer.press_origin()).map_or(vec2(10.0, 7.0), |p| p - bounds.min);
|
||||
let p = at - grab;
|
||||
app.ui.toolbar_pos = Some([p.x, p.y]);
|
||||
start(ctx, Moving::Tools, grab);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the pointer at `at` would drop `what`, and the zone that lights up for it.
|
||||
fn drop_target(app: &VectorcraftApp, ctx: &egui::Context, what: Moving, at: Pos2) -> Option<(Drop, Rect)> {
|
||||
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.
|
||||
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)) {
|
||||
return Some((Drop::Stack(first), head));
|
||||
}
|
||||
}
|
||||
zone(dock_rect_id()).filter(|_| app.ui.dock && app.ui.screen_mode < 3).map(|r| (Drop::Dock, r))
|
||||
}
|
||||
|
||||
/// Light up a drop zone: over the docked panels, under the floating ones (the moved one too).
|
||||
fn highlight(ctx: &egui::Context, zone: Rect) {
|
||||
let t = Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Background, Id::new("floating-drop-zone")));
|
||||
painter.rect(zone.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.2), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let Some(m) = ctx.data(|d| d.get_temp::<Move>(move_id())) else { return };
|
||||
let at = ctx.input(|i| i.pointer.interact_pos());
|
||||
let target = at.filter(|at| at.distance(m.from) > TRAVEL).and_then(|at| drop_target(app, ctx, m.what, at));
|
||||
if ctx.input(|i| i.pointer.primary_down()) {
|
||||
if let Some(p) = at.map(|at| at - m.grab)
|
||||
&& p.x.is_finite()
|
||||
&& p.y.is_finite()
|
||||
{
|
||||
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];
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
||||
let Some((drop, _)) = target else { return };
|
||||
let Moving::Panel(id) = m.what else {
|
||||
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);
|
||||
match drop {
|
||||
Drop::Dock => dock(&mut app.ui, &ids, active),
|
||||
Drop::Stack(onto) => stack(&mut app.ui, &ids, active, onto),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a press on a floating group did.
|
||||
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.
|
||||
Move(&'static str, Pos2),
|
||||
/// 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).
|
||||
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 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 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);
|
||||
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 (strip, _) = ui.allocate_exact_size(vec2(width, STRIP), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.panel_darker);
|
||||
let mut left = strip.left();
|
||||
for (k, (&id, galley)) in 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 {
|
||||
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));
|
||||
} else if lone && resp.drag_started() {
|
||||
action = Some(Action::Move(id, pos));
|
||||
} else if !lone && resp.dragged() && pointer.is_some_and(|p| !strip.expand(4.0).contains(p)) {
|
||||
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());
|
||||
if resp.drag_started() {
|
||||
action = Some(Action::Move(active, pos));
|
||||
}
|
||||
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to move"));
|
||||
}
|
||||
panels::panel_menu(app, ui, active, menu);
|
||||
dock::panel_body(app, ui, active, width, tall);
|
||||
});
|
||||
});
|
||||
}
|
||||
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.
|
||||
pub fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
||||
let float = match id {
|
||||
"window.panel.float" => true,
|
||||
"window.panel.dock" => false,
|
||||
_ => return None,
|
||||
};
|
||||
Some(float_or_dock(app, float, p))
|
||||
}
|
||||
|
||||
/// The `x`, `y` params: a top-left corner in screen points, if given.
|
||||
fn position(p: &Value) -> Result<Option<Pos2>, String> {
|
||||
let coord = |k: &str| match p.get(k) {
|
||||
None | Some(Value::Null) => Some(None),
|
||||
Some(v) => v.as_f64().filter(|f| f.abs() < 1e6).map(|f| Some(f as f32)),
|
||||
};
|
||||
match (coord("x"), coord("y")) {
|
||||
(Some(Some(x)), Some(Some(y))) => Ok(Some(pos2(x, y))),
|
||||
(Some(None), Some(None)) => Ok(None),
|
||||
_ => Err("x and y must both be numbers".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn float_or_dock(app: &mut VectorcraftApp, float_it: bool, p: &Value) -> Result<Value, String> {
|
||||
let raw = p.get("panel").and_then(Value::as_str).unwrap_or_default().trim();
|
||||
let group = crate::menus::opt_bool(p, "group")?.unwrap_or(false);
|
||||
let at = position(p)?;
|
||||
if raw.eq_ignore_ascii_case("tools") || raw.eq_ignore_ascii_case("toolbar") {
|
||||
app.ui.toolbar_pos = float_it.then(|| at.map_or(TOOLS_AT, |p| [p.x, p.y]));
|
||||
app.ui.toolbar |= float_it;
|
||||
return Ok(json!({ "panel": "tools", "floating": float_it, "pos": app.ui.toolbar_pos }));
|
||||
}
|
||||
let Some(id) = crate::menus::normalize_panel(raw) else { return Err(format!("unknown panel `{raw}`")) };
|
||||
let own = group_of(&app.ui, id);
|
||||
// The panel, or with `group` its whole group: its floating group, or the tabs left in the dock.
|
||||
let ids: Vec<&'static str> = match (group, own.and_then(|i| app.ui.floating_panels.get(i))) {
|
||||
(true, Some(g)) => ids_of(g),
|
||||
(true, None) if DockTab::from_id(id).is_some() => docked_tabs(&app.ui).map(|t| t.info().0).collect(),
|
||||
_ => vec![id],
|
||||
};
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
// 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()) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
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) }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{Event, PointerButton};
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SCREEN: Vec2 = vec2(1400.0, 900.0);
|
||||
|
||||
/// Frames of the whole window, 1400 × 900, with pointer input one event a frame (as the
|
||||
/// control channel's `ui.drag` sends it).
|
||||
struct Harness {
|
||||
app: VectorcraftApp,
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
fn button(pos: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
fn new() -> Self {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let mut h = Self { app, ctx: egui::Context::default(), time: 0.0 };
|
||||
h.settle();
|
||||
h
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) {
|
||||
self.time += 0.05;
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
|
||||
let raw = egui::RawInput { time: Some(self.time), screen_rect: Some(screen), events, ..Default::default() };
|
||||
let app = &mut self.app;
|
||||
self.ctx
|
||||
.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn settle(&mut self) {
|
||||
for _ in 0..4 {
|
||||
self.frame(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Press at `from` and move to `to` in steps, the button still down.
|
||||
fn hold(&mut self, from: Pos2, to: Pos2) {
|
||||
self.frame(vec![Event::PointerMoved(from)]);
|
||||
self.frame(vec![button(from, true)]);
|
||||
for k in 1..=8 {
|
||||
self.frame(vec![Event::PointerMoved(from + (to - from) * (k as f32 / 8.0))]);
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self, at: Pos2) {
|
||||
self.frame(vec![button(at, false)]);
|
||||
self.settle();
|
||||
}
|
||||
|
||||
fn drag(&mut self, from: Pos2, to: Pos2) {
|
||||
self.hold(from, to);
|
||||
self.release(to);
|
||||
}
|
||||
|
||||
fn click(&mut self, at: Pos2) {
|
||||
self.frame(vec![Event::PointerMoved(at), button(at, true)]);
|
||||
self.release(at);
|
||||
}
|
||||
|
||||
fn temp_rect(&self, id: Id) -> Rect {
|
||||
self.ctx.data(|d| d.get_temp::<Rect>(id)).unwrap()
|
||||
}
|
||||
|
||||
/// The floating group whose first panel is `first`, as drawn.
|
||||
fn group(&self, first: &str) -> Rect {
|
||||
self.ctx.memory(|m| m.area_rect(area_id(first))).unwrap()
|
||||
}
|
||||
|
||||
/// A point on a floating group's title bar (left of its ×).
|
||||
fn title(&self, first: &str) -> Pos2 {
|
||||
let r = self.group(first);
|
||||
pos2(r.left() + 30.0, r.top() + 1.0 + TITLE / 2.0)
|
||||
}
|
||||
|
||||
/// The rects of the widgets that sense a drag and pass `keep`, top to bottom, left to right.
|
||||
fn widgets(&self, keep: impl Fn(Rect) -> bool) -> Vec<Rect> {
|
||||
let mut r: Vec<Rect> = self.ctx.viewport(|vp| {
|
||||
vp.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).filter(|w| w.sense.senses_drag() && keep(w.rect)).map(|w| w.rect).collect()
|
||||
});
|
||||
r.sort_by(|a, b| a.top().total_cmp(&b.top()).then(a.left().total_cmp(&b.left())));
|
||||
r
|
||||
}
|
||||
|
||||
/// The dock's tabs, left to right.
|
||||
fn dock_tabs(&self) -> Vec<Rect> {
|
||||
let dock = self.temp_rect(dock_rect_id());
|
||||
self.widgets(|r| r.height() == 32.0 && dock.contains_rect(r))
|
||||
}
|
||||
|
||||
/// The icon column's panel icons, top to bottom.
|
||||
fn icons(&self) -> Vec<Rect> {
|
||||
let dock = self.temp_rect(dock_rect_id());
|
||||
self.widgets(|r| r.size() == vec2(30.0, 30.0) && dock.contains_rect(r))
|
||||
}
|
||||
|
||||
fn groups(&self) -> Vec<Vec<&str>> {
|
||||
self.app.ui.floating_panels.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dock_tab_dragged_out_floats_and_dropped_on_the_dock_docks_again() {
|
||||
let mut h = Harness::new();
|
||||
let tabs = h.dock_tabs();
|
||||
assert_eq!(tabs.len(), 3, "Properties, Layers, Libraries: {tabs:?}");
|
||||
let to = pos2(600.0, 400.0);
|
||||
h.drag(tabs[1].center(), to);
|
||||
assert_eq!(h.groups(), [["layers"]], "Layers floats");
|
||||
assert!(h.group("layers").contains(to), "under the pointer: {:?}", h.group("layers"));
|
||||
assert_eq!(h.dock_tabs().len(), 2, "its tab left the dock");
|
||||
assert_eq!(h.app.ui.dock_tab, DockTab::Properties);
|
||||
// A click on its title bar moves nothing, even over the dock.
|
||||
let dock = h.temp_rect(dock_rect_id());
|
||||
h.app.run("window.panel.float", json!({"panel": "layers", "x": dock.left() + 10.0, "y": 300})).unwrap();
|
||||
h.settle();
|
||||
h.click(h.title("layers"));
|
||||
assert_eq!(h.groups(), [["layers"]], "a click doesn't dock it");
|
||||
// Dragged by its title bar onto the dock, the dock lights up, and the release docks it.
|
||||
h.app.run("window.panel.float", json!({"panel": "layers", "x": 500, "y": 300})).unwrap();
|
||||
h.settle();
|
||||
let over = dock.center();
|
||||
h.hold(h.title("layers"), over);
|
||||
assert!(h.group("layers").contains(over), "the group follows the pointer");
|
||||
let target = drop_target(&h.app, &h.ctx, Moving::Panel("layers"), over);
|
||||
assert_eq!(target.map(|t| t.0), Some(Drop::Dock), "the dock is the drop zone");
|
||||
h.release(over);
|
||||
assert!(h.groups().is_empty(), "docked: {:?}", h.groups());
|
||||
assert_eq!((h.dock_tabs().len(), h.app.ui.dock_tab), (3, DockTab::Layers), "back in the dock, shown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_strip_right_of_the_tabs_floats_the_whole_group_and_its_close_box_docks_it() {
|
||||
let mut h = Harness::new();
|
||||
let tabs = h.dock_tabs();
|
||||
let rest = tabs[2].right_center() + vec2(30.0, 0.0);
|
||||
h.drag(rest, pos2(500.0, 300.0));
|
||||
assert_eq!(h.groups(), [["properties", "layers", "libraries"]]);
|
||||
assert!(h.dock_tabs().is_empty(), "the tabbed group left the dock");
|
||||
let column = h.temp_rect(dock_rect_id());
|
||||
assert!(column.width() < 40.0, "only the icon column is left: {column:?}");
|
||||
// Its × puts them back.
|
||||
let g = h.group("properties");
|
||||
h.click(pos2(g.right() - 11.0, g.top() + 1.0 + TITLE / 2.0));
|
||||
assert!(h.groups().is_empty());
|
||||
assert_eq!(h.dock_tabs().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icons_dragged_out_float_stack_onto_another_group_and_tear_out_of_it() {
|
||||
let mut h = Harness::new();
|
||||
// Color, Color Guide, Swatches… top to bottom.
|
||||
let icons = h.icons();
|
||||
let count = icons.len();
|
||||
h.drag(icons[2].center(), pos2(500.0, 250.0));
|
||||
assert_eq!(h.groups(), [["swatches"]]);
|
||||
assert_eq!(h.icons().len(), count - 1, "its icon left the column");
|
||||
h.drag(h.icons()[0].center(), pos2(500.0, 650.0));
|
||||
assert_eq!(h.groups(), [["swatches"], ["color"]]);
|
||||
// Color's title bar dropped on the Swatches group's tabs stacks them.
|
||||
let onto = h.group("swatches").left_top() + vec2(150.0, 1.0 + TITLE + STRIP / 2.0);
|
||||
h.hold(h.title("color"), onto);
|
||||
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("color"), onto).map(|t| t.0), Some(Drop::Stack("swatches")));
|
||||
h.release(onto);
|
||||
assert_eq!(h.groups(), [["swatches", "color"]]);
|
||||
assert_eq!(h.app.ui.floating_panels[0].active, 1, "showing the panel dropped on it");
|
||||
// A click shows a tab; a tab dragged out of the strip floats on its own again.
|
||||
let g = h.group("swatches");
|
||||
let tabs = h.widgets(|r| r.height() == STRIP && g.contains_rect(r));
|
||||
assert_eq!(tabs.len(), 3, "two tabs and the strip right of them: {tabs:?}");
|
||||
h.click(tabs[0].center());
|
||||
assert_eq!(h.app.ui.floating_panels[0].active, 0);
|
||||
let to = pos2(900.0, 500.0);
|
||||
h.drag(tabs[1].center(), to);
|
||||
assert_eq!(h.groups(), [["swatches"], ["color"]]);
|
||||
assert!(h.group("color").contains(to));
|
||||
// Window › Color shows it where it floats; Dock Panel puts it back.
|
||||
assert_eq!(h.app.run("window.panel", json!({"panel": "Color"})).unwrap(), json!({"floating": ["color"]}));
|
||||
assert_eq!(crate::menus::checked(&h.app, "window.panel", &json!({"panel": "color"})), Some(true));
|
||||
h.app.run("window.panel.dock", json!({"panel": "color"})).unwrap();
|
||||
h.settle();
|
||||
assert_eq!(h.icons().len(), count - 1, "Color is back in the column");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_popped_out_panel_dragged_by_its_title_floats() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.panel", json!({"panel": "swatches"})).unwrap();
|
||||
h.settle();
|
||||
let flyout = h.ctx.memory(|m| m.area_rect(Id::new("icon-panel"))).unwrap();
|
||||
let to = pos2(500.0, 400.0);
|
||||
h.drag(flyout.left_top() + vec2(20.0, 13.0), to);
|
||||
assert_eq!((h.groups(), h.app.ui.open_panel.as_deref()), (vec![vec!["swatches"]], None));
|
||||
assert!(h.group("swatches").contains(to), "under the pointer: {:?}", h.group("swatches"));
|
||||
// Its panel menu docks it again.
|
||||
h.app.run("window.panel.dock", json!({"panel": "swatches"})).unwrap();
|
||||
assert!(h.groups().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tools_panel_floats_by_its_title_bar_and_docks_on_the_left_edge() {
|
||||
let mut h = Harness::new();
|
||||
let zone = h.temp_rect(tools_zone_id());
|
||||
let title = zone.left_top() + vec2(30.0, 7.0);
|
||||
let to = pos2(400.0, 300.0);
|
||||
h.drag(title, to);
|
||||
let pos = h.app.ui.toolbar_pos.expect("the Tools panel floats");
|
||||
let float = h.ctx.memory(|m| m.area_rect(Id::new("toolbar-floating"))).unwrap();
|
||||
assert!(float.contains(to) && (pos2(pos[0], pos[1]) - float.min).length() < 1.0, "under the pointer: {float:?}");
|
||||
// Back on the window's left edge.
|
||||
let edge = pos2(10.0, 400.0);
|
||||
h.hold(float.left_top() + vec2(30.0, 7.0), edge);
|
||||
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Tools, edge).map(|t| t.0), Some(Drop::Dock));
|
||||
h.release(edge);
|
||||
assert_eq!(h.app.ui.toolbar_pos, None, "docked");
|
||||
// A click on the title bar still switches the columns.
|
||||
let double = h.app.ui.toolbar_double;
|
||||
h.click(title);
|
||||
assert_ne!(h.app.ui.toolbar_double, double);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_panels_are_kept_in_workspaces_and_preferences_and_clamped_into_the_window() {
|
||||
let mut h = Harness::new();
|
||||
let app = &mut h.app;
|
||||
// Saved on a much bigger screen.
|
||||
app.run("window.panel.float", json!({"panel": "layers", "x": 5000, "y": 4000})).unwrap();
|
||||
let r = app.run("window.panel.float", json!({"panel": "stroke", "onto": "Layers"})).unwrap();
|
||||
assert_eq!(r["group"], json!(["layers", "stroke"]));
|
||||
app.run("window.panel.float", json!({"panel": "tools", "x": 300, "y": 200})).unwrap();
|
||||
app.run("window.workspace.new", json!({"name": "Floating"})).unwrap();
|
||||
let saved = app.ui.floating_panels.clone();
|
||||
app.run("window.workspace", json!({"name": "Essentials"})).unwrap();
|
||||
assert!(app.ui.floating_panels.is_empty() && app.ui.toolbar_pos.is_none(), "Essentials docks everything");
|
||||
app.run("window.workspace", json!({"name": "Floating"})).unwrap();
|
||||
assert_eq!((&app.ui.floating_panels, app.ui.toolbar_pos), (&saved, Some([300.0, 200.0])));
|
||||
let prefs: UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
let prefs = prefs.sanitized();
|
||||
assert_eq!((&prefs.floating_panels, prefs.toolbar_pos), (&saved, Some([300.0, 200.0])));
|
||||
// Unknown and repeated panels (a hand-edited file) are dropped.
|
||||
let edited: UiState = serde_json::from_value(json!({
|
||||
"floating_panels": [{"panels": ["nope", "layers"], "active": 7, "pos": [1, 2]}, {"panels": ["layers"], "pos": [3, 4]}],
|
||||
}))
|
||||
.unwrap();
|
||||
let edited = edited.sanitized();
|
||||
assert_eq!(edited.floating_panels, [FloatingPanels { panels: vec!["layers".into()], active: 0, pos: [1.0, 2.0] }]);
|
||||
h.settle();
|
||||
let g = h.group("layers");
|
||||
let screen = Rect::from_min_size(Pos2::ZERO, SCREEN);
|
||||
assert!(screen.contains_rect(g), "clamped into the window: {g:?}");
|
||||
assert!(h.dock_tabs().len() == 2 && h.app.ui.dock_tab == DockTab::Properties);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_and_dock_commands_check_their_params() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
assert!(app.run("window.panel.float", json!({"panel": "nope"})).is_err());
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "x": "left", "y": 2})).is_err());
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "x": 2})).is_err(), "both x and y");
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "group": "yes"})).is_err());
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "onto": "color"})).is_err(), "Color isn't floating");
|
||||
assert!(app.ui.floating_panels.is_empty());
|
||||
// The tabbed group's tabs float together.
|
||||
let r = app.run("window.panel.float", json!({"panel": "layers", "group": true})).unwrap();
|
||||
assert_eq!(r["group"], json!(["properties", "layers", "libraries"]));
|
||||
assert_eq!(app.ui.floating_panels[0].active, 1);
|
||||
assert!(app.run("window.panel.float", json!({"panel": "layers", "onto": "properties"})).is_err(), "not onto its own group");
|
||||
// Floated out of its group; a lone panel moves.
|
||||
app.run("window.panel.float", json!({"panel": "layers", "x": 10, "y": 20})).unwrap();
|
||||
assert_eq!((app.ui.floating_panels.len(), floating_pos(&app, "layers")), (2, [10.0, 20.0]));
|
||||
app.run("window.panel.float", json!({"panel": "layers", "x": 30, "y": 40})).unwrap();
|
||||
assert_eq!((app.ui.floating_panels.len(), floating_pos(&app, "layers")), (2, [30.0, 40.0]));
|
||||
app.run("window.panel.dock", json!({"panel": "properties", "group": true})).unwrap();
|
||||
assert_eq!(app.ui.floating_panels.len(), 1);
|
||||
assert_eq!(app.ui.dock_tab, DockTab::Properties);
|
||||
assert_eq!(app.run("window.panel.dock", json!({"panel": "tools"})).unwrap()["floating"], json!(false));
|
||||
app.ui.toolbar = false;
|
||||
app.run("window.panel.float", json!({"panel": "Toolbar"})).unwrap();
|
||||
assert!(app.ui.toolbar && app.ui.toolbar_pos == Some(TOOLS_AT), "floating shows the Tools panel");
|
||||
}
|
||||
|
||||
fn floating_pos(app: &VectorcraftApp, id: &str) -> [f32; 2] {
|
||||
group_of(&app.ui, id).map(|i| app.ui.floating_panels[i].pos).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -611,4 +611,6 @@
|
|||
Drag to move Přetažením přesunete
|
||||
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
|
||||
Dock Panel Ukotvit panel
|
||||
Float Tool Group Plovoucí skupina nástrojů
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3053,6 +3053,12 @@ axis Both Ambos
|
|||
Drag to move Arrastrar para mover
|
||||
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
|
||||
Dock Panel Anclar panel
|
||||
Drag to float the panel group Arrastrar para hacer flotar el grupo de paneles
|
||||
Put back in the dock Devolver al área de paneles
|
||||
Drag to float the toolbar Arrastrar para hacer flotar la barra de herramientas
|
||||
Drag to the window's left edge to dock Arrastrar al borde izquierdo de la ventana para anclar
|
||||
Float Tool Group Hacer flotar el grupo de herramientas
|
||||
|
||||
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
|
||||
|
|
@ -3573,3 +3579,5 @@ axis Both Ambos
|
|||
@msg Reload the page to go on (changes not saved yet may be lost). Vuelve a cargar la página para continuar (los cambios que aún no se guardaron podrían perderse).
|
||||
@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 x and y must both be numbers x e y deben ser números
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3041,4 +3041,10 @@ axis Both 両方
|
|||
Drag to move 移動するにはドラッグ
|
||||
Drag to float as a toolbar ドラッグしてツールバーとして浮かせる
|
||||
Put back in the toolbar ツールバーに戻す
|
||||
Float Panel パネルをフローティング
|
||||
Dock Panel パネルをドッキング
|
||||
Drag to float the panel group ドラッグしてパネルグループをフローティング
|
||||
Put back in the dock ドックに戻す
|
||||
Drag to float the toolbar ドラッグしてツールバーをフローティング
|
||||
Drag to the window's left edge to dock ウィンドウの左端までドラッグしてドッキング
|
||||
Float Tool Group ツールグループをフローティング
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2633,4 +2633,6 @@
|
|||
Drag to move Arraste para mover
|
||||
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
|
||||
Dock Panel Encaixar Painel
|
||||
Float Tool Group Flutuar Grupo de Ferramentas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3298,4 +3298,10 @@ axis Both 两者
|
|||
Drag to move 拖动以移动
|
||||
Drag to float as a toolbar 拖动使其浮动为工具栏
|
||||
Put back in the toolbar 放回工具栏
|
||||
Float Panel 浮动面板
|
||||
Dock Panel 停放面板
|
||||
Drag to float the panel group 拖动以浮动面板组
|
||||
Put back in the dock 放回停放区
|
||||
Drag to float the toolbar 拖动以浮动工具栏
|
||||
Drag to the window's left edge to dock 拖到窗口左边缘以停放
|
||||
Float Tool Group 浮动工具组
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3072,4 +3072,10 @@ axis Both 兩者
|
|||
Drag to move 拖曳以移動
|
||||
Drag to float as a toolbar 拖曳使其浮動為工具列
|
||||
Put back in the toolbar 放回工具列
|
||||
Float Panel 浮動面板
|
||||
Dock Panel 停駐面板
|
||||
Drag to float the panel group 拖曳以浮動面板群組
|
||||
Put back in the dock 放回停駐區
|
||||
Drag to float the toolbar 拖曳以浮動工具列
|
||||
Drag to the window's left edge to dock 拖曳到視窗左側邊緣以停駐
|
||||
Float Tool Group 浮動工具組
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -24,6 +24,7 @@ pub mod cursors;
|
|||
pub mod dialogs;
|
||||
pub mod dock;
|
||||
pub mod find_font;
|
||||
pub mod floating;
|
||||
pub mod font_menu;
|
||||
pub mod graphics;
|
||||
pub mod i18n;
|
||||
|
|
@ -895,6 +896,7 @@ impl VectorcraftApp {
|
|||
let t0 = now_ms();
|
||||
scrub::begin_frame(self, &ctx);
|
||||
font_menu::end_stale_preview(self, &ctx);
|
||||
floating::track(self, &ctx);
|
||||
let t = theme::Tokens::get(&ctx);
|
||||
if self.ui.screen_mode < 2 {
|
||||
chrome::app_bar(self, ui);
|
||||
|
|
@ -919,6 +921,7 @@ impl VectorcraftApp {
|
|||
canvas::show(self, ui);
|
||||
});
|
||||
dock::floating_panel(self, &ctx);
|
||||
floating::show(self, &ctx);
|
||||
panels::library_panel::show_window(self, &ctx);
|
||||
dialogs::show(self, &ctx);
|
||||
palette::show(self, &ctx);
|
||||
|
|
|
|||
|
|
@ -225,6 +225,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
("window.taskBar.reset", "Reset Bar Position", "", "{} unpin the Contextual Task Bar and put it back under the selection"),
|
||||
("window.dock", "Panels", "Tab", "{} show/hide all panels"),
|
||||
("window.panel", "Show Panel", "", "{panel: id} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too)"),
|
||||
(
|
||||
"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}",
|
||||
),
|
||||
(
|
||||
"window.panel.dock",
|
||||
"Dock Panel",
|
||||
"",
|
||||
"{panel: id or \"tools\", group?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; \"tools\" docks the Tools panel at the window's left edge",
|
||||
),
|
||||
(
|
||||
"window.collapseDock",
|
||||
"Collapse Panels to Icons",
|
||||
|
|
@ -772,13 +784,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
|
||||
/// Canonical panel id for `window.panel`: a dock tab's or an icon panel's id or display label,
|
||||
/// matched case-insensitively (`"Layers"`, `"swatches"`, `"Color Guide"`).
|
||||
fn normalize_panel(input: &str) -> Option<&'static str> {
|
||||
pub(crate) fn normalize_panel(input: &str) -> Option<&'static str> {
|
||||
let name = input.trim();
|
||||
crate::state::all_panels().find(|(id, label)| name.eq_ignore_ascii_case(id) || name.eq_ignore_ascii_case(label)).map(|(id, _)| id)
|
||||
}
|
||||
|
||||
/// An optional `{key?: bool}` param: none when it is omitted or null (the commands then toggle).
|
||||
fn opt_bool(p: &Value, key: &str) -> Result<Option<bool>, String> {
|
||||
pub(crate) fn opt_bool(p: &Value, key: &str) -> Result<Option<bool>, String> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::Bool(b)) => Ok(Some(*b)),
|
||||
|
|
@ -803,7 +815,10 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
if let Some(r) = crate::panels::character::intercept_text_command(app, id) {
|
||||
return Some(r);
|
||||
}
|
||||
if let Some(r) = crate::shortcut_editor::run_command(app, id, p).or_else(|| crate::workspaces::run_command(app, id, p)) {
|
||||
if let Some(r) = crate::shortcut_editor::run_command(app, id, p)
|
||||
.or_else(|| crate::workspaces::run_command(app, id, p))
|
||||
.or_else(|| crate::floating::command(app, id, p))
|
||||
{
|
||||
return Some(r);
|
||||
}
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str).map(str::to_string);
|
||||
|
|
@ -1059,6 +1074,15 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
// Canonical id (case-insensitive; display labels work too), then the
|
||||
// dock tab it names, if any.
|
||||
let canonical = normalize_panel(&raw);
|
||||
// A floating panel shows its tab in its group.
|
||||
if let Some(p) = canonical
|
||||
&& 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);
|
||||
let out = json!({ "floating": g.panels });
|
||||
app.ui.dock = true;
|
||||
return Some(Ok(out));
|
||||
}
|
||||
let tab = canonical.and_then(DockTab::from_id);
|
||||
match (canonical, tab) {
|
||||
// A collapsed dock pops the panel out of its icon, like the icon panels.
|
||||
|
|
@ -1370,7 +1394,8 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
|||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
let canonical = normalize_panel(panel);
|
||||
match canonical.and_then(DockTab::from_id) {
|
||||
Some(tab) if !app.ui.dock_collapsed => app.ui.dock_tab == tab,
|
||||
_ if canonical.is_some_and(|id| crate::floating::group_of(&app.ui, id).is_some()) => true,
|
||||
Some(tab) if !app.ui.dock_collapsed => crate::floating::shown_tab(&app.ui) == Some(tab),
|
||||
_ => canonical.is_some_and(|id| app.ui.open_panel.as_deref() == Some(id)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,8 +113,9 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Items of a panel's (≡) menu. Unimplemented Illustrator items are listed disabled.
|
||||
pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
/// Items of a panel's (≡) menu. Unimplemented Illustrator items are listed disabled. False for a
|
||||
/// panel without items of its own.
|
||||
pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool {
|
||||
match id {
|
||||
"swatches" => swatches::menu(app, ui),
|
||||
"color" => color::menu(app, ui),
|
||||
|
|
@ -152,10 +153,9 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
|||
links::ID => links::menu(app, ui),
|
||||
asset_export::ID => asset_export::menu(app, ui),
|
||||
css_properties::ID => css_properties::menu(app, ui),
|
||||
_ => {
|
||||
ui.add_enabled(false, egui::Button::new(tl!("No options")).frame(false));
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The ≡ panel-menu button drawn into `rect` (the right end of a panel's title/tab strip).
|
||||
|
|
@ -167,7 +167,20 @@ pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) {
|
|||
egui::Popup::menu(&resp).show(|ui| {
|
||||
crate::widgets::menu_scroll(ui, |ui| {
|
||||
ui.set_min_width(220.0);
|
||||
panel_menu_items(app, ui, id);
|
||||
if panel_menu_items(app, ui, id) {
|
||||
ui.separator();
|
||||
}
|
||||
// The panel floats out of the dock or goes back in, as dragging its tab does.
|
||||
let (label, cmd) = if crate::floating::group_of(&app.ui, id).is_some() {
|
||||
(tl!("Dock Panel"), "window.panel.dock")
|
||||
} else {
|
||||
(tl!("Float Panel"), "window.panel.float")
|
||||
};
|
||||
if crate::widgets::menu_item(ui, label, true, false)
|
||||
&& let Err(e) = app.run(cmd, json!({ "panel": id }))
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,6 +268,13 @@ pub struct UiState {
|
|||
/// Tool flyouts torn off the toolbar into floating panels.
|
||||
#[serde(default)]
|
||||
pub floating_flyouts: Vec<FloatingFlyout>,
|
||||
/// Panels dragged out of the dock: each group floats as its own stack of tabs.
|
||||
#[serde(default)]
|
||||
pub floating_panels: Vec<FloatingPanels>,
|
||||
/// The Tools panel floats with its top-left corner here (dragged out by its title bar); `None`:
|
||||
/// docked at the window's left edge.
|
||||
#[serde(default)]
|
||||
pub toolbar_pos: Option<[f32; 2]>,
|
||||
pub status_bar: bool,
|
||||
pub dock: bool,
|
||||
pub view: ViewFlags,
|
||||
|
|
@ -411,6 +418,34 @@ pub struct TaskBarPlace {
|
|||
pub offset: Option<(u64, egui::Vec2)>,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct FloatingPanels {
|
||||
/// Panel ids (`window.panel`), in tab order.
|
||||
pub panels: Vec<String>,
|
||||
/// The tab shown.
|
||||
#[serde(default)]
|
||||
pub active: usize,
|
||||
/// Top-left corner in screen points.
|
||||
pub pos: [f32; 2],
|
||||
}
|
||||
|
||||
impl FloatingPanels {
|
||||
/// Keep known panels, each in one group, drop the groups left empty and the toolbar position
|
||||
/// that isn't a number (a hand-edited preferences file or workspace).
|
||||
pub fn sanitize(groups: &mut Vec<FloatingPanels>, toolbar_pos: &mut Option<[f32; 2]>) {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
groups.retain_mut(|g| {
|
||||
g.panels.retain(|id| all_panels().any(|(p, _)| p == id) && seen.insert(id.clone()));
|
||||
g.active = g.active.min(g.panels.len().saturating_sub(1));
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
if toolbar_pos.is_some_and(|p| !p.iter().all(|v| v.is_finite())) {
|
||||
*toolbar_pos = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
/// Clear transient state after loading saved preferences.
|
||||
pub fn sanitized(mut self) -> Self {
|
||||
|
|
@ -429,6 +464,7 @@ impl UiState {
|
|||
f.tools.retain(|id| vectorcraft_tools::tool_info(id).is_some());
|
||||
f.tools.first().is_some_and(|k| seen.insert(k.clone()))
|
||||
});
|
||||
FloatingPanels::sanitize(&mut self.floating_panels, &mut self.toolbar_pos);
|
||||
// Overrides that can't fire (modifier-only chords recorded by older versions, #487) give
|
||||
// the default back.
|
||||
self.shortcut_overrides.retain(|_, c| c.is_empty() || crate::shortcut_editor::normalize(c).is_some());
|
||||
|
|
@ -452,6 +488,8 @@ impl Default for UiState {
|
|||
task_bar_place: TaskBarPlace::default(),
|
||||
slot_tool: Default::default(),
|
||||
floating_flyouts: vec![],
|
||||
floating_panels: vec![],
|
||||
toolbar_pos: None,
|
||||
status_bar: true,
|
||||
dock: true,
|
||||
view: ViewFlags::default(),
|
||||
|
|
|
|||
|
|
@ -89,148 +89,170 @@ pub fn remember(app: &mut VectorcraftApp, id: &str) {
|
|||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let all = slots(app);
|
||||
// The user's choice alone: a window too short for the tools scrolls them.
|
||||
let cols = if app.ui.toolbar_double { 2 } else { 1 };
|
||||
let w = if cols == 2 { 76.0 } else { WIDTH };
|
||||
egui::Panel::left("toolbar")
|
||||
.resizable(false)
|
||||
.exact_size(w)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).stroke(Stroke::new(1.5, t.border)))
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
// Dock header with » and a grip strip.
|
||||
let (hdr, hresp) = ui.allocate_exact_size(vec2(ui.available_width(), 14.0), Sense::click());
|
||||
ui.painter().rect_filled(hdr, 0.0, t.tab_strip);
|
||||
icons::paint(
|
||||
ui,
|
||||
if cols == 2 { "chevrons-left" } else { "chevrons-right" },
|
||||
egui::Rect::from_min_size(hdr.left_top() + vec2(3.0, 2.0), vec2(10.0, 10.0)),
|
||||
if hresp.hovered() { t.text_strong } else { t.text },
|
||||
);
|
||||
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked()
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
&& let Err(e) = app.run("window.toolbarColumns", json!({ "double": cols == 1 }))
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
let (grip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 6.0), Sense::hover());
|
||||
for k in 0..6 {
|
||||
ui.painter().line_segment(
|
||||
[pos2(grip.center().x - 9.0, grip.top() + 1.5 + k as f32 * 0.6), pos2(grip.center().x + 9.0, grip.top() + 1.5 + k as f32 * 0.6)],
|
||||
Stroke::new(0.4, t.text_disabled),
|
||||
);
|
||||
}
|
||||
// The tools and the controls under them scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "toolbar", |ui| {
|
||||
let active = app.session.tool_id();
|
||||
// The tool button whose long press opened its flyout, until the next press.
|
||||
let held_id = egui::Id::new("toolbar-held");
|
||||
if ui.input(|inp| inp.pointer.any_pressed()) {
|
||||
ui.data_mut(|d| d.remove::<egui::Id>(held_id));
|
||||
}
|
||||
let held: Option<egui::Id> = ui.data(|d| d.get_temp(held_id));
|
||||
let mut open_flyout: Option<(Vec<&'static str>, egui::Rect)> = None;
|
||||
// A group torn off into a floating flyout is raised instead of opening its flyout.
|
||||
let mut raise: Option<&'static str> = None;
|
||||
let mut i = 0;
|
||||
while i < all.len() {
|
||||
if let Some(cat) = all[i].0 {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 18.0), Sense::hover());
|
||||
// A long name is cut to its first four characters in the single column.
|
||||
let cat = tl!(cat);
|
||||
let label = if cols == 1 && cat.chars().count() > 6 {
|
||||
format!("{}...", cat.chars().take(4).collect::<String>())
|
||||
} else {
|
||||
cat.to_string()
|
||||
};
|
||||
ui.painter().text(r.center() + vec2(0.0, 2.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(11.0), t.text);
|
||||
}
|
||||
// One row = `cols` slots (a category label always starts a new row).
|
||||
let mut row = vec![i];
|
||||
while row.len() < cols && i + row.len() < all.len() && all[i + row.len()].0.is_none() {
|
||||
row.push(i + row.len());
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
if cols == 1 {
|
||||
ui.add_space((WIDTH - 36.0) / 2.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
for &k in &row {
|
||||
let slot = &all[k].1;
|
||||
let shown_id = if slot.contains(&active) {
|
||||
active.to_string()
|
||||
} else {
|
||||
app.ui.slot_tool.get(slot[0]).cloned().unwrap_or_else(|| slot[0].to_string())
|
||||
};
|
||||
let Some(shown) = tool_info(&shown_id).or_else(|| tool_info(slot[0])) else { continue };
|
||||
let is_active = slot.contains(&active);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(36.0, PITCH - 1.0), Sense::click_and_drag());
|
||||
let well = egui::Rect::from_center_size(rect.center(), vec2(35.5, 27.5));
|
||||
if is_active {
|
||||
ui.painter().rect_filled(well, CornerRadius::same(1), t.tool_active);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(well, CornerRadius::same(1), t.hover);
|
||||
}
|
||||
let ir = egui::Rect::from_center_size(rect.center(), vec2(18.0, 18.0));
|
||||
icons::paint(ui, icons::tool_icon(shown.icon), ir, if is_active { t.text_strong } else { t.icon });
|
||||
if slot.len() > 1 {
|
||||
let c = rect.center() + vec2(12.5, 10.0);
|
||||
ui.painter().add(egui::Shape::convex_polygon(
|
||||
vec![c, c + vec2(-3.5, 0.0), c + vec2(0.0, -3.5)],
|
||||
t.icon,
|
||||
Stroke::NONE,
|
||||
));
|
||||
}
|
||||
let press = if slot.len() > 1 { flyout_press(ui, &resp, rect) } else { None };
|
||||
let alt = ui.input(|inp| inp.modifiers.alt);
|
||||
if press.is_some() {
|
||||
if is_floating(app, slot[0]) {
|
||||
raise = Some(slot[0]);
|
||||
} else {
|
||||
open_flyout = Some((slot.clone(), rect));
|
||||
}
|
||||
if press == Some(FlyoutPress::Primary) {
|
||||
ui.data_mut(|d| d.insert_temp(held_id, resp.id));
|
||||
}
|
||||
} else if resp.clicked() && held == Some(resp.id) {
|
||||
// Releasing the long press that opened the flyout leaves it open.
|
||||
} else if resp.clicked() && alt && slot.len() > 1 {
|
||||
let idx = slot.iter().position(|x| *x == shown.id).unwrap_or(0);
|
||||
app.select_tool(slot[(idx + 1) % slot.len()]);
|
||||
} else if resp.double_clicked() {
|
||||
app.select_tool(shown.id);
|
||||
app.run("tool.options", json!({ "tool": shown.id })).ok();
|
||||
} else if resp.clicked() {
|
||||
app.select_tool(shown.id);
|
||||
}
|
||||
resp.on_hover_text(tip(shown));
|
||||
}
|
||||
});
|
||||
i += row.len();
|
||||
}
|
||||
if let Some(key) = raise {
|
||||
raise_floating(ui.ctx(), key);
|
||||
}
|
||||
if let Some((slot, rect)) = open_flyout {
|
||||
app.ui.flyout = Some(0);
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::new("flyout-anchor"), rect);
|
||||
d.insert_temp(egui::Id::new("flyout-tools"), slot.iter().map(|s| s.to_string()).collect::<Vec<String>>());
|
||||
});
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
bottom_controls(app, ui, &t);
|
||||
// Where the panel docks: along the window's left edge, under the bars.
|
||||
let edge = ui.available_rect_before_wrap();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));
|
||||
let frame =
|
||||
egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).stroke(Stroke::new(1.5, t.border));
|
||||
match app.ui.toolbar_pos {
|
||||
None => {
|
||||
egui::Panel::left("toolbar").resizable(false).exact_size(w).frame(frame).show(ui, |ui| body(app, ui, cols, None));
|
||||
}
|
||||
Some(pos) => {
|
||||
// Floating, kept inside the window, its tools scrolling when they don't fit under it.
|
||||
let ctx = ui.ctx().clone();
|
||||
let id = egui::Id::new("toolbar-floating");
|
||||
let screen = ctx.content_rect();
|
||||
// Kept on screen by its title bar and first tools: the rest scrolls.
|
||||
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(w, 160.0), |r| vec2(r.width(), r.height().min(160.0)));
|
||||
let at = crate::floating::clamp(pos, size, screen);
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(at).show(&ctx, |ui| {
|
||||
flyout_frame(&t, false).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).show(ui, |ui| {
|
||||
ui.set_width(w);
|
||||
ui.set_max_height(screen.bottom() - at.y - 12.0);
|
||||
body(app, ui, cols, Some(at));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// Before the flyout: one torn off this frame floats from the next (its bar mustn't be in two
|
||||
// layers in one frame).
|
||||
floating(app, ui.ctx());
|
||||
flyout(app, ui.ctx());
|
||||
}
|
||||
|
||||
/// The Tools panel's contents, docked or floating at `floating`.
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egui::Pos2>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let all = slots(app);
|
||||
let bounds = ui.max_rect();
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
// Title bar with » and a grip strip: a drag on either floats the panel or moves it.
|
||||
let (hdr, hresp) = ui.allocate_exact_size(vec2(ui.available_width(), 14.0), Sense::click_and_drag());
|
||||
ui.painter().rect_filled(hdr, 0.0, t.tab_strip);
|
||||
icons::paint(
|
||||
ui,
|
||||
if cols == 2 { "chevrons-left" } else { "chevrons-right" },
|
||||
egui::Rect::from_min_size(hdr.left_top() + vec2(3.0, 2.0), vec2(10.0, 10.0)),
|
||||
if hresp.hovered() { t.text_strong } else { t.text },
|
||||
);
|
||||
let (grip, gresp) = ui.allocate_exact_size(vec2(ui.available_width(), 6.0), Sense::drag());
|
||||
for k in 0..6 {
|
||||
ui.painter().line_segment(
|
||||
[pos2(grip.center().x - 9.0, grip.top() + 1.5 + k as f32 * 0.6), pos2(grip.center().x + 9.0, grip.top() + 1.5 + k as f32 * 0.6)],
|
||||
Stroke::new(0.4, t.text_disabled),
|
||||
);
|
||||
}
|
||||
crate::floating::drag_tools(app, ui.ctx(), &hresp.union(gresp.clone()), bounds, floating);
|
||||
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked()
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
&& let Err(e) = app.run("window.toolbarColumns", json!({ "double": cols == 1 }))
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
let grip_tip = if floating.is_some() { tl!("Drag to the window's left edge to dock") } else { tl!("Drag to float the toolbar") };
|
||||
gresp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(grip_tip);
|
||||
// The tools and the controls under them scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "toolbar", |ui| {
|
||||
let active = app.session.tool_id();
|
||||
// The tool button whose long press opened its flyout, until the next press.
|
||||
let held_id = egui::Id::new("toolbar-held");
|
||||
if ui.input(|inp| inp.pointer.any_pressed()) {
|
||||
ui.data_mut(|d| d.remove::<egui::Id>(held_id));
|
||||
}
|
||||
let held: Option<egui::Id> = ui.data(|d| d.get_temp(held_id));
|
||||
let mut open_flyout: Option<(Vec<&'static str>, egui::Rect)> = None;
|
||||
// A group torn off into a floating flyout is raised instead of opening its flyout.
|
||||
let mut raise: Option<&'static str> = None;
|
||||
let mut i = 0;
|
||||
while i < all.len() {
|
||||
if let Some(cat) = all[i].0 {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 18.0), Sense::hover());
|
||||
// A long name is cut to its first four characters in the single column.
|
||||
let cat = tl!(cat);
|
||||
let label =
|
||||
if cols == 1 && cat.chars().count() > 6 { format!("{}...", cat.chars().take(4).collect::<String>()) } else { cat.to_string() };
|
||||
ui.painter().text(r.center() + vec2(0.0, 2.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(11.0), t.text);
|
||||
}
|
||||
// One row = `cols` slots (a category label always starts a new row).
|
||||
let mut row = vec![i];
|
||||
while row.len() < cols && i + row.len() < all.len() && all[i + row.len()].0.is_none() {
|
||||
row.push(i + row.len());
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
if cols == 1 {
|
||||
ui.add_space((WIDTH - 36.0) / 2.0);
|
||||
} else {
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
for &k in &row {
|
||||
let slot = &all[k].1;
|
||||
let shown_id = if slot.contains(&active) {
|
||||
active.to_string()
|
||||
} else {
|
||||
app.ui.slot_tool.get(slot[0]).cloned().unwrap_or_else(|| slot[0].to_string())
|
||||
};
|
||||
let Some(shown) = tool_info(&shown_id).or_else(|| tool_info(slot[0])) else { continue };
|
||||
let is_active = slot.contains(&active);
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(36.0, PITCH - 1.0), Sense::click_and_drag());
|
||||
let well = egui::Rect::from_center_size(rect.center(), vec2(35.5, 27.5));
|
||||
if is_active {
|
||||
ui.painter().rect_filled(well, CornerRadius::same(1), t.tool_active);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(well, CornerRadius::same(1), t.hover);
|
||||
}
|
||||
let ir = egui::Rect::from_center_size(rect.center(), vec2(18.0, 18.0));
|
||||
icons::paint(ui, icons::tool_icon(shown.icon), ir, if is_active { t.text_strong } else { t.icon });
|
||||
if slot.len() > 1 {
|
||||
let c = rect.center() + vec2(12.5, 10.0);
|
||||
ui.painter().add(egui::Shape::convex_polygon(vec![c, c + vec2(-3.5, 0.0), c + vec2(0.0, -3.5)], t.icon, Stroke::NONE));
|
||||
}
|
||||
let press = if slot.len() > 1 { flyout_press(ui, &resp, rect) } else { None };
|
||||
let alt = ui.input(|inp| inp.modifiers.alt);
|
||||
if press.is_some() {
|
||||
if is_floating(app, slot[0]) {
|
||||
raise = Some(slot[0]);
|
||||
} else {
|
||||
open_flyout = Some((slot.clone(), rect));
|
||||
}
|
||||
if press == Some(FlyoutPress::Primary) {
|
||||
ui.data_mut(|d| d.insert_temp(held_id, resp.id));
|
||||
}
|
||||
} else if resp.clicked() && held == Some(resp.id) {
|
||||
// Releasing the long press that opened the flyout leaves it open.
|
||||
} else if resp.clicked() && alt && slot.len() > 1 {
|
||||
let idx = slot.iter().position(|x| *x == shown.id).unwrap_or(0);
|
||||
app.select_tool(slot[(idx + 1) % slot.len()]);
|
||||
} else if resp.double_clicked() {
|
||||
app.select_tool(shown.id);
|
||||
app.run("tool.options", json!({ "tool": shown.id })).ok();
|
||||
} else if resp.clicked() {
|
||||
app.select_tool(shown.id);
|
||||
}
|
||||
resp.on_hover_text(tip(shown));
|
||||
}
|
||||
});
|
||||
i += row.len();
|
||||
}
|
||||
if let Some(key) = raise {
|
||||
raise_floating(ui.ctx(), key);
|
||||
}
|
||||
if let Some((slot, rect)) = open_flyout {
|
||||
app.ui.flyout = Some(0);
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::new("flyout-anchor"), rect);
|
||||
d.insert_temp(egui::Id::new("flyout-tools"), slot.iter().map(|s| s.to_string()).collect::<Vec<String>>());
|
||||
});
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
bottom_controls(app, ui, &t);
|
||||
});
|
||||
}
|
||||
|
||||
/// How a press on a tool group's button opens its flyout.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum FlyoutPress {
|
||||
|
|
@ -631,9 +653,7 @@ fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
// Kept on screen (a smaller window, or a position saved on a bigger one).
|
||||
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(strip_width(&f.tools), PITCH), |r| r.size());
|
||||
let [x, y] = f.pos;
|
||||
let (x, y) = if x.is_finite() && y.is_finite() { (x, y) } else { (screen.left() + 60.0, screen.top() + 100.0) };
|
||||
let pos = pos2(x.min(screen.right() - size.x).max(screen.left()), y.min(screen.bottom() - size.y).max(screen.top()));
|
||||
let pos = crate::floating::clamp(f.pos, size, screen);
|
||||
let mut input = FlyoutInput::default();
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true));
|
||||
|
|
|
|||
|
|
@ -9,8 +9,15 @@ use crate::{icons, scrub};
|
|||
|
||||
/// Square icon button; `selected` draws the pressed well.
|
||||
pub fn icon_button(ui: &mut Ui, icon: &str, tip: &str, selected: bool, size: f32) -> Response {
|
||||
let (_, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
|
||||
paint_icon_button(ui, resp, icon, tip, selected)
|
||||
}
|
||||
|
||||
/// [`icon_button`] drawn for a response the caller made (its own id or sense).
|
||||
pub fn paint_icon_button(ui: &mut Ui, resp: Response, icon: &str, tip: &str, selected: bool) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::click());
|
||||
let rect = resp.rect;
|
||||
let size = rect.width();
|
||||
let bg = if selected {
|
||||
t.tool_active
|
||||
} else if resp.hovered() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::sync::atomic::Ordering;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::state::{Dialog, DockTab, FloatingFlyout, UiState};
|
||||
use crate::state::{Dialog, DockTab, FloatingFlyout, FloatingPanels, UiState};
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
|
|
@ -29,6 +29,10 @@ pub struct Workspace {
|
|||
pub status_bar: bool,
|
||||
/// Tool groups torn off the toolbar into floating strips (built-in workspaces: none).
|
||||
pub floating_flyouts: Vec<FloatingFlyout>,
|
||||
/// Panels dragged out of the dock, where they float (built-in workspaces: none).
|
||||
pub floating_panels: Vec<FloatingPanels>,
|
||||
/// Where the Tools panel floats (built-in workspaces: docked).
|
||||
pub toolbar_pos: Option<[f32; 2]>,
|
||||
}
|
||||
|
||||
impl Default for Workspace {
|
||||
|
|
@ -123,6 +127,8 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
|||
open_panel: ui.open_panel.clone(),
|
||||
status_bar: ui.status_bar,
|
||||
floating_flyouts: ui.floating_flyouts.clone(),
|
||||
floating_panels: ui.floating_panels.clone(),
|
||||
toolbar_pos: ui.toolbar_pos,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +146,9 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
|||
ui.open_panel = w.open_panel.clone();
|
||||
ui.status_bar = w.status_bar;
|
||||
ui.floating_flyouts = w.floating_flyouts.clone();
|
||||
ui.floating_panels = w.floating_panels.clone();
|
||||
ui.toolbar_pos = w.toolbar_pos;
|
||||
FloatingPanels::sanitize(&mut ui.floating_panels, &mut ui.toolbar_pos);
|
||||
ui.flyout = None;
|
||||
ui.workspace = w.name.clone();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,20 @@ Bar Position (`window.taskBar.reset`), which unpins it and puts it back under th
|
|||
Panel. As in Illustrator, neither the position nor the pin is saved: each launch starts with the bar under the
|
||||
selection.
|
||||
|
||||
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
|
||||
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`;
|
||||
`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
|
||||
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).
|
||||
|
||||
Flatten Transparency: `ui.flattenTransparencyDialog` opens the `flattenTransparency` dialog for the selection
|
||||
(fields `preset`: a preset name, setting it loads that preset's options; the option keys of
|
||||
`object.flattenTransparency`: `balance` 0–100, `lineArtPpi` and `gradientPpi` 1–2400, `textToOutlines`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue