mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 07:17:08 +00:00
Merge pull request #6988 from dotCooCoo/feat/control-bar-float
Control bar: dragging the gripper away from both docked edges floats the bar
This commit is contained in:
@@ -469,7 +469,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.control.dock",
|
||||
"Dock Control Bar",
|
||||
"",
|
||||
"{edge: \"top\"|\"bottom\"} dock the Control bar to the top of the window, under the menu bar, or to its bottom, as the bar's panel menu (Dock to Top, Dock to Bottom) and a drag of its gripper do; kept with the workspace; another edge is an error and changes nothing; returns {edge}",
|
||||
"{edge: \"top\"|\"bottom\"|\"float\"} dock the Control bar to the top of the window, under the menu bar, or to its bottom, as the bar's panel menu (Dock to Top, Dock to Bottom) and a drag of its gripper do, or float it over the window at the position it last floated at (first: its top-left corner at 60, 80 in window points), as a gripper drag released away from both edges does; kept with the workspace; another edge is an error and changes nothing; returns {edge}",
|
||||
),
|
||||
(
|
||||
"window.control.options",
|
||||
|
||||
@@ -577,10 +577,10 @@ impl VectorcraftApp {
|
||||
chrome::hint_bar(self, ui);
|
||||
}
|
||||
if (self.ui.screen_mode >= 3 || (!self.ui.status_bar && !self.ui.help_bar))
|
||||
&& !(self.ui.control_bar && self.ui.control_bar_bottom && self.ui.screen_mode < 2)
|
||||
&& !(self.ui.control_bar && self.ui.control_bar_bottom && !self.ui.control_bar_floating && self.ui.screen_mode < 2)
|
||||
{
|
||||
// Keeps the previous frame's value valid for menus drawn above, before the bars. A
|
||||
// Control bar docked at the bottom has set it to the bar's top.
|
||||
// Control bar docked at the bottom has set it to the bar's top; a floating one sets none.
|
||||
widgets::set_menu_floor(&ctx, ctx.content_rect().bottom());
|
||||
}
|
||||
if self.ui.toolbar && self.ui.screen_mode < 3 {
|
||||
|
||||
@@ -18,9 +18,12 @@ mod hints;
|
||||
mod status_bar;
|
||||
mod tabs;
|
||||
|
||||
pub use control_bar::{
|
||||
AnchorControls, CONTROL_BAR_EDGES, CONTROL_BAR_GROUPS, anchor_buttons, anchor_controls, control_bar, control_bar_moving, control_bar_placement,
|
||||
read_hidden_groups,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use control_bar::ISOLATION_BUTTON;
|
||||
pub use control_bar::{AnchorControls, CONTROL_BAR_EDGES, CONTROL_BAR_GROUPS, anchor_buttons, anchor_controls, control_bar, read_hidden_groups};
|
||||
use control_bar::{CONTROL_BAR_FLOAT_AT, ISOLATION_BUTTON};
|
||||
#[cfg(test)]
|
||||
use hints::hint_segments;
|
||||
pub use hints::{hint_bar, hint_strings};
|
||||
|
||||
@@ -303,9 +303,18 @@ fn artboard_align_group(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The edges of the window the Control bar docks to (`window.control.dock {edge}`): the top, then
|
||||
/// the bottom.
|
||||
pub const CONTROL_BAR_EDGES: [&str; 2] = ["top", "bottom"];
|
||||
/// The places the Control bar goes to (`window.control.dock {edge}`): docked to the top or the
|
||||
/// bottom of the window, or floating over it.
|
||||
pub const CONTROL_BAR_EDGES: [&str; 3] = ["top", "bottom", "float"];
|
||||
|
||||
/// Where `window.control.dock {edge: "float"}` floats the Control bar the first time: its top-left
|
||||
/// corner in window points.
|
||||
pub const CONTROL_BAR_FLOAT_AT: [f32; 2] = [60.0, 80.0];
|
||||
|
||||
/// The entry of [`CONTROL_BAR_EDGES`] the Control bar is at in `ui`.
|
||||
pub fn control_bar_placement(ui: &crate::state::UiState) -> &'static str {
|
||||
CONTROL_BAR_EDGES[if ui.control_bar_floating { 2 } else { usize::from(ui.control_bar_bottom) }]
|
||||
}
|
||||
|
||||
/// The groups of the Control bar's options that its panel menu turns on or off
|
||||
/// (`window.control.options {group, visible?}`), by id, in the menu's order. Every group is on
|
||||
@@ -370,6 +379,76 @@ const MENU_BUTTON: f32 = 16.0;
|
||||
/// The height of the band drawn along the edge of the window that a gripper drag docks the bar to.
|
||||
const DOCK_BAND: f32 = 4.0;
|
||||
|
||||
/// The Control bar's height, docked or floating.
|
||||
const HEIGHT: f32 = 34.0;
|
||||
|
||||
/// The Control bar's inner margin left and right, around its gripper and its panel menu button.
|
||||
const MARGIN: f32 = 10.0;
|
||||
|
||||
/// The width the floating Control bar takes in a window wide enough for it.
|
||||
const FLOAT_WIDTH: f32 = 800.0;
|
||||
|
||||
/// How near the edge the bar docks to a gripper drag is released for the bar to dock there; a
|
||||
/// release farther from both edges floats the bar.
|
||||
const DOCK_ZONE: f32 = 24.0;
|
||||
|
||||
/// The key of a gripper drag in progress: where the pointer holds the bar from its top-left
|
||||
/// corner, and the gripper's widget id.
|
||||
fn move_id() -> egui::Id {
|
||||
egui::Id::unique("control_bar_gripper_move")
|
||||
}
|
||||
|
||||
/// Is the Control bar's gripper being dragged, the frame that releases it included? An Escape then
|
||||
/// cancels the drag, and nothing else. A drag left over from a bar hidden while the button was held
|
||||
/// ends with the button.
|
||||
pub fn control_bar_moving(ctx: &egui::Context) -> bool {
|
||||
let Some((_, gripper)) = ctx.data(|d| d.get_temp::<(egui::Vec2, egui::Id)>(move_id())) else { return false };
|
||||
ctx.input(|i| i.pointer.primary_down()) || ctx.drag_stopped_id() == Some(gripper)
|
||||
}
|
||||
|
||||
/// Where a gripper release puts the Control bar.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Release {
|
||||
/// Docked to the bottom (true) or the top (false).
|
||||
Edge(bool),
|
||||
/// Floating, its top-left corner here.
|
||||
Float(egui::Pos2),
|
||||
}
|
||||
|
||||
/// The width of the floating Control bar in a window whose room below the menu bar is `room`.
|
||||
fn float_width(room: egui::Rect) -> f32 {
|
||||
FLOAT_WIDTH.min(room.width()).max(2.0 * MARGIN + MENU_BUTTON + 6.0)
|
||||
}
|
||||
|
||||
/// The floating Control bar's rect for its saved corner `pos`, kept inside `room`, the window below
|
||||
/// the menu bar, so its gripper and panel menu stay in reach.
|
||||
fn float_rect(pos: Option<[f32; 2]>, room: egui::Rect) -> egui::Rect {
|
||||
let size = vec2(float_width(room), HEIGHT);
|
||||
egui::Rect::from_min_size(crate::tools_float::clamp(pos.unwrap_or(CONTROL_BAR_FLOAT_AT), size, room), size)
|
||||
}
|
||||
|
||||
/// Where a gripper drag released at `at` puts the bar, held at `grab` from its top-left corner:
|
||||
/// within [`DOCK_ZONE`] of the top or the bottom of `room` (the window below the menu bar) docked
|
||||
/// there, farther from both floating under the pointer. In a window too short for the two zones
|
||||
/// and a floating bar between them, it docks at the nearer edge. A release outside `window` puts
|
||||
/// it nowhere, and the bar stays where it was.
|
||||
fn release_at(at: egui::Pos2, grab: egui::Vec2, room: egui::Rect, window: egui::Rect) -> Option<Release> {
|
||||
if !window.contains(at) || !at.x.is_finite() || !at.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if room.height() < 2.0 * DOCK_ZONE + HEIGHT {
|
||||
return Some(Release::Edge(at.y > room.center().y));
|
||||
}
|
||||
Some(if at.y < room.top() + DOCK_ZONE {
|
||||
Release::Edge(false)
|
||||
} else if at.y > room.bottom() - DOCK_ZONE {
|
||||
Release::Edge(true)
|
||||
} else {
|
||||
let corner = at - grab;
|
||||
Release::Float(float_rect(Some([corner.x, corner.y]), room).min)
|
||||
})
|
||||
}
|
||||
|
||||
/// Docks the Control bar to `edge` of [`CONTROL_BAR_EDGES`], through its command.
|
||||
fn dock(app: &mut VectorcraftApp, edge: &str) {
|
||||
crate::menus::invoke(app, "window.control.dock", json!({ "edge": edge }));
|
||||
@@ -377,7 +456,8 @@ fn dock(app: &mut VectorcraftApp, edge: &str) {
|
||||
|
||||
/// The Control bar's panel menu: an item for each group of [`CONTROL_BAR_GROUPS`], checked while
|
||||
/// the group is on, which turns it on or off; then Dock to Top and Dock to Bottom, the edge the bar
|
||||
/// is docked to checked. In a window too short for every item, the list scrolls.
|
||||
/// is docked to checked, and neither while it floats. In a window too short for every item, the
|
||||
/// list scrolls.
|
||||
fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
||||
let resp = crate::panels::menu_button(ui, "control", rect);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
@@ -390,7 +470,7 @@ fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
||||
}
|
||||
ui.separator();
|
||||
for (i, label) in [tl!("Dock to Top"), tl!("Dock to Bottom")].into_iter().enumerate() {
|
||||
if widgets::menu_item(ui, label, true, usize::from(app.ui.control_bar_bottom) == i) {
|
||||
if widgets::menu_item(ui, label, true, control_bar_placement(&app.ui) == CONTROL_BAR_EDGES[i]) {
|
||||
dock(app, CONTROL_BAR_EDGES[i]);
|
||||
}
|
||||
}
|
||||
@@ -453,170 +533,227 @@ fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity:
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar's gripper. Dragging it shows a band along the top or the bottom edge of the
|
||||
/// window, whichever the pointer is nearer, and docks the bar there on release.
|
||||
fn gripper(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) {
|
||||
/// The Control bar's gripper, at the left end of the bar `bar`. Dragging it shows where a release
|
||||
/// puts the bar ([`release_at`] in `dock_room`, the window below the menu bar): a band along the
|
||||
/// edge it docks to, or the outline of the bar floating under the pointer. The bar stays where it is
|
||||
/// until the release. Escape, or the window losing the focus, cancels the drag, and a release
|
||||
/// outside the window leaves the bar where it was.
|
||||
fn gripper(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, dock_room: egui::Rect) {
|
||||
let (g, resp) = ui.allocate_exact_size(vec2(6.0, 20.0), Sense::drag());
|
||||
for i in 0..5 {
|
||||
ui.painter().circle_filled(g.center_top() + vec2(0.0, 2.0 + i as f32 * 4.0), 0.9, t.text_disabled);
|
||||
}
|
||||
let resp = resp.on_hover_cursor(egui::CursorIcon::Grab);
|
||||
if !resp.dragged() && !resp.drag_stopped() {
|
||||
let ctx = ui.ctx().clone();
|
||||
if resp.drag_started()
|
||||
&& let Some(press) = ctx.input(|i| i.pointer.press_origin())
|
||||
{
|
||||
ctx.data_mut(|d| d.insert_temp(move_id(), (press - bar.min, resp.id)));
|
||||
}
|
||||
let Some((grab, _)) = ctx.data(|d| d.get_temp::<(egui::Vec2, egui::Id)>(move_id())) else { return };
|
||||
let canceled = ctx.input(|i| i.key_pressed(egui::Key::Escape) || !i.focused);
|
||||
if canceled || !(resp.dragged() || resp.drag_stopped()) {
|
||||
ctx.data_mut(|d| d.remove::<(egui::Vec2, egui::Id)>(move_id()));
|
||||
return;
|
||||
}
|
||||
let Some(pos) = resp.interact_pointer_pos().or_else(|| ui.ctx().pointer_latest_pos()) else { return };
|
||||
let window = ui.ctx().content_rect();
|
||||
let bottom = pos.y > window.center().y;
|
||||
let window = ctx.content_rect();
|
||||
let target = ctx.input(|i| i.pointer.latest_pos()).and_then(|at| release_at(at, grab, dock_room, window));
|
||||
if resp.drag_stopped() {
|
||||
if bottom != app.ui.control_bar_bottom {
|
||||
dock(app, CONTROL_BAR_EDGES[usize::from(bottom)]);
|
||||
ctx.data_mut(|d| d.remove::<(egui::Vec2, egui::Id)>(move_id()));
|
||||
match target {
|
||||
Some(Release::Edge(bottom)) if app.ui.control_bar_floating || bottom != app.ui.control_bar_bottom => {
|
||||
dock(app, CONTROL_BAR_EDGES[usize::from(bottom)]);
|
||||
}
|
||||
Some(Release::Float(at)) => {
|
||||
app.ui.control_bar_float_pos = Some([at.x, at.y]);
|
||||
if !app.ui.control_bar_floating {
|
||||
dock(app, CONTROL_BAR_EDGES[2]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
let band = if bottom {
|
||||
egui::Rect::from_min_max(egui::pos2(window.left(), window.bottom() - DOCK_BAND), window.max)
|
||||
} else {
|
||||
egui::Rect::from_min_max(window.min, egui::pos2(window.right(), window.top() + DOCK_BAND))
|
||||
};
|
||||
let layer = egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("control_bar_dock_band"));
|
||||
ui.ctx().layer_painter(layer).rect_filled(band, 0.0, t.accent);
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("control_bar_dock_band")));
|
||||
match target {
|
||||
Some(Release::Edge(bottom)) => {
|
||||
let band = if bottom {
|
||||
egui::Rect::from_min_max(egui::pos2(dock_room.left(), dock_room.bottom() - DOCK_BAND), dock_room.max)
|
||||
} else {
|
||||
egui::Rect::from_min_max(dock_room.min, egui::pos2(dock_room.right(), dock_room.top() + DOCK_BAND))
|
||||
};
|
||||
painter.rect_filled(band, 0.0, t.accent);
|
||||
}
|
||||
Some(Release::Float(at)) => {
|
||||
let outline = egui::Rect::from_min_size(at, vec2(float_width(dock_room), HEIGHT));
|
||||
painter.rect_stroke(outline, 0.0, Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar (Window → Control), context-sensitive like Illustrator's, docked to the top or
|
||||
/// the bottom of the window.
|
||||
/// the bottom of the window, or floating over the window below the menu bar, where it takes no room
|
||||
/// from the canvas. A floating bar is [`FLOAT_WIDTH`] wide, or as wide as the window when that is
|
||||
/// narrower, and is kept inside the window when the window shrinks.
|
||||
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
// The window below the menu bar: where the bar docks at the top, and where it floats.
|
||||
let dock_room = ui.available_rect_before_wrap();
|
||||
let frame = egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(10, 0)).stroke(Stroke::new(1.0, t.border));
|
||||
if app.ui.control_bar_floating {
|
||||
let bar = float_rect(app.ui.control_bar_float_pos, dock_room);
|
||||
let ctx = ui.ctx().clone();
|
||||
let area = egui::Area::new(egui::Id::unique("control_bar_floating")).order(egui::Order::Middle).fixed_pos(bar.min).constrain(false);
|
||||
area.show(&ctx, |ui| {
|
||||
frame.show(ui, |ui| {
|
||||
// Inside the frame's margins and its 1-point border.
|
||||
ui.set_width(bar.width() - 2.0 * MARGIN - 2.0);
|
||||
ui.set_height(HEIGHT - 2.0);
|
||||
contents(app, ui, &t, bar, dock_room, true);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
let bottom = app.ui.control_bar_bottom;
|
||||
let panel = if bottom { egui::Panel::bottom("control_bar") } else { egui::Panel::top("control_bar") };
|
||||
let bar = panel
|
||||
.exact_size(34.0)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(10, 0)).stroke(Stroke::new(1.0, t.border)))
|
||||
.show(ui, |ui| {
|
||||
// The panel menu button takes the right end, and the controls trim to the room left of it:
|
||||
// they are laid out in that room, so every room check below leaves the button's width.
|
||||
let full = ui.max_rect();
|
||||
let menu = egui::Rect::from_center_size(egui::pos2(full.right() - MENU_BUTTON / 2.0, full.center().y), vec2(MENU_BUTTON, MENU_BUTTON));
|
||||
let room = egui::Rect::from_min_max(full.min, egui::pos2(menu.left() - ui.spacing().item_spacing.x, full.bottom()));
|
||||
let layout = egui::Layout::left_to_right(egui::Align::Center);
|
||||
let controls = ui.scope_builder(egui::UiBuilder::new().max_rect(room).layout(layout), |ui| {
|
||||
gripper(app, ui, &t);
|
||||
if app.session.active().is_none() {
|
||||
ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
// The Artboard tool's controls fill the narrowest window, so they keep it to themselves there.
|
||||
if group_on(app, "snapping") && (app.session.tool_id() != "artboard" || ui.available_width() >= ARTBOARD_SNAPPING_WIDTH) {
|
||||
snapping_menu(app, ui);
|
||||
}
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let sel = st.selection.objects.clone();
|
||||
let units = app.session.general_unit();
|
||||
let first = sel.first().and_then(|id| st.doc.node(*id));
|
||||
let anchors = anchor_controls(app);
|
||||
let label = match first {
|
||||
_ if app.session.tool_id() == "artboard" => tl!("Artboard"),
|
||||
Some(_) if sel.len() == 1 && anchors != AnchorControls::None => {
|
||||
tl!("Anchor Point")
|
||||
}
|
||||
Some(vectorcraft_doc::Node { kind: NodeKind::Image(im), .. }) if sel.len() == 1 => {
|
||||
if im.link.is_some() {
|
||||
tl!("Linked File")
|
||||
} else {
|
||||
tl!("Embedded")
|
||||
}
|
||||
}
|
||||
Some(n) if sel.len() == 1 && crate::panels::image_trace::is_trace(n) => {
|
||||
tl!("Image Tracing")
|
||||
}
|
||||
_ => tl!(crate::panels::appearance::object_label(app)),
|
||||
};
|
||||
let has_first = first.is_some();
|
||||
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
|
||||
// With the Artboard tool in use, the bar shows the active artboard's controls in place
|
||||
// of the object controls.
|
||||
let (artboard_options, transform) = (group_on(app, "artboardOptions"), group_on(app, "transform"));
|
||||
if crate::panels::artboards::control_bar(app, ui, artboard_options, transform) {
|
||||
artboard_align_group(app, ui);
|
||||
return;
|
||||
}
|
||||
if anchors != AnchorControls::None && group_on(app, "anchors") {
|
||||
anchor_buttons(app, ui, anchors);
|
||||
}
|
||||
let object_options = group_on(app, "objectOptions");
|
||||
if object_options {
|
||||
isolation_button(app, ui);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
// An image or an Image Trace object shows its own controls in place of Fill and Stroke,
|
||||
// and leaves Fill and Stroke out while the Image Options group is off too.
|
||||
let image = if group_on(app, "imageOptions") {
|
||||
crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui)
|
||||
} else {
|
||||
crate::place::selected_image_info(app).is_some() || crate::panels::image_trace::selected_trace(app).is_some()
|
||||
};
|
||||
if group_on(app, "toolOptions") {
|
||||
crate::toolbar::control_bar_options(app, ui);
|
||||
}
|
||||
if object_options {
|
||||
crate::dialogs::envelope::control_bar(app, ui);
|
||||
crate::panels::symbols::control_bar(app, ui);
|
||||
}
|
||||
let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 };
|
||||
if !image && group_on(app, "fillStroke") {
|
||||
fill_and_stroke(app, ui);
|
||||
}
|
||||
opacity_and_style(app, ui, &t, opacity, !sel.is_empty());
|
||||
if group_on(app, "character") {
|
||||
crate::panels::character::control_bar(app, ui);
|
||||
}
|
||||
if !groups_before_transform(app, ui, sel.len()) {
|
||||
return;
|
||||
}
|
||||
// The Transform link opens the whole Transform panel (reference point, rotate,
|
||||
// shear, options) in a popover; X/Y/W/H follow inline while the bar has room.
|
||||
crate::panels::transform::link(app, ui);
|
||||
// The bounding box, rotated with rotated objects: its centre and its own sides.
|
||||
if let Some(b) = app.selection_box()
|
||||
&& ui.available_width() >= inline_transform_width(ui, &t)
|
||||
{
|
||||
let c = b.center();
|
||||
let origin = app.ruler_origin();
|
||||
let link = crate::panels::transform::proportional(app, ui);
|
||||
let copy = crate::panels::transform::alt_enter_copies(app, ui);
|
||||
let values = [(c.x, origin.x), (c.y, origin.y), (b.rect.width(), 0.0), (b.rect.height(), 0.0)];
|
||||
// The W/H link sits between W and H, but is made after H so Tab goes from W
|
||||
// straight to H and finds the link after it.
|
||||
let mut link_room = None;
|
||||
for ((k, lbl), (v, offset)) in INLINE_FIELDS.into_iter().zip(values) {
|
||||
if k == "height" {
|
||||
link_room = Some(crate::panels::transform::constrain_link_room(ui));
|
||||
}
|
||||
widgets::field_label(ui, inline_label(&t, lbl));
|
||||
let (id, shown) = (("cb", k), Some(v - offset));
|
||||
let set = if matches!(k, "width" | "height") {
|
||||
widgets::size_field(ui, id, shown, units, INLINE_FIELD_WIDTH)
|
||||
} else {
|
||||
widgets::num_field(ui, id, shown, units, INLINE_FIELD_WIDTH)
|
||||
};
|
||||
if let Some(nv) = set {
|
||||
app.run("object.setBounds", json!({k: nv + offset, "reference": 4, "proportional": link, "copy": copy})).ok();
|
||||
}
|
||||
}
|
||||
if let Some(room) = link_room {
|
||||
let id = ui.scope_id().with("cb-constrain-link");
|
||||
crate::panels::transform::constrain_link_at(app, ui, ui.interact(room, id, Sense::click()));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Controls that keep their size can still run into the button's room in a narrow window;
|
||||
// the button is left out then.
|
||||
if controls.response.rect.right() <= menu.left() {
|
||||
panel_menu(app, ui, menu);
|
||||
}
|
||||
});
|
||||
let bar = panel.exact_size(HEIGHT).frame(frame).show(ui, |ui| {
|
||||
// Out to the frame's 1-point border.
|
||||
let outer = ui.max_rect().expand2(vec2(MARGIN + 1.0, 1.0));
|
||||
contents(app, ui, &t, outer, dock_room, false);
|
||||
});
|
||||
// Docked at the bottom, the bar is a bottom bar that menus stop above.
|
||||
if bottom {
|
||||
widgets::set_menu_floor(ui.ctx(), bar.response.rect.top());
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar's gripper, controls and panel menu, in the bar `bar`. A `floating` bar clips its
|
||||
/// controls to the room left of the panel menu button, and always shows the button.
|
||||
fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, dock_room: egui::Rect, floating: bool) {
|
||||
// The panel menu button takes the right end, and the controls trim to the room left of it:
|
||||
// they are laid out in that room, so every room check below leaves the button's width.
|
||||
let full = ui.max_rect();
|
||||
let menu = egui::Rect::from_center_size(egui::pos2(full.right() - MENU_BUTTON / 2.0, full.center().y), vec2(MENU_BUTTON, MENU_BUTTON));
|
||||
let room = egui::Rect::from_min_max(full.min, egui::pos2(menu.left() - ui.spacing().item_spacing.x, full.bottom()));
|
||||
let layout = egui::Layout::left_to_right(egui::Align::Center);
|
||||
let controls = ui.scope_builder(egui::UiBuilder::new().max_rect(room).layout(layout), |ui| {
|
||||
if floating {
|
||||
ui.set_clip_rect(room.intersect(ui.clip_rect()));
|
||||
}
|
||||
gripper(app, ui, t, bar, dock_room);
|
||||
if app.session.active().is_none() {
|
||||
ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
// The Artboard tool's controls fill the narrowest window, so they keep it to themselves there.
|
||||
if group_on(app, "snapping") && (app.session.tool_id() != "artboard" || ui.available_width() >= ARTBOARD_SNAPPING_WIDTH) {
|
||||
snapping_menu(app, ui);
|
||||
}
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let sel = st.selection.objects.clone();
|
||||
let units = app.session.general_unit();
|
||||
let first = sel.first().and_then(|id| st.doc.node(*id));
|
||||
let anchors = anchor_controls(app);
|
||||
let label = match first {
|
||||
_ if app.session.tool_id() == "artboard" => tl!("Artboard"),
|
||||
Some(_) if sel.len() == 1 && anchors != AnchorControls::None => {
|
||||
tl!("Anchor Point")
|
||||
}
|
||||
Some(vectorcraft_doc::Node { kind: NodeKind::Image(im), .. }) if sel.len() == 1 => {
|
||||
if im.link.is_some() {
|
||||
tl!("Linked File")
|
||||
} else {
|
||||
tl!("Embedded")
|
||||
}
|
||||
}
|
||||
Some(n) if sel.len() == 1 && crate::panels::image_trace::is_trace(n) => {
|
||||
tl!("Image Tracing")
|
||||
}
|
||||
_ => tl!(crate::panels::appearance::object_label(app)),
|
||||
};
|
||||
let has_first = first.is_some();
|
||||
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
|
||||
// With the Artboard tool in use, the bar shows the active artboard's controls in place
|
||||
// of the object controls.
|
||||
let (artboard_options, transform) = (group_on(app, "artboardOptions"), group_on(app, "transform"));
|
||||
if crate::panels::artboards::control_bar(app, ui, artboard_options, transform) {
|
||||
artboard_align_group(app, ui);
|
||||
return;
|
||||
}
|
||||
if anchors != AnchorControls::None && group_on(app, "anchors") {
|
||||
anchor_buttons(app, ui, anchors);
|
||||
}
|
||||
let object_options = group_on(app, "objectOptions");
|
||||
if object_options {
|
||||
isolation_button(app, ui);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
// An image or an Image Trace object shows its own controls in place of Fill and Stroke,
|
||||
// and leaves Fill and Stroke out while the Image Options group is off too.
|
||||
let image = if group_on(app, "imageOptions") {
|
||||
crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui)
|
||||
} else {
|
||||
crate::place::selected_image_info(app).is_some() || crate::panels::image_trace::selected_trace(app).is_some()
|
||||
};
|
||||
if group_on(app, "toolOptions") {
|
||||
crate::toolbar::control_bar_options(app, ui);
|
||||
}
|
||||
if object_options {
|
||||
crate::dialogs::envelope::control_bar(app, ui);
|
||||
crate::panels::symbols::control_bar(app, ui);
|
||||
}
|
||||
let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 };
|
||||
if !image && group_on(app, "fillStroke") {
|
||||
fill_and_stroke(app, ui);
|
||||
}
|
||||
opacity_and_style(app, ui, t, opacity, !sel.is_empty());
|
||||
if group_on(app, "character") {
|
||||
crate::panels::character::control_bar(app, ui);
|
||||
}
|
||||
if !groups_before_transform(app, ui, sel.len()) {
|
||||
return;
|
||||
}
|
||||
// The Transform link opens the whole Transform panel (reference point, rotate,
|
||||
// shear, options) in a popover; X/Y/W/H follow inline while the bar has room.
|
||||
crate::panels::transform::link(app, ui);
|
||||
// The bounding box, rotated with rotated objects: its centre and its own sides.
|
||||
if let Some(b) = app.selection_box()
|
||||
&& ui.available_width() >= inline_transform_width(ui, t)
|
||||
{
|
||||
let c = b.center();
|
||||
let origin = app.ruler_origin();
|
||||
let link = crate::panels::transform::proportional(app, ui);
|
||||
let copy = crate::panels::transform::alt_enter_copies(app, ui);
|
||||
let values = [(c.x, origin.x), (c.y, origin.y), (b.rect.width(), 0.0), (b.rect.height(), 0.0)];
|
||||
// The W/H link sits between W and H, but is made after H so Tab goes from W
|
||||
// straight to H and finds the link after it.
|
||||
let mut link_room = None;
|
||||
for ((k, lbl), (v, offset)) in INLINE_FIELDS.into_iter().zip(values) {
|
||||
if k == "height" {
|
||||
link_room = Some(crate::panels::transform::constrain_link_room(ui));
|
||||
}
|
||||
widgets::field_label(ui, inline_label(t, lbl));
|
||||
let (id, shown) = (("cb", k), Some(v - offset));
|
||||
let set = if matches!(k, "width" | "height") {
|
||||
widgets::size_field(ui, id, shown, units, INLINE_FIELD_WIDTH)
|
||||
} else {
|
||||
widgets::num_field(ui, id, shown, units, INLINE_FIELD_WIDTH)
|
||||
};
|
||||
if let Some(nv) = set {
|
||||
app.run("object.setBounds", json!({k: nv + offset, "reference": 4, "proportional": link, "copy": copy})).ok();
|
||||
}
|
||||
}
|
||||
if let Some(room) = link_room {
|
||||
let id = ui.scope_id().with("cb-constrain-link");
|
||||
crate::panels::transform::constrain_link_at(app, ui, ui.interact(room, id, Sense::click()));
|
||||
}
|
||||
}
|
||||
});
|
||||
// Controls that keep their size can still run into the button's room in a narrow window;
|
||||
// the button is left out then. A floating bar clips them, so its button always shows.
|
||||
if floating || controls.response.rect.right() <= menu.left() {
|
||||
panel_menu(app, ui, menu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2354,49 +2354,279 @@ fn the_control_bar_panel_menu_scrolls_in_a_short_window() {
|
||||
assert!(app.ui.control_bar_bottom, "Dock to Bottom docks the bar at the bottom");
|
||||
}
|
||||
|
||||
/// Dragging the Control bar's gripper docks the bar at the edge of the window the pointer is
|
||||
/// released nearer; a drag released nearer the edge it is at leaves it there.
|
||||
/// The gripper of a Control bar docked at the top or the bottom of [`DOCK_SCREEN`]: the bar's first
|
||||
/// item, inside its 10-point margin, centered in the 34-point bar.
|
||||
fn docked_grip(bottom: bool) -> egui::Pos2 {
|
||||
egui::pos2(13.0, if bottom { DOCK_SCREEN.bottom() - 17.0 } else { 17.0 })
|
||||
}
|
||||
|
||||
/// The gripper of the Control bar floating with its top-left corner at `pos`: inside its 1-point
|
||||
/// border and 10-point margin.
|
||||
fn floating_grip(pos: [f32; 2]) -> egui::Pos2 {
|
||||
egui::pos2(pos[0] + 14.0, pos[1] + 17.0)
|
||||
}
|
||||
|
||||
/// A drag of the Control bar's gripper from `from` to `to` in window `screen`, with `during` sent
|
||||
/// at the last move before the release, released at `to`.
|
||||
fn drag_gripper_in(
|
||||
app: &mut crate::VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
screen: egui::Rect,
|
||||
from: egui::Pos2,
|
||||
to: egui::Pos2,
|
||||
during: Vec<egui::Event>,
|
||||
) {
|
||||
// A bar that has just started floating takes a frame to show.
|
||||
control_bar_frame_in(app, ctx, screen, vec![]);
|
||||
control_bar_frame_in(app, ctx, screen, vec![]);
|
||||
control_bar_frame_in(app, ctx, screen, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
|
||||
for k in 1..=4 {
|
||||
control_bar_frame_in(app, ctx, screen, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
|
||||
}
|
||||
control_bar_frame_in(app, ctx, screen, during);
|
||||
control_bar_frame_in(app, ctx, screen, vec![press_at(to, false)]);
|
||||
control_bar_frame_in(app, ctx, screen, vec![]);
|
||||
}
|
||||
|
||||
/// [`drag_gripper_in`] in [`DOCK_SCREEN`] with nothing sent during the drag.
|
||||
fn drag_gripper(app: &mut crate::VectorcraftApp, ctx: &egui::Context, from: egui::Pos2, to: egui::Pos2) {
|
||||
drag_gripper_in(app, ctx, DOCK_SCREEN, from, to, vec![]);
|
||||
}
|
||||
|
||||
/// Dragging the Control bar's gripper and releasing it within 24 points of the top or the bottom of
|
||||
/// the window docks the bar at that edge, the edge it is at included.
|
||||
#[test]
|
||||
fn dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
// The gripper is the bar's first item, inside its 10-point margin, centered in the 34-point bar.
|
||||
let grip = |bottom: bool| egui::pos2(13.0, if bottom { DOCK_SCREEN.bottom() - 17.0 } else { 17.0 });
|
||||
let drag = |app: &mut crate::VectorcraftApp, from: egui::Pos2, to: egui::Pos2| {
|
||||
control_bar_frame(app, &ctx, vec![]);
|
||||
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
|
||||
for k in 1..=4 {
|
||||
control_bar_frame(app, &ctx, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
|
||||
}
|
||||
control_bar_frame(app, &ctx, vec![press_at(to, false)]);
|
||||
};
|
||||
drag(&mut app, grip(false), egui::pos2(400.0, 200.0));
|
||||
assert!(app.ui.control_bar_bottom, "released nearer the bottom: docked at the bottom");
|
||||
drag(&mut app, grip(true), egui::pos2(400.0, 160.0));
|
||||
assert!(app.ui.control_bar_bottom, "released nearer the bottom again: still at the bottom");
|
||||
drag(&mut app, grip(true), egui::pos2(400.0, 120.0));
|
||||
assert!(!app.ui.control_bar_bottom, "released nearer the top: docked at the top");
|
||||
drag(&mut app, grip(false), egui::pos2(400.0, 140.0));
|
||||
assert!(!app.ui.control_bar_bottom, "released nearer the top again: still at the top");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(false), egui::pos2(400.0, 290.0));
|
||||
assert!(app.ui.control_bar_bottom && !app.ui.control_bar_floating, "released at the bottom: docked at the bottom");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(true), egui::pos2(400.0, 280.0));
|
||||
assert!(app.ui.control_bar_bottom && !app.ui.control_bar_floating, "released at the bottom again: still at the bottom");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(true), egui::pos2(400.0, 10.0));
|
||||
assert!(!app.ui.control_bar_bottom && !app.ui.control_bar_floating, "released at the top: docked at the top");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(false), egui::pos2(400.0, 20.0));
|
||||
assert!(!app.ui.control_bar_bottom && !app.ui.control_bar_floating, "released at the top again: still at the top");
|
||||
}
|
||||
|
||||
/// `window.control.dock {edge}` docks the Control bar to the top or the bottom of the window, and
|
||||
/// the panel menu checks the edge in use; another edge, or none, is an error and changes nothing.
|
||||
/// Dragging the docked Control bar's gripper away from both edges and releasing it there floats
|
||||
/// the bar under the pointer, held where the gripper was grabbed: 800 points wide, over the window,
|
||||
/// with its gripper and panel menu. The bar stays docked until the release.
|
||||
#[test]
|
||||
fn the_control_bar_dock_command_takes_top_or_bottom() {
|
||||
fn dragging_the_control_bar_gripper_away_from_both_edges_floats_it() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let from = docked_grip(false);
|
||||
control_bar_frame(&mut app, &ctx, vec![]);
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
|
||||
let to = egui::pos2(400.0, 150.0);
|
||||
for k in 1..=4 {
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
|
||||
}
|
||||
assert!(!app.ui.control_bar_floating && super::control_bar_moving(&ctx), "still docked while the drag goes on");
|
||||
control_bar_frame(&mut app, &ctx, vec![press_at(to, false)]);
|
||||
assert!(app.ui.control_bar_floating && !super::control_bar_moving(&ctx), "released away from both edges: floating");
|
||||
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "float"})), Some(true));
|
||||
assert_eq!(app.ui.control_bar_float_pos, Some([to.x - from.x, to.y - from.y]), "held where the gripper was grabbed");
|
||||
control_bar_frame(&mut app, &ctx, vec![]);
|
||||
let menu = node_center(&control_bar_frame(&mut app, &ctx, vec![]), "Panel menu").unwrap_or_else(|| panic!("the floating bar has a panel menu"));
|
||||
let left = to.x - from.x;
|
||||
assert!((menu.y - to.y).abs() < 1.0 && (menu.x - (left + 800.0 - 19.0)).abs() < 1.0, "the panel menu ends the 800-point bar: {menu:?}");
|
||||
}
|
||||
|
||||
/// A floating Control bar's gripper moves it, and docks it again at the top or the bottom of the
|
||||
/// window; from either edge it floats again.
|
||||
#[test]
|
||||
fn floating_control_bar_moves_and_redocks_from_each_edge() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
let at = super::CONTROL_BAR_FLOAT_AT;
|
||||
drag_gripper(&mut app, &ctx, floating_grip(at), egui::pos2(300.0, 100.0));
|
||||
assert!(app.ui.control_bar_floating, "moved, it still floats");
|
||||
let moved = [300.0 - 14.0, 100.0 - 17.0];
|
||||
assert_eq!(app.ui.control_bar_float_pos, Some(moved));
|
||||
drag_gripper(&mut app, &ctx, floating_grip(moved), egui::pos2(500.0, 290.0));
|
||||
assert!(!app.ui.control_bar_floating && app.ui.control_bar_bottom, "released at the bottom: docked there");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(true), egui::pos2(500.0, 160.0));
|
||||
assert!(app.ui.control_bar_floating, "from the bottom it floats again");
|
||||
let [x, y] = app.ui.control_bar_float_pos.unwrap_or_default();
|
||||
drag_gripper(&mut app, &ctx, floating_grip([x, y]), egui::pos2(500.0, 8.0));
|
||||
assert!(!app.ui.control_bar_floating && !app.ui.control_bar_bottom, "released at the top: docked there");
|
||||
drag_gripper(&mut app, &ctx, docked_grip(false), egui::pos2(700.0, 120.0));
|
||||
assert!(app.ui.control_bar_floating, "from the top it floats again");
|
||||
}
|
||||
|
||||
/// A floating Control bar's panel menu keeps Dock to Top and Dock to Bottom, neither checked, and
|
||||
/// each docks the bar at its edge.
|
||||
#[test]
|
||||
fn floating_control_bar_menu_docks_to_either_edge() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let frame = |app: &mut crate::VectorcraftApp, events| control_bar_frame_in(app, &ctx, MENU_SCREEN, events);
|
||||
for (item, bottom) in [("Dock to Bottom", true), ("Dock to Top", false)] {
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
frame(&mut app, vec![]);
|
||||
let menu = node_center(&frame(&mut app, vec![]), "Panel menu").unwrap_or_else(|| panic!("the floating bar has a panel menu"));
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(menu), press_at(menu, true)]);
|
||||
frame(&mut app, vec![press_at(menu, false)]);
|
||||
let nodes = frame(&mut app, vec![]);
|
||||
let checked = nodes.iter().filter_map(|n| n.label()).filter(|l| l.starts_with('✓') && l.contains("Dock")).count();
|
||||
assert_eq!(checked, 0, "neither edge is checked while the bar floats");
|
||||
let at = node_center(&nodes, item).unwrap_or_else(|| panic!("the menu lists {item}"));
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(at), press_at(at, true)]);
|
||||
frame(&mut app, vec![press_at(at, false)]);
|
||||
assert!(!app.ui.control_bar_floating && app.ui.control_bar_bottom == bottom, "{item} docks the bar");
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape during a gripper drag cancels it: a docked bar stays at its edge and a floating one where
|
||||
/// it floats.
|
||||
#[test]
|
||||
fn control_bar_drag_escape_restores_placement() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let escape =
|
||||
|| vec![egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }];
|
||||
drag_gripper_in(&mut app, &ctx, DOCK_SCREEN, docked_grip(false), egui::pos2(400.0, 150.0), escape());
|
||||
assert!(!app.ui.control_bar_floating && !app.ui.control_bar_bottom, "the docked bar stays at the top");
|
||||
assert!(!super::control_bar_moving(&ctx), "the drag is over");
|
||||
drag_gripper_in(&mut app, &ctx, DOCK_SCREEN, docked_grip(false), egui::pos2(400.0, 290.0), escape());
|
||||
assert!(!app.ui.control_bar_bottom, "a canceled drag over the bottom does not dock it there");
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
let at = super::CONTROL_BAR_FLOAT_AT;
|
||||
drag_gripper_in(&mut app, &ctx, DOCK_SCREEN, floating_grip(at), egui::pos2(500.0, 200.0), escape());
|
||||
assert!(app.ui.control_bar_floating, "the floating bar still floats");
|
||||
assert_eq!(app.ui.control_bar_float_pos, None, "where it floated");
|
||||
// A bar hidden in the middle of a drag leaves no drag to take a later Escape once the button
|
||||
// is released.
|
||||
let (from, to) = (floating_grip(at), egui::pos2(500.0, 200.0));
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
|
||||
for k in 1..=4 {
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
|
||||
}
|
||||
assert!(super::control_bar_moving(&ctx), "the drag goes on");
|
||||
for events in [vec![press_at(to, false)], vec![]] {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(DOCK_SCREEN), ..Default::default() }, |_| {});
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
assert!(!super::control_bar_moving(&ctx), "released with the bar hidden, the drag is over");
|
||||
}
|
||||
|
||||
/// An Escape in the same frame as the gripper's release is the drag's: the frame's shortcuts, which
|
||||
/// run before the bar is drawn, still see the drag, and the bar stays where it was.
|
||||
#[test]
|
||||
fn control_bar_drag_escape_on_release_frame_is_the_drags() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (from, to) = (docked_grip(false), egui::pos2(400.0, 150.0));
|
||||
control_bar_frame(&mut app, &ctx, vec![]);
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from), press_at(from, true)]);
|
||||
for k in 1..=4 {
|
||||
control_bar_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from.lerp(to, k as f32 / 4.0))]);
|
||||
}
|
||||
let escape = egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
let mut moving = None;
|
||||
let input = egui::RawInput { events: vec![escape, press_at(to, false)], screen_rect: Some(DOCK_SCREEN), ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| {
|
||||
moving = Some(super::control_bar_moving(ui.ctx()));
|
||||
super::control_bar(&mut app, ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert_eq!(moving, Some(true), "the release frame's shortcuts see the drag");
|
||||
assert!(!app.ui.control_bar_floating && !app.ui.control_bar_bottom, "the canceled drag leaves the bar at the top");
|
||||
control_bar_frame(&mut app, &ctx, vec![]);
|
||||
assert!(!super::control_bar_moving(&ctx), "the drag is over");
|
||||
}
|
||||
|
||||
/// A gripper drag released outside the window leaves the bar where it was.
|
||||
#[test]
|
||||
fn control_bar_drag_release_outside_window_keeps_previous_placement() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
drag_gripper(&mut app, &ctx, docked_grip(false), egui::pos2(400.0, DOCK_SCREEN.bottom() + 120.0));
|
||||
assert!(!app.ui.control_bar_floating && !app.ui.control_bar_bottom, "the bar stays at the top");
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
drag_gripper(&mut app, &ctx, floating_grip(super::CONTROL_BAR_FLOAT_AT), egui::pos2(-50.0, 150.0));
|
||||
assert!(app.ui.control_bar_floating && app.ui.control_bar_float_pos.is_none(), "the floating bar stays where it was");
|
||||
}
|
||||
|
||||
/// In a window too short for a floating bar between the two docking zones, a release docks the bar
|
||||
/// at the nearer edge; a floating bar saved on a bigger window is drawn inside a smaller one, its
|
||||
/// panel menu in reach, without docking it.
|
||||
#[test]
|
||||
fn floating_control_bar_stays_reachable_in_tiny_and_resized_windows() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let tiny = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 70.0));
|
||||
drag_gripper_in(&mut app, &ctx, tiny, egui::pos2(13.0, 17.0), egui::pos2(300.0, 40.0), vec![]);
|
||||
assert!(!app.ui.control_bar_floating && app.ui.control_bar_bottom, "released below the middle: docked at the bottom");
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
app.ui.control_bar_float_pos = Some([1000.0, 250.0]);
|
||||
let small = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 120.0));
|
||||
control_bar_frame_in(&mut app, &ctx, small, vec![]);
|
||||
let menu = node_center(&control_bar_frame_in(&mut app, &ctx, small, vec![]), "Panel menu")
|
||||
.unwrap_or_else(|| panic!("the floating bar has a panel menu"));
|
||||
assert!(small.shrink(1.0).contains(menu), "the panel menu is inside the smaller window: {menu:?}");
|
||||
assert!(app.ui.control_bar_floating, "a smaller window does not dock the bar");
|
||||
// The gripper too: a drag from where the bar is drawn moves it.
|
||||
let grip = egui::pos2(small.right() - 500.0 + 14.0, small.bottom() - 34.0 + 17.0);
|
||||
drag_gripper_in(&mut app, &ctx, small, grip, egui::pos2(200.0, 60.0), vec![]);
|
||||
assert!(app.ui.control_bar_floating, "moved inside the smaller window");
|
||||
assert_eq!(app.ui.control_bar_float_pos, Some([0.0, 43.0]), "kept inside the window, its gripper under the pointer");
|
||||
}
|
||||
|
||||
/// `window.control.dock {edge}` docks the Control bar to the top or the bottom of the window, or
|
||||
/// floats it, and the panel menu checks the edge in use; another edge, or none, is an error and
|
||||
/// changes nothing. A floating bar floats at the position it last floated at, docked in between
|
||||
/// or hidden by Tab and shown again.
|
||||
#[test]
|
||||
fn the_control_bar_dock_command_takes_top_bottom_or_float() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
assert_eq!(app.run("window.control.dock", json!({"edge": "bottom"})).unwrap(), json!({"edge": "bottom"}));
|
||||
assert!(app.ui.control_bar_bottom);
|
||||
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "bottom"})), Some(true));
|
||||
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "top"})), Some(false));
|
||||
for p in [json!({"edge": "left"}), json!({"edge": "Bottom"}), json!({"edge": 1}), json!({})] {
|
||||
for p in [json!({"edge": "left"}), json!({"edge": "Bottom"}), json!({"edge": "Float"}), json!({"edge": 1}), json!({})] {
|
||||
let err = app.run("window.control.dock", p.clone()).unwrap_err();
|
||||
assert_eq!(err, "`edge` must be one of top, bottom", "{p}");
|
||||
assert!(app.ui.control_bar_bottom, "{p} leaves the bar at the bottom");
|
||||
assert_eq!(err, "`edge` must be one of top, bottom, float", "{p}");
|
||||
assert!(app.ui.control_bar_bottom && !app.ui.control_bar_floating, "{p} leaves the bar at the bottom");
|
||||
}
|
||||
assert_eq!(app.run("window.control.dock", json!({"edge": "float"})).unwrap(), json!({"edge": "float"}));
|
||||
assert!(app.ui.control_bar_floating && app.ui.control_bar_bottom, "floating, it keeps the edge it docked to last");
|
||||
for edge in ["top", "bottom"] {
|
||||
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({ "edge": edge })), Some(false), "{edge}");
|
||||
}
|
||||
assert_eq!(crate::menus::checked(&app, "window.control.dock", &json!({"edge": "float"})), Some(true));
|
||||
let err = app.run("window.control.dock", json!({"edge": "side"})).unwrap_err();
|
||||
assert_eq!(err, "`edge` must be one of top, bottom, float");
|
||||
assert!(app.ui.control_bar_floating, "an error leaves the bar floating");
|
||||
app.ui.control_bar_float_pos = Some([240.0, 160.0]);
|
||||
assert_eq!(app.run("window.control.dock", json!({"edge": "top"})).unwrap(), json!({"edge": "top"}));
|
||||
assert!(!app.ui.control_bar_bottom);
|
||||
assert!(!app.ui.control_bar_bottom && !app.ui.control_bar_floating);
|
||||
app.run("window.control.dock", json!({"edge": "float"})).unwrap();
|
||||
assert_eq!(app.ui.control_bar_float_pos, Some([240.0, 160.0]), "it floats where it last floated");
|
||||
// Tab hides the floating bar, and Tab again shows it floating where it was.
|
||||
app.ui.control_bar = true;
|
||||
app.run("window.dock", json!({})).unwrap();
|
||||
assert!(!app.ui.control_bar && app.ui.control_bar_floating, "Tab hides the bar");
|
||||
app.run("window.dock", json!({})).unwrap();
|
||||
assert!(app.ui.control_bar && app.ui.control_bar_floating, "Tab shows it floating again");
|
||||
assert_eq!(app.ui.control_bar_float_pos, Some([240.0, 160.0]));
|
||||
}
|
||||
|
||||
/// The labels of the Control bar panel menu's groups of options, in the menu's order.
|
||||
@@ -2667,6 +2897,10 @@ fn menus_stop_above_a_control_bar_docked_at_the_bottom() {
|
||||
assert_eq!(f, screen.bottom(), "docked at the top: menus stop at the window's bottom");
|
||||
let f = floor(&mut app, true, true, 0);
|
||||
assert!(f < bar_top - 1.0, "the status and hint bars show above the Control bar: menus stop above them, {f}");
|
||||
// Floating after docking at the bottom, the bar is no bottom bar.
|
||||
app.ui.control_bar_floating = true;
|
||||
let f = floor(&mut app, false, true, 0);
|
||||
assert_eq!(f, screen.bottom(), "floating: menus stop at the window's bottom");
|
||||
}
|
||||
|
||||
/// With the Artboard tool in use, the Control bar shows the Align buttons after the artboard
|
||||
|
||||
@@ -516,13 +516,17 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
}
|
||||
"window.control" => flag(&mut app.ui.control_bar),
|
||||
"window.helpBar" => flag(&mut app.ui.help_bar),
|
||||
// The Control bar's panel menu and gripper: the edge of the window the bar docks to.
|
||||
// The Control bar's panel menu and gripper: the edge of the window the bar docks to, or
|
||||
// floating over the window at the position it last floated at.
|
||||
"window.control.dock" => {
|
||||
let key = "edge";
|
||||
let edges = crate::chrome::CONTROL_BAR_EDGES;
|
||||
match edges.iter().position(|e| s(key).as_deref() == Some(*e)) {
|
||||
Some(i) => {
|
||||
app.ui.control_bar_bottom = i == 1;
|
||||
app.ui.control_bar_floating = i == 2;
|
||||
if i < 2 {
|
||||
app.ui.control_bar_bottom = i == 1;
|
||||
}
|
||||
Ok(json!({ key: edges[i] }))
|
||||
}
|
||||
None => Err(format!("`{key}` must be one of {}", edges.join(", "))),
|
||||
|
||||
@@ -63,9 +63,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"view.rotate" => app.view().zip(p.get("angle").and_then(Value::as_f64)).is_some_and(|(view, a)| (view.rotation - a).abs() < 1e-9),
|
||||
"window.control" => app.ui.control_bar,
|
||||
"window.helpBar" => app.ui.help_bar,
|
||||
"window.control.dock" => {
|
||||
p.get("edge").and_then(Value::as_str) == Some(crate::chrome::CONTROL_BAR_EDGES[usize::from(app.ui.control_bar_bottom)])
|
||||
}
|
||||
"window.control.dock" => p.get("edge").and_then(Value::as_str) == Some(crate::chrome::control_bar_placement(&app.ui)),
|
||||
"window.control.options" => p
|
||||
.get("group")
|
||||
.and_then(Value::as_str)
|
||||
|
||||
@@ -470,7 +470,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.control.dock",
|
||||
"Dock Control Bar",
|
||||
"",
|
||||
"{edge: \"top\"|\"bottom\"} dock the Control bar to the top of the window, under the menu bar, or to its bottom, as the bar's panel menu (Dock to Top, Dock to Bottom) and a drag of its gripper do; kept with the workspace; another edge is an error and changes nothing; returns {edge}",
|
||||
"{edge: \"top\"|\"bottom\"|\"float\"} dock the Control bar to the top of the window, under the menu bar, or to its bottom, as the bar's panel menu (Dock to Top, Dock to Bottom) and a drag of its gripper do, or float it over the window at the position it last floated at (first: its top-left corner at 60, 80 in window points), as a gripper drag released away from both edges does; kept with the workspace; another edge is an error and changes nothing; returns {edge}",
|
||||
),
|
||||
(
|
||||
"window.control.options",
|
||||
|
||||
@@ -487,20 +487,22 @@ fn hold_back_key(app: &VectorcraftApp, ctx: &egui::Context, key: Key) {
|
||||
ctx.input_mut(|i| i.events.retain(|e| !matches!(e, egui::Event::Key { key: k, pressed: true, .. } if *k == key)));
|
||||
}
|
||||
|
||||
/// The key of the mark that the Escape held down ended a ruler-origin drag or a floating Tools move.
|
||||
/// The key of the mark that the Escape held down ended a ruler-origin drag, a floating Tools move
|
||||
/// or a Control bar gripper drag.
|
||||
fn escape_ended_gesture_id() -> egui::Id {
|
||||
egui::Id::unique("escape-ended-gesture")
|
||||
}
|
||||
|
||||
/// Does this frame's Escape end a ruler-origin drag or a floating Tools move? The repeats of an
|
||||
/// Escape that ended one do too, up to the frame that releases Escape or presses it again.
|
||||
/// Does this frame's Escape end a ruler-origin drag, a floating Tools move or a Control bar gripper
|
||||
/// drag? The repeats of an Escape that ended one do too, up to the frame that releases Escape or
|
||||
/// presses it again.
|
||||
fn escape_ended_gesture(ctx: &egui::Context) -> bool {
|
||||
let (down, pressed, repeated) = ctx.input(|i| {
|
||||
let escape =
|
||||
|repeat: bool| i.events.iter().any(|e| matches!(e, egui::Event::Key { key: Key::Escape, pressed: true, repeat: r, .. } if *r == repeat));
|
||||
(i.key_down(Key::Escape), escape(false), escape(true))
|
||||
});
|
||||
if crate::canvas::ruler_origin_dragging(ctx) || crate::tools_float::escape_cancelled_move(ctx) {
|
||||
if crate::canvas::ruler_origin_dragging(ctx) || crate::tools_float::escape_cancelled_move(ctx) || crate::chrome::control_bar_moving(ctx) {
|
||||
if down {
|
||||
ctx.data_mut(|d| d.insert_temp(escape_ended_gesture_id(), true));
|
||||
}
|
||||
|
||||
@@ -428,6 +428,15 @@ pub struct UiState {
|
||||
/// The Control bar is docked to the bottom of the window instead of the top
|
||||
/// (`window.control.dock`).
|
||||
pub control_bar_bottom: bool,
|
||||
/// The Control bar floats over the window at `control_bar_float_pos` instead of docking to an
|
||||
/// edge (`window.control.dock {edge: "float"}`); `control_bar_bottom` keeps the edge it last
|
||||
/// docked to.
|
||||
#[serde(default)]
|
||||
pub control_bar_floating: bool,
|
||||
/// The top-left corner of the floating Control bar in window points, kept while the bar is
|
||||
/// docked; none: `chrome::CONTROL_BAR_FLOAT_AT`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub control_bar_float_pos: Option<[f32; 2]>,
|
||||
/// The groups of the Control bar's options (`chrome::CONTROL_BAR_GROUPS`) that its panel menu
|
||||
/// has off (`window.control.options`); none: every group shows.
|
||||
#[serde(deserialize_with = "crate::chrome::read_hidden_groups")]
|
||||
@@ -1003,7 +1012,8 @@ pub struct FloatingPanels {
|
||||
pub docked: Option<u32>,
|
||||
}
|
||||
|
||||
/// Drop a Tools panel position that isn't a number (a hand-edited preferences file or workspace).
|
||||
/// Drop a floating position of the Tools panel or the Control bar that isn't a number (a
|
||||
/// hand-edited preferences file or workspace).
|
||||
pub fn sanitize_toolbar_pos(toolbar_pos: &mut Option<[f32; 2]>) {
|
||||
if toolbar_pos.is_some_and(|p| !p.iter().all(|v| v.is_finite())) {
|
||||
*toolbar_pos = None;
|
||||
@@ -1027,6 +1037,7 @@ impl UiState {
|
||||
}
|
||||
FloatingFlyout::sanitize(&mut self.floating_flyouts);
|
||||
sanitize_toolbar_pos(&mut self.toolbar_pos);
|
||||
sanitize_toolbar_pos(&mut self.control_bar_float_pos);
|
||||
crate::edit_toolbar::sanitize(&mut self.toolbar_edits);
|
||||
// `read_preferences` converted the file's older layouts already; this covers a state read
|
||||
// any other way. What it reports lost was dropped there.
|
||||
@@ -1076,6 +1087,8 @@ impl Default for UiState {
|
||||
learn_locate: None,
|
||||
control_bar: false,
|
||||
control_bar_bottom: false,
|
||||
control_bar_floating: false,
|
||||
control_bar_float_pos: None,
|
||||
control_bar_hidden_groups: Default::default(),
|
||||
toolbar: true,
|
||||
toolbar_double: false,
|
||||
|
||||
@@ -24,6 +24,13 @@ pub struct Workspace {
|
||||
/// The Control bar is docked to the bottom of the window (built-in workspaces, and workspaces
|
||||
/// saved without this field: the top).
|
||||
pub control_bar_bottom: bool,
|
||||
/// The Control bar floats at `control_bar_float_pos` (built-in workspaces, and workspaces saved
|
||||
/// without this field: docked at the edge `control_bar_bottom` names).
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub control_bar_floating: bool,
|
||||
/// Where the Control bar floats, or last floated (built-in workspaces: none, the first position).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub control_bar_float_pos: Option<[f32; 2]>,
|
||||
/// The groups of the Control bar's options its panel menu has off (built-in workspaces, and
|
||||
/// workspaces saved without this field: none).
|
||||
#[serde(skip_serializing_if = "std::collections::BTreeSet::is_empty", deserialize_with = "crate::chrome::read_hidden_groups")]
|
||||
@@ -190,6 +197,8 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
name: name.into(),
|
||||
control_bar: ui.control_bar,
|
||||
control_bar_bottom: ui.control_bar_bottom,
|
||||
control_bar_floating: ui.control_bar_floating,
|
||||
control_bar_float_pos: ui.control_bar_float_pos,
|
||||
control_bar_hidden_groups: ui.control_bar_hidden_groups.clone(),
|
||||
rulers: ui.view.rulers,
|
||||
toolbar: ui.toolbar,
|
||||
@@ -221,6 +230,13 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.control_bar = w.control_bar;
|
||||
ui.control_bar_bottom = w.control_bar_bottom;
|
||||
ui.control_bar_floating = w.control_bar_floating;
|
||||
// A floating workspace floats the bar where it was saved (none: the first position); a docked
|
||||
// one without a floating position keeps the one the bar last floated at.
|
||||
if w.control_bar_floating || w.control_bar_float_pos.is_some() {
|
||||
ui.control_bar_float_pos = w.control_bar_float_pos;
|
||||
crate::state::sanitize_toolbar_pos(&mut ui.control_bar_float_pos);
|
||||
}
|
||||
ui.control_bar_hidden_groups = w.control_bar_hidden_groups.clone();
|
||||
ui.view.rulers = w.rulers;
|
||||
ui.toolbar = w.toolbar;
|
||||
@@ -813,6 +829,43 @@ mod tests {
|
||||
assert!(ui.control_bar && !ui.control_bar_bottom, "an older workspace docks the bar at the top");
|
||||
}
|
||||
|
||||
/// A workspace keeps a floating Control bar and where it floats, through the preferences' JSON
|
||||
/// too; switching to a docked workspace docks it and keeps that position for the next float.
|
||||
/// Reset floats the bar where the workspace was saved floating. A workspace or preferences saved
|
||||
/// before floating keep their top or bottom edge, and a position that is not a number is
|
||||
/// dropped.
|
||||
#[test]
|
||||
fn saved_workspaces_restore_floating_control_bar_position() {
|
||||
let mut ui = UiState { control_bar: true, control_bar_floating: true, control_bar_float_pos: Some([220.0, 140.0]), ..Default::default() };
|
||||
save_as(&mut ui, "Floating").unwrap();
|
||||
switch(&mut ui, ESSENTIALS).unwrap();
|
||||
assert!(!ui.control_bar_floating && !ui.control_bar_bottom, "Essentials docks the bar at the top");
|
||||
assert_eq!(ui.control_bar_float_pos, Some([220.0, 140.0]), "the last floating position is kept while docked");
|
||||
switch(&mut ui, "Floating").unwrap();
|
||||
assert!(ui.control_bar_floating, "the saved workspace floats the bar again");
|
||||
assert_eq!(ui.control_bar_float_pos, Some([220.0, 140.0]));
|
||||
let back: UiState = serde_json::from_value(serde_json::to_value(&ui).unwrap()).unwrap();
|
||||
assert!(back.control_bar_floating && back.custom_workspaces[0].control_bar_floating, "both survive the preferences' JSON");
|
||||
assert_eq!(back.custom_workspaces[0].control_bar_float_pos, Some([220.0, 140.0]));
|
||||
// Saved before the bar floated: the edge it was docked to, top or bottom.
|
||||
for bottom in [false, true] {
|
||||
let old: Workspace = serde_json::from_value(json!({"name": "Old", "controlBar": true, "controlBarBottom": bottom})).unwrap();
|
||||
apply(&mut ui, &old);
|
||||
assert!(!ui.control_bar_floating && ui.control_bar_bottom == bottom, "an older workspace docks the bar at its edge, bottom: {bottom}");
|
||||
let old: UiState = serde_json::from_value(json!({"control_bar": true, "control_bar_bottom": bottom})).unwrap();
|
||||
assert!(!old.control_bar_floating && old.control_bar_bottom == bottom, "older preferences dock it at their edge, bottom: {bottom}");
|
||||
}
|
||||
// Saved floating at the first position: Reset after a move floats it there again.
|
||||
let mut ui = UiState { control_bar: true, control_bar_floating: true, ..Default::default() };
|
||||
save_as(&mut ui, "First").unwrap();
|
||||
ui.control_bar_float_pos = Some([300.0, 90.0]);
|
||||
reset(&mut ui);
|
||||
assert!(ui.control_bar_floating, "Reset floats the bar");
|
||||
assert_eq!(ui.control_bar_float_pos, None, "at the first position, as the workspace was saved");
|
||||
let odd: UiState = serde_json::from_value(json!({"control_bar_floating": true, "control_bar_float_pos": [1e39, 5.0]})).unwrap();
|
||||
assert_eq!(odd.sanitized().control_bar_float_pos, None, "a position out of range is dropped");
|
||||
}
|
||||
|
||||
/// A workspace keeps the groups of Control bar options its panel menu has off: switching
|
||||
/// brings each workspace's back, Reset turns a built-in workspace's on again, and they survive
|
||||
/// the preferences' JSON. A workspace or preferences saved without them, or with groups the bar
|
||||
|
||||
+12
-5
@@ -5854,11 +5854,18 @@ through the views in tab order and add `view` to their reply for a document with
|
||||
reports `windows` (every window as `[document index, view number]`, in tab order), `currentWindow` and each pane's
|
||||
`view`. Views are not saved.
|
||||
`window.control.dock {edge}` (desktop app) docks the Control bar to the top of the window, under the menu bar
|
||||
(`edge: "top"`, the default), or to its bottom (`"bottom"`), and returns `{edge}`. The bar's panel menu runs it with
|
||||
Dock to Top and Dock to Bottom, the edge in use checked, and so does a drag of the bar's gripper, which docks the bar at
|
||||
the edge the pointer is released nearer and shows a band along that edge while it moves. The edge is kept with the
|
||||
workspace, and a workspace saved without it docks the bar at the top. Another `edge`, or none, fails with "`edge` must
|
||||
be one of top, bottom" and leaves the bar where it is.
|
||||
(`edge: "top"`, the default), or to its bottom (`"bottom"`), or floats it over the window (`"float"`), and returns
|
||||
`{edge}`. A floating bar floats where it last floated; the first time, its top-left corner is at 60, 80 in window
|
||||
points. It is 800 points wide, or as wide as the window when that is narrower, takes no room from the canvas, and is
|
||||
kept inside the window below the menu bar. The bar's panel menu runs the command with Dock to Top and Dock to Bottom,
|
||||
the edge in use checked and neither while the bar floats. So does a drag of the bar's gripper: released within 24
|
||||
points of the top (under the menu bar) or the bottom of the window it docks the bar at that edge, showing a band along
|
||||
that edge while it moves, and released farther from both edges it floats the bar there, showing the bar's outline while
|
||||
it moves. In a window too short for a floating bar between those two zones, the release docks the bar at the nearer
|
||||
edge. Escape cancels the drag, and a release outside the window leaves the bar where it was. The edge, whether the bar
|
||||
floats and where it floats are kept with the workspace, and a workspace saved without them docks the bar at the edge it
|
||||
names, or at the top. Another `edge`, or none, fails with "`edge` must be one of top, bottom, float" and leaves the bar
|
||||
where it is.
|
||||
`window.control.options {group, visible?}` (desktop app) turns a group of the Control bar's options on (`visible:
|
||||
true`), off (`false`) or toggles it (omitted), and returns `{group, visible}`. The groups are `snapping` (the Snapping
|
||||
menu), `anchors` (Convert and Anchors), `toolOptions` (the active tool's options), `objectOptions` (Isolate Selected
|
||||
|
||||
@@ -621,7 +621,7 @@ since 2026-10-01.
|
||||
- **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`.
|
||||
- **Global Edit's enabled check on large paths:** the check that decides whether Select › Start Global Edit can run on the selected path, compound path or symbol instance (which the Control bar makes every frame) measures the box of its anchors and handles instead of building and normalizing a copy of every anchor, with the same answer. On a 200,000-anchor path the check takes 0.7 ms instead of 2.2 to 3.0 ms. Regression tests: `support_*` in `crates/engine/src/cmd/globaledit.rs`.
|
||||
- **Control bar's inline X, Y, W and H fields fit the bar:** the Control bar shows the inline X, Y, W and H fields only when all four, their labels as drawn in the current language and the W/H link fit inside the bar's margin; otherwise it shows the Transform link alone. This is VectorCraft's own layout rule. Regression test: `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`.
|
||||
- **Control bar docks to the top or the bottom:** the Control bar's panel menu, at the bar's right end, has Dock to Top and Dock to Bottom, the edge in use checked, and dragging the bar's gripper docks it at the top or the bottom edge of the window, whichever the pointer is released nearer, with a band along that edge while it moves (`window.control.dock {edge}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Keeping the edge with the workspace, with older workspaces docking the bar at the top, is VectorCraft's own rule. Not done: dragging the gripper away from both edges to float the bar. Regression tests: `the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom`, `dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge`, `the_control_bar_dock_command_takes_top_or_bottom` and `menus_stop_above_a_control_bar_docked_at_the_bottom` in `crates/ui-egui/src/chrome/tests.rs`, `saved_workspaces_restore_the_control_bar_edge` in `crates/ui-egui/src/workspaces.rs`.
|
||||
- **Control bar docks to the top or the bottom, or floats:** the Control bar's panel menu, at the bar's right end, has Dock to Top and Dock to Bottom, the edge in use checked and neither while the bar floats. Dragging the bar's gripper and releasing it within 24 points of the top (under the menu bar) or the bottom of the window docks it at that edge, with a band along that edge while it moves; releasing it farther from both edges floats the bar there, with its outline under the pointer while it moves, and a floating bar's gripper moves it or docks it again (`window.control.dock {edge: "top"|"bottom"|"float"}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Escape cancels a gripper drag. The 24-point zones, the floating bar's 800-point width and first position, a release outside the window leaving the bar where it was, a window too short for a floating bar docking it at the nearer edge, the floating bar staying inside a window that shrinks, and keeping the edge and the floating position with the workspace, with older workspaces docking the bar at their edge, are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom`, `dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge`, `dragging_the_control_bar_gripper_away_from_both_edges_floats_it`, `floating_control_bar_moves_and_redocks_from_each_edge`, `floating_control_bar_menu_docks_to_either_edge`, `control_bar_drag_escape_restores_placement`, `control_bar_drag_release_outside_window_keeps_previous_placement`, `floating_control_bar_stays_reachable_in_tiny_and_resized_windows`, `the_control_bar_dock_command_takes_top_bottom_or_float` and `menus_stop_above_a_control_bar_docked_at_the_bottom` in `crates/ui-egui/src/chrome/tests.rs`, `saved_workspaces_restore_the_control_bar_edge` and `saved_workspaces_restore_floating_control_bar_position` in `crates/ui-egui/src/workspaces.rs`.
|
||||
- **Control bar panel menu turns groups of options on or off:** the Control bar's panel menu lists the bar's groups of options (Snapping, Anchor Points, Tool Options, Object Options, Image Options, Fill and Stroke, Opacity, Graphic Style, Character, Paragraph, Vertical Alignment, Align and Distribute, Transform, Artboard Options and Document Options) above Dock to Top and Dock to Bottom, each checked while it is on; a click turns that group off or on (`window.control.options {group, visible?}`), and a group that is off stays out of the bar even while its tool or object is selected, as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). The list of groups and what each holds, every group on to start, a group that is off leaving its room to the groups after it, and keeping the groups with the preferences and the workspace are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_turns_each_group_on_or_off`, `the_control_bar_keeps_its_label_and_menu_with_every_group_off`, `the_control_bar_panel_menu_scrolls_in_a_short_window`, `the_control_bar_options_command_turns_a_group_on_or_off`, `groups_that_are_off_leave_their_room_to_the_later_groups`, `the_artboard_tool_s_control_bar_follows_its_two_groups` and `the_anchor_points_group_holds_convert_and_anchors` in `crates/ui-egui/src/chrome/tests.rs`, `the_image_options_group_holds_the_image_and_trace_controls` in `crates/ui-egui/src/tests/place.rs`, `the_character_and_paragraph_groups_hold_the_type_controls` in `crates/ui-egui/src/panels/paragraph/tests.rs`, `saved_workspaces_restore_the_control_bar_groups` in `crates/ui-egui/src/workspaces.rs`.
|
||||
- **Pattern-only backquote drags:** With Selection move, bounding-box scale or rotate, or the Rotate, Reflect, Scale or Shear tool, backquote held at mouse release applies the drag matrix to selected pattern paint while object geometry stays in place. The key may be pressed after mouse press; if released before the mouse button, the drag commits as an ordinary transform. Objects without pattern paint stay still; stroke-only patterns and patterned objects in mixed selections take the same matrix. Regression tests: `backquote_move_uses_release_state_and_one_commit` and `backquote_box_scale_and_rotate_use_pattern_matrix_at_release` in `select/tests.rs`, `backquote_drag_previews_and_commits_pattern_only_for_each_transform_tool` in `xform/transform.rs`, `selection_backquote_drag_changes_pattern_fill_and_stroke_without_moving_mixed_art` and `selection_backquote_drag_changes_stroke_only_pattern` in `tests/pattern.rs`, `egui_backtick_drag_uses_key_state_at_release` in `canvas/tests.rs`, and `synthetic_pointer_backquote_keeps_solid_object_in_place` in `control/tests.rs`. Scores and hours unchanged.
|
||||
- **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged.
|
||||
|
||||
Reference in New Issue
Block a user