mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge upstream/main: keep panel normalization alongside dock collapse behavior
This commit is contained in:
@@ -83,6 +83,7 @@ pub fn inspect(app: &VectorcraftApp, ctx: &egui::Context) -> Value {
|
||||
"documents": app.session.documents().iter().map(|d| json!({"title": d.title(), "dirty": d.is_dirty()})).collect::<Vec<_>>(),
|
||||
"activeDocument": app.session.active_index(),
|
||||
"perf": {"frameMs": app.perf.frame_ms, "renderMs": app.perf.render_ms, "fps": app.perf.fps},
|
||||
"graphicsAdapter": app.graphics_adapter,
|
||||
// Saves and exports still being written in the background (Background Save / Export).
|
||||
"background": app.background.jobs.iter().map(|j| j.label.as_str()).collect::<Vec<_>>(),
|
||||
})
|
||||
|
||||
@@ -37,6 +37,11 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
))
|
||||
.size(11.0),
|
||||
);
|
||||
// The GPU the window renders with, for bug reports (Preferences › Performance picks it).
|
||||
if let Some(adapter) = &app.graphics_adapter {
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Graphics: {adapter}"), &[("adapter", adapter.as_str())])).size(11.0));
|
||||
}
|
||||
});
|
||||
app.ui.about = open;
|
||||
}
|
||||
|
||||
+336
-74
@@ -1,109 +1,200 @@
|
||||
//! The right dock: Properties | Layers | Libraries tabs, plus the collapsed icon-panel column
|
||||
//! whose panels pop out to the left.
|
||||
//! whose panels pop out to the left. The double arrow at the top of the dock collapses the tabbed
|
||||
//! group to icons at the top of the icon column (`window.collapseDock`); the arrow above the column
|
||||
//! then expands it again.
|
||||
|
||||
use egui::{CornerRadius, Sense, Stroke, Ui, vec2};
|
||||
use egui::{CornerRadius, Rect, Sense, Stroke, Ui, vec2};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::state::{DockTab, ICON_PANEL_GROUPS, ICON_PANELS};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, 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.
|
||||
const HEADER: f32 = 14.0;
|
||||
/// The tabbed group's width until the dock has been laid out (its default size).
|
||||
const DOCK_WIDTH: f32 = 300.0;
|
||||
/// Top of a popped-out panel.
|
||||
const FLYOUT_TOP: f32 = 110.0;
|
||||
|
||||
/// Where the icon column's left edge was drawn last (egui temp memory, one frame old).
|
||||
fn column_left_id() -> egui::Id {
|
||||
egui::Id::new("dock-icon-column-left")
|
||||
}
|
||||
|
||||
/// Collapse the tabbed group to icons or expand it again (`window.collapseDock`). Expanding while
|
||||
/// one of its panels is popped out shows that panel's tab instead.
|
||||
pub fn set_collapsed(app: &mut VectorcraftApp, collapsed: bool) {
|
||||
app.ui.dock_collapsed = collapsed;
|
||||
app.ui.dock = true;
|
||||
if !collapsed && let Some(tab) = app.ui.open_panel.as_deref().and_then(DockTab::from_id) {
|
||||
app.ui.dock_tab = tab;
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The strip along the top of a dock column with its double arrow: » collapses the dock to icons,
|
||||
/// « expands it. The whole strip is the click target (as on the toolbar). True when clicked.
|
||||
fn collapse_header(ui: &mut Ui, collapsed: bool) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (hdr, resp) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER), Sense::click());
|
||||
ui.painter().rect_filled(hdr, 0.0, t.tab_strip);
|
||||
let arrow = Rect::from_min_size(hdr.right_top() + vec2(-16.0, 1.0), vec2(12.0, 12.0));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(arrow, CornerRadius::same(2), t.hover);
|
||||
}
|
||||
let icon = if collapsed { "chevrons-left" } else { "chevrons-right" };
|
||||
icons::paint(ui, icon, arrow.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text });
|
||||
resp.on_hover_text(if collapsed { tl!("Expand Panels") } else { tl!("Collapse to Icons") }).clicked()
|
||||
}
|
||||
|
||||
/// Run `window.collapseDock` for a click on a double arrow.
|
||||
fn toggle(app: &mut VectorcraftApp, collapsed: bool) {
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
if let Err(e) = app.run("window.collapseDock", json!({ "collapsed": collapsed })) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let open = app.ui.open_panel.as_deref() == Some(id);
|
||||
if widgets::icon_button(ui, icon, label, open, 30.0).clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
// Main tabbed group.
|
||||
egui::Panel::right("dock")
|
||||
.resizable(true)
|
||||
.default_size(300.0)
|
||||
.size_range(230.0..=520.0)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.5, t.border)))
|
||||
.show(ui, |ui| {
|
||||
// Tab strip.
|
||||
let (hdr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 14.0), Sense::hover());
|
||||
ui.painter().rect_filled(hdr, 0.0, t.tab_strip);
|
||||
icons::paint(ui, "chevrons-right", egui::Rect::from_min_size(hdr.right_top() + vec2(-14.0, 2.0), vec2(10.0, 10.0)), t.text);
|
||||
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, label) in [(DockTab::Properties, "Properties"), (DockTab::Layers, "Layers"), (DockTab::Libraries, "Libraries")] {
|
||||
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);
|
||||
// Main tabbed group, unless it is collapsed to icons.
|
||||
if !app.ui.dock_collapsed {
|
||||
egui::Panel::right("dock")
|
||||
.resizable(true)
|
||||
.default_size(DOCK_WIDTH)
|
||||
.size_range(230.0..=520.0)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.5, t.border)))
|
||||
.show(ui, |ui| {
|
||||
if collapse_header(ui, false) {
|
||||
toggle(app, true);
|
||||
}
|
||||
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;
|
||||
// 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();
|
||||
}
|
||||
x = r.right();
|
||||
}
|
||||
let menu_id = match app.ui.dock_tab {
|
||||
DockTab::Properties => "properties",
|
||||
DockTab::Layers => "layers",
|
||||
DockTab::Libraries => "libraries",
|
||||
};
|
||||
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 {
|
||||
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),
|
||||
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 {
|
||||
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),
|
||||
});
|
||||
});
|
||||
});
|
||||
// Collapsed icon-panel strip, left of the expanded panel group (like Illustrator's dock).
|
||||
egui::Panel::right("icon_column")
|
||||
}
|
||||
// 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;
|
||||
let column = egui::Panel::right("icon_column")
|
||||
.resizable(false)
|
||||
.exact_size(ICON_COL)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(4, 6)).stroke(Stroke::new(1.5, t.border)))
|
||||
.frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.5, t.border)))
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
// The icons scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "icon_column", |ui| {
|
||||
for (gi, group) in ICON_PANEL_GROUPS.iter().enumerate() {
|
||||
if gi > 0 {
|
||||
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));
|
||||
}
|
||||
for id in group.iter() {
|
||||
let Some((_, label, icon)) = ICON_PANELS.iter().find(|p| p.0 == *id) else { continue };
|
||||
let open = app.ui.open_panel.as_deref() == Some(*id);
|
||||
if widgets::icon_button(ui, icon, label, open, 30.0).clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
if collapsed && collapse_header(ui, true) {
|
||||
toggle(app, false);
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::symmetric(4, 6)).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
// The icons scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "icon_column", |ui| {
|
||||
if collapsed {
|
||||
for tab in DockTab::ALL {
|
||||
let (id, label, icon) = tab.info();
|
||||
panel_icon(app, ui, id, label, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let left = column.response.rect.left();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(column_left_id(), left));
|
||||
}
|
||||
|
||||
/// An icon panel popped out next to the icon column.
|
||||
/// 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.
|
||||
pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(id) = app.ui.open_panel.clone() else { return };
|
||||
let Some((_, label, _)) = ICON_PANELS.iter().find(|p| p.0 == id) 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 t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
// Pinned by its right edge, 6 points left of the icon column: a panel wider than its 256
|
||||
// points grows towards the canvas rather than over the panel icons.
|
||||
let right = screen.right() - ICON_COL - 300.0 - 6.0;
|
||||
// Pinned by its right edge, 6 points left of the icon column wherever the dock's width (or its
|
||||
// collapsed state) put it: content wider than the flyout grows it towards the canvas rather
|
||||
// than over the panel icons. Without a dock on screen it keeps to the window's right edge.
|
||||
let column = if app.ui.dock && app.ui.screen_mode < 3 {
|
||||
ctx.data(|d| d.get_temp::<f32>(column_left_id()))
|
||||
.filter(|x| x.is_finite() && *x > screen.left() && *x <= screen.right())
|
||||
.unwrap_or_else(|| screen.right() - ICON_COL - if app.ui.dock_collapsed { 0.0 } else { DOCK_WIDTH })
|
||||
} else {
|
||||
screen.right()
|
||||
};
|
||||
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 tall = (screen.height() - FLYOUT_TOP - 26.0 - 20.0 - 40.0).clamp(160.0, 560.0);
|
||||
let mut open = true;
|
||||
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, 110.0)).show(ctx, |ui| {
|
||||
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(256.0);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(256.0, 26.0), Sense::hover());
|
||||
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 tab = egui::Rect::from_min_size(
|
||||
let title = egui::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(tab, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel);
|
||||
ui.painter().text(tab.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, tl!(label), theme::semibold(12.0), t.text);
|
||||
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 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 });
|
||||
@@ -113,8 +204,26 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
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(236.0);
|
||||
panels::show_icon_panel(app, ui, &id);
|
||||
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),
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -179,4 +288,157 @@ mod tests {
|
||||
}
|
||||
assert!(icons.last().unwrap().bottom() <= 400.0, "scrolled into view: {:?}", icons.last());
|
||||
}
|
||||
|
||||
const SCREEN: egui::Vec2 = vec2(1600.0, 1000.0);
|
||||
|
||||
/// A frame of the dock and its flyout on a 1600 × 1000 window.
|
||||
struct Harness {
|
||||
app: VectorcraftApp,
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
fn new() -> Self {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let mut h = Self { app, ctx, time: 0.0 };
|
||||
h.settle();
|
||||
h
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<egui::Event>) {
|
||||
self.time += 0.05;
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, SCREEN);
|
||||
let input = egui::RawInput { time: Some(self.time), events, screen_rect: Some(screen), ..Default::default() };
|
||||
let app = &mut self.app;
|
||||
self.ctx
|
||||
.run_ui(input, |ui| {
|
||||
show(app, ui);
|
||||
floating_panel(app, ui.ctx());
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn settle(&mut self) {
|
||||
for _ in 0..3 {
|
||||
self.frame(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
fn click(&mut self, at: egui::Pos2) {
|
||||
let button =
|
||||
|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
self.frame(vec![egui::Event::PointerMoved(at), button(true)]);
|
||||
self.frame(vec![button(false)]);
|
||||
self.settle();
|
||||
}
|
||||
|
||||
/// The icon column's left edge as last drawn.
|
||||
fn column_left(&self) -> f32 {
|
||||
self.ctx.data(|d| d.get_temp::<f32>(column_left_id())).unwrap()
|
||||
}
|
||||
|
||||
/// The panel icons of the column, top to bottom.
|
||||
fn icons(&self) -> Vec<Rect> {
|
||||
widget_rects(&self.ctx, vec2(30.0, 30.0))
|
||||
}
|
||||
|
||||
/// The double arrow at the top right of the window (the dock's, or the collapsed column's).
|
||||
fn arrow(&self) -> egui::Pos2 {
|
||||
egui::pos2(SCREEN.x - 10.0, HEADER / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_panel_count() -> usize {
|
||||
ICON_PANEL_GROUPS.iter().map(|g| g.len()).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_double_arrow_collapses_the_dock_to_icons_and_back() {
|
||||
let mut h = Harness::new();
|
||||
assert!(!h.app.ui.dock_collapsed);
|
||||
// Expanded, the group (at least its 230-point minimum) is right of the column.
|
||||
let expanded = |left: f32| left <= SCREEN.x - ICON_COL - 230.0;
|
||||
assert!(expanded(h.column_left()), "expanded: the group is right of the column: {}", h.column_left());
|
||||
assert_eq!(h.icons().len(), icon_panel_count());
|
||||
h.click(h.arrow());
|
||||
assert!(h.app.ui.dock_collapsed, "the » collapses the dock");
|
||||
assert!(h.column_left() > SCREEN.x - ICON_COL - 2.0, "collapsed: only the icon column is left: {}", h.column_left());
|
||||
let icons = h.icons();
|
||||
assert_eq!(icons.len(), icon_panel_count() + 3, "Properties, Layers and Libraries became icons");
|
||||
assert!(icons[0].top() > HEADER, "the icons sit under the « strip");
|
||||
h.click(h.arrow());
|
||||
assert!(!h.app.ui.dock_collapsed, "the « expands the dock again");
|
||||
assert!(expanded(h.column_left()), "the group is back: {}", h.column_left());
|
||||
assert_eq!(h.icons().len(), icon_panel_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_collapsed_panel_pops_out_left_of_the_column() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.settle();
|
||||
// Properties, Layers, Libraries head the column.
|
||||
let layers = h.icons()[1];
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
h.settle();
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::new("icon-panel"))).unwrap();
|
||||
let column = h.column_left();
|
||||
assert!(rect.right() <= column, "the flyout {rect:?} stays left of the column at {column}");
|
||||
assert!(rect.left() >= 0.0 && rect.bottom() <= SCREEN.y, "the flyout {rect:?} is on screen");
|
||||
assert!(rect.height() > 200.0, "Layers gets room for its list: {rect:?}");
|
||||
// Its » puts it away.
|
||||
h.click(egui::pos2(rect.right() - 13.0, rect.top() + 13.0));
|
||||
assert_eq!(h.app.ui.open_panel, None, "the flyout's » closes it");
|
||||
// Expanding with a panel popped out shows its tab.
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
h.click(h.arrow());
|
||||
assert!(!h.app.ui.dock_collapsed);
|
||||
assert_eq!((h.app.ui.open_panel.as_deref(), h.app.ui.dock_tab), (None, DockTab::Layers));
|
||||
// Properties pops out as wide as the dock and shows its sections, not a sliver, also as
|
||||
// the first flyout of a session (a fresh harness: the flyouts share one area, which
|
||||
// would otherwise keep the size the Layers flyout gave it).
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.app.run("window.panel", json!({"panel": "properties"})).unwrap();
|
||||
h.settle();
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::new("icon-panel"))).unwrap();
|
||||
assert!(rect.width() >= DOCK_WIDTH && rect.right() <= h.column_left(), "Properties flyout {rect:?}");
|
||||
// Taller than the 400 points egui sizes a new area at: its sections (Align, Quick
|
||||
// Actions) aren't cut off below Appearance.
|
||||
assert!(rect.height() > 450.0 && rect.bottom() <= SCREEN.y, "Properties flyout {rect:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_collapse_command_sets_or_toggles_the_dock() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
assert_eq!(app.run("window.collapseDock", json!({})).unwrap(), json!(true));
|
||||
assert!(app.ui.dock_collapsed && crate::menus::checked(&app, "window.collapseDock", &json!({})) == Some(true));
|
||||
assert_eq!(app.run("window.collapseDock", json!({"collapsed": true})).unwrap(), json!(true));
|
||||
assert!(app.run("window.collapseDock", json!({"collapsed": "yes"})).is_err());
|
||||
// Window › Layers pops Layers out of its icon while collapsed.
|
||||
app.run("window.panel", json!({"panel": "layers"})).unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("layers"));
|
||||
assert_eq!(crate::menus::checked(&app, "window.panel", &json!({"panel": "layers"})), Some(true));
|
||||
assert_eq!(app.run("window.collapseDock", json!({})).unwrap(), json!(false));
|
||||
assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_tab), (None, DockTab::Layers));
|
||||
app.run("window.panel", json!({"panel": "properties"})).unwrap();
|
||||
assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_tab), (None, DockTab::Properties));
|
||||
// Saved with the preferences and with a user workspace; the built-in ones expand it.
|
||||
app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
let saved: crate::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
assert!(saved.sanitized().dock_collapsed);
|
||||
app.run("window.workspace.new", json!({"name": "Icons"})).unwrap();
|
||||
app.run("window.workspace", json!({"name": "Essentials"})).unwrap();
|
||||
assert!(!app.ui.dock_collapsed, "Essentials shows the panels");
|
||||
app.run("window.workspace", json!({"name": "Icons"})).unwrap();
|
||||
assert!(app.ui.dock_collapsed, "the user workspace keeps them collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,3 +569,6 @@
|
||||
Make Compound Path Vytvořit složenou cestu
|
||||
Release Compound Path Uvolnit složenou cestu
|
||||
Select All Vybrat vše
|
||||
Collapse Panels to Icons Sbalit panely na ikony
|
||||
Collapse to Icons Sbalit na ikony
|
||||
Expand Panels Rozbalit panely
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -569,3 +569,6 @@
|
||||
Make Compound Path 複合パスを作成
|
||||
Release Compound Path 複合パスを解除
|
||||
Select All すべてを選択
|
||||
Collapse Panels to Icons パネルをアイコンに折りたたむ
|
||||
Collapse to Icons アイコンに折りたたむ
|
||||
Expand Panels パネルを展開
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -148,6 +148,7 @@
|
||||
Appearance Panel 外觀面板
|
||||
Appearance panel 外觀面板
|
||||
Applies when the linked file is relinked or updated. 重新連結或更新連結檔案時套用。
|
||||
Applies the next time VectorCraft starts. 下次啟動 VectorCraft 時生效。
|
||||
Apply Brush 套用筆刷
|
||||
Apply Character Style 套用字元樣式
|
||||
Apply Effect 套用效果
|
||||
@@ -361,6 +362,8 @@
|
||||
CMYK CMYK
|
||||
CMYK Color CMYK 色彩
|
||||
CMYK colours print with their own values; off, they go through the colour settings too. CMYK 顏色以其本身的數值列印;關閉時,也會經過色彩設定處理。
|
||||
Collapse Panels to Icons 將面板收合為圖示
|
||||
Collapse to Icons 收合為圖示
|
||||
Collate 逐份列印
|
||||
Collect for Export 收集以供匯出
|
||||
Collect in New Layer 收集至新圖層
|
||||
@@ -815,6 +818,7 @@
|
||||
Expand Gradient Mesh 展開漸層網格
|
||||
Expand Gradient To 展開漸層為
|
||||
Expand Options 展開選項
|
||||
Expand Panels 展開面板
|
||||
Expand Repeat 展開重複
|
||||
Expand Shape 展開形狀
|
||||
Expand: 展開:
|
||||
@@ -1017,6 +1021,8 @@
|
||||
Graphic Styles 繪圖樣式
|
||||
Graphic Styles Options 繪圖樣式選項
|
||||
Graphics 圖形
|
||||
Graphics Processor 圖形處理器
|
||||
Graphics: {adapter} 圖形:{adapter}
|
||||
Gravity 重力
|
||||
Grays 灰色
|
||||
Grays and Neutrals 灰色與中性色
|
||||
@@ -1073,6 +1079,7 @@
|
||||
Hide Transparency Grid 隱藏透明度格點
|
||||
High 高
|
||||
High (300 ppi) 高 (300 ppi)
|
||||
High Performance (discrete) 高效能 (獨立顯示卡)
|
||||
Highlight Alternate Glyphs 醒目提示替代字符
|
||||
Highlight anchors on mouse over 滑鼠移過時醒目提示錨點
|
||||
Highlight Substituted Fonts 醒目提示被取代的字體
|
||||
@@ -1775,6 +1782,7 @@
|
||||
Polygon Properties: 多邊形內容:
|
||||
Polygon Tool 多邊形工具
|
||||
Portrait 直向
|
||||
Power Saving (integrated) 節能 (內建顯示晶片)
|
||||
Portuguese 葡萄牙文
|
||||
Position 位置
|
||||
Postcard 明信片
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -293,6 +293,9 @@ pub struct VectorcraftApp {
|
||||
/// Windows and Linux: the window has no OS decorations, so the app bar is the title bar (drag,
|
||||
/// double-click to maximize, caption buttons) and invisible edge zones resize the window.
|
||||
pub custom_titlebar: bool,
|
||||
/// The graphics adapter the window renders with ("name (backend)"), as the host reports it:
|
||||
/// shown in Help › About and `ui.inspect` for GPU bug reports. `None` when unknown.
|
||||
pub graphics_adapter: Option<String>,
|
||||
/// File → Place: picked files, the place cursor's thumbnails, the Control bar's image details.
|
||||
pub place: place::PlaceState,
|
||||
/// The Paste commands can paste from the system clipboard alone: it holds something to paste
|
||||
@@ -367,6 +370,7 @@ impl VectorcraftApp {
|
||||
canvas_rect: None,
|
||||
hover_doc: None,
|
||||
custom_titlebar: false,
|
||||
graphics_adapter: None,
|
||||
place: Default::default(),
|
||||
system_paste: false,
|
||||
system_paste_at: f64::NEG_INFINITY,
|
||||
|
||||
+44
-21
@@ -172,6 +172,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("window.taskBar", "Contextual Task Bar", "", "{}"),
|
||||
("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.collapseDock",
|
||||
"Collapse Panels to Icons",
|
||||
"",
|
||||
"{collapsed?: bool} collapse the dock's Properties | Layers | Libraries group to icons (true), expand it (false) or toggle (omitted), as the double arrow at the top of the dock does; returns the new state",
|
||||
),
|
||||
("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"),
|
||||
("window.workspace", "Workspace", "", "{name} switch workspace (Essentials, Essentials Classic, Painting, …)"),
|
||||
("window.workspace.reset", "Reset Essentials", "", "{} reset the current workspace"),
|
||||
@@ -917,27 +923,39 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
app.ui.control_bar = on;
|
||||
Ok(json!(on))
|
||||
}
|
||||
"window.collapseDock" => {
|
||||
let collapsed = match p.get("collapsed") {
|
||||
None | Some(Value::Null) => !app.ui.dock_collapsed,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Some(Err("collapsed must be true or false".into())),
|
||||
};
|
||||
crate::dock::set_collapsed(app, collapsed);
|
||||
Ok(json!(collapsed))
|
||||
}
|
||||
"window.panel" => {
|
||||
let raw = s("panel").unwrap_or_default();
|
||||
match normalize_panel(&raw) {
|
||||
Some("properties") => {
|
||||
app.ui.dock_tab = DockTab::Properties;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Some("layers") => {
|
||||
app.ui.dock_tab = DockTab::Layers;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Some("libraries") => {
|
||||
app.ui.dock_tab = DockTab::Libraries;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Some(p) => {
|
||||
// Canonical id (case-insensitive; display labels work too), then the
|
||||
// dock tab it names, if any.
|
||||
let canonical = normalize_panel(&raw);
|
||||
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.
|
||||
(Some(p), Some(tab)) if app.ui.dock_collapsed => {
|
||||
app.ui.dock_tab = tab;
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(p) { None } else { Some(p.to_string()) };
|
||||
app.ui.dock = true;
|
||||
Ok(json!({"open": app.ui.open_panel}))
|
||||
}
|
||||
None => Err(format!("unknown panel `{raw}`")),
|
||||
(Some(_), Some(tab)) => {
|
||||
app.ui.dock_tab = tab;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
(Some(p), None) => {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(p) { None } else { Some(p.to_string()) };
|
||||
app.ui.dock = true;
|
||||
Ok(json!({"open": app.ui.open_panel}))
|
||||
}
|
||||
(None, _) => Err(format!("unknown panel `{raw}`")),
|
||||
}
|
||||
}
|
||||
"window.brightness" => match s("brightness").as_deref().and_then(Brightness::parse) {
|
||||
@@ -1217,14 +1235,13 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"window.taskBar" => app.ui.task_bar,
|
||||
"window.panel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
match normalize_panel(panel) {
|
||||
Some("properties") => app.ui.dock_tab == DockTab::Properties,
|
||||
Some("layers") => app.ui.dock_tab == DockTab::Layers,
|
||||
Some("libraries") => app.ui.dock_tab == DockTab::Libraries,
|
||||
Some(id) => app.ui.open_panel.as_deref() == Some(id),
|
||||
None => false,
|
||||
let canonical = normalize_panel(panel);
|
||||
match canonical.and_then(DockTab::from_id) {
|
||||
Some(tab) if !app.ui.dock_collapsed => app.ui.dock_tab == tab,
|
||||
_ => canonical.is_some_and(|id| app.ui.open_panel.as_deref() == Some(id)),
|
||||
}
|
||||
}
|
||||
"window.collapseDock" => app.ui.dock_collapsed,
|
||||
"window.workspace" => p.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()),
|
||||
"window.brightness" => p.get("brightness").and_then(Value::as_str).and_then(Brightness::parse) == Some(app.ui.brightness),
|
||||
"view.slices.lock" => app.session.slices_locked(),
|
||||
@@ -3166,6 +3183,12 @@ mod tests {
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("swatches"));
|
||||
assert!(checked(&app, "window.panel", &json!({"panel": "Swatches"})).unwrap());
|
||||
assert!(app.run("window.panel", json!({"panel": "nope"})).is_err());
|
||||
// A collapsed dock pops the panel out of its icon, by canonical id.
|
||||
app.ui.dock_collapsed = true;
|
||||
app.run("window.panel", json!({"panel": "Layers"})).unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("layers"));
|
||||
assert!(checked(&app, "window.panel", &json!({"panel": "Layers"})).unwrap());
|
||||
app.ui.dock_collapsed = false;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -279,6 +279,10 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) {
|
||||
d.fields.insert(sp.key.into(), json!(opts[i].0));
|
||||
}
|
||||
});
|
||||
if sp.key == "gpuPreference" {
|
||||
// The window's graphics device is chosen once, when the app starts.
|
||||
ui.label(egui::RichText::new(tl!("Applies the next time VectorCraft starts.")).color(t.text_dim).size(11.0));
|
||||
}
|
||||
}
|
||||
PrefKind::Color => {
|
||||
labeled(ui, sp.label, |ui| {
|
||||
@@ -393,6 +397,38 @@ mod tests {
|
||||
assert_eq!(a.session.prefs.ui_brightness, "light");
|
||||
}
|
||||
|
||||
/// Performance › Graphics Processor (#306): shown with its restart note, applied by OK and
|
||||
/// saved with the UI state under the key the desktop app reads before the window opens.
|
||||
#[test]
|
||||
fn graphics_processor_preference_shows_and_persists() {
|
||||
fn texts(s: &egui::Shape, out: &mut Vec<String>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut a = app();
|
||||
open(&mut a, Some("Performance"));
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
// The window sizes itself on the first frame and draws on the second.
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let mut shown = vec![];
|
||||
out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
|
||||
assert!(shown.iter().any(|t| t.starts_with("Graphics Processor")), "{shown:?}");
|
||||
assert!(shown.iter().any(|t| t == "Power Saving (integrated)"), "{shown:?}");
|
||||
assert!(shown.iter().any(|t| t == "Applies the next time VectorCraft starts."), "{shown:?}");
|
||||
a.ui.dialog.as_mut().unwrap().fields.insert("gpuPreference".into(), json!("highPerformance"));
|
||||
confirm(&mut a).unwrap();
|
||||
assert_eq!(a.session.prefs.gpu_preference, "highPerformance");
|
||||
let saved: Value = serde_json::from_slice(&serde_json::to_vec(&a.ui).unwrap()).unwrap();
|
||||
assert_eq!(saved["engine_prefs"]["gpuPreference"], json!("highPerformance"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_category_renders_fields() {
|
||||
for c in PREF_CATEGORIES {
|
||||
|
||||
@@ -76,6 +76,24 @@ pub enum DockTab {
|
||||
Libraries,
|
||||
}
|
||||
|
||||
impl DockTab {
|
||||
/// The tab's panel id (`window.panel`), English label and icon (when the dock is collapsed).
|
||||
pub fn info(self) -> (&'static str, &'static str, &'static str) {
|
||||
match self {
|
||||
DockTab::Properties => ("properties", "Properties", "dc-options"),
|
||||
DockTab::Layers => ("layers", "Layers", "layers"),
|
||||
DockTab::Libraries => ("libraries", "Libraries", "library"),
|
||||
}
|
||||
}
|
||||
|
||||
pub const ALL: [DockTab; 3] = [DockTab::Properties, DockTab::Layers, DockTab::Libraries];
|
||||
|
||||
/// The tab whose panel id is `id`.
|
||||
pub fn from_id(id: &str) -> Option<DockTab> {
|
||||
DockTab::ALL.into_iter().find(|t| t.info().0 == id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Panels that live as collapsed icons in the dock (Essentials Classic).
|
||||
pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
("color", "Color", "palette"),
|
||||
@@ -219,7 +237,12 @@ pub struct UiState {
|
||||
pub legacy_language: Option<String>,
|
||||
pub brightness: Brightness,
|
||||
pub dock_tab: DockTab,
|
||||
/// Icon panel currently popped out of the collapsed column.
|
||||
/// The dock's tabbed group (Properties | Layers | Libraries) is collapsed to icons at the top of
|
||||
/// the icon column (the dock's double arrow, `window.collapseDock`).
|
||||
#[serde(default)]
|
||||
pub dock_collapsed: bool,
|
||||
/// Icon panel currently popped out of the collapsed column (also `properties`, `layers` or
|
||||
/// `libraries` while the dock is collapsed).
|
||||
pub open_panel: Option<String>,
|
||||
pub control_bar: bool,
|
||||
pub toolbar: bool,
|
||||
@@ -358,6 +381,7 @@ impl Default for UiState {
|
||||
legacy_language: None,
|
||||
brightness: Brightness::MediumDark,
|
||||
dock_tab: DockTab::Properties,
|
||||
dock_collapsed: false,
|
||||
open_panel: None,
|
||||
control_bar: false,
|
||||
toolbar: true,
|
||||
|
||||
@@ -23,6 +23,8 @@ pub struct Workspace {
|
||||
pub task_bar: bool,
|
||||
pub dock: bool,
|
||||
pub dock_tab: DockTab,
|
||||
/// The dock's tabbed group is collapsed to icons (built-in workspaces: expanded).
|
||||
pub dock_collapsed: bool,
|
||||
pub open_panel: Option<String>,
|
||||
pub status_bar: bool,
|
||||
}
|
||||
@@ -115,6 +117,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
task_bar: ui.task_bar,
|
||||
dock: ui.dock,
|
||||
dock_tab: ui.dock_tab,
|
||||
dock_collapsed: ui.dock_collapsed,
|
||||
open_panel: ui.open_panel.clone(),
|
||||
status_bar: ui.status_bar,
|
||||
}
|
||||
@@ -130,6 +133,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.task_bar = w.task_bar;
|
||||
ui.dock = w.dock;
|
||||
ui.dock_tab = w.dock_tab;
|
||||
ui.dock_collapsed = w.dock_collapsed;
|
||||
ui.open_panel = w.open_panel.clone();
|
||||
ui.status_bar = w.status_bar;
|
||||
ui.flyout = None;
|
||||
|
||||
Reference in New Issue
Block a user