Contextual Task Bar: the Smooth tool shows its Fidelity and Tool Options

While the Smooth tool is in use with a path it can smooth selected, the
bar shows the tool's Fidelity field and Tool Options..., which opens the
Smooth tool's options. The field sets Fidelity through tool.setOption
with the same range, unit and steps as the field in the options dialog.
With nothing smoothable selected, the tool shows no bar. The bar leaves
out the selection tools' actions and the Fill, Stroke, Recolor Artwork
and Lock controls. On a canvas too narrow for the field, More Options
lists Fidelity, which opens the tool's options.

The Smooth tool's cursor and the bar share one check of the selection,
which the bar keeps for the document's revision.
This commit is contained in:
dotCooCoo
2026-11-15 03:56:40 -08:00
parent 8f339eeaa1
commit f459465407
6 changed files with 227 additions and 33 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ pub use curvature::{
CurvatureTool, EndCurve, curvature_close, curvature_extend, curvature_insert, curvature_move, curvature_remove, curvature_smooth,
};
pub use family::FamilyTool;
pub use gesture::GestureTool;
pub use gesture::{GestureTool, can_smooth};
/// Colour of tool feedback (Illustrator's selection blue).
pub const FEEDBACK: [u8; 3] = [0x4f, 0x9d, 0xff];
+9 -2
View File
@@ -791,9 +791,16 @@ fn redraw_targets(cx: &ToolContext, id: NodeId) -> Vec<NodeId> {
/// Whether the selection contains a path the Smooth command can act on: one with a subpath of three
/// or more anchors, or of two anchors joined by a curve.
fn has_selected_smooth_path(cx: &ToolContext) -> bool {
let editable = cx.doc.editable();
can_smooth(cx.doc, cx.selection)
}
/// Whether `selection` in `doc` holds an editable path, selected or inside a selected object, that
/// the Smooth tool can smooth: one with a subpath of three or more anchors, or of two anchors
/// joined by a curve.
pub fn can_smooth(doc: &vectorcraft_doc::Document, selection: &vectorcraft_doc::Selection) -> bool {
let editable = doc.editable();
let smoothable = |sp: &vectorcraft_geom::SubPath| sp.anchors.len() >= 3 || (0..sp.segment_count()).any(|i| !super::segment_is_straight(sp, i));
cx.selection.objects.iter().filter_map(|id| cx.doc.node(*id)).any(|n| {
selection.objects.iter().filter_map(|id| doc.node(*id)).any(|n| {
let mut found = false;
n.walk(&mut |child| {
if !found
+114 -24
View File
@@ -75,6 +75,27 @@ fn shape_tool_on_shape(app: &VectorcraftApp) -> bool {
})
}
/// Whether the Smooth tool is in use with a selected path it can smooth
/// ([`vectorcraft_tools::draw2::can_smooth`]). The answer is kept in `ctx` for the document's
/// revision, which moves with every change of the document or the selection, so an unchanged
/// selection is not walked again each frame.
fn smooth_tool_on_paths(app: &VectorcraftApp, ctx: &egui::Context) -> bool {
if app.session.tool_id() != "smooth" {
return false;
}
let Some(st) = app.session.active() else { return false };
let id = task_bar_id().with("smooth");
let key = (st.uid, st.revision);
if let Some((k, found)) = ctx.data(|d| d.get_temp::<((u64, u64), bool)>(id))
&& k == key
{
return found;
}
let found = vectorcraft_tools::draw2::can_smooth(&st.doc, &st.selection);
ctx.data_mut(|d| d.insert_temp(id, (key, found)));
found
}
/// Whether the bar shows its blank canvas actions, Draw (Pencil) and Place Image: a selection tool is
/// in use and the active document has no artwork, only layers and guides.
fn blank_canvas(app: &VectorcraftApp) -> bool {
@@ -130,7 +151,9 @@ pub(crate) fn task_bar_shows_type(app: &VectorcraftApp) -> bool {
/// While a basic shape tool (Rectangle, Rounded Rectangle, Ellipse, Polygon or Star) has one closed
/// path selected, as after it draws one, the bar offers Edit Path, Fill and Stroke, and Shape
/// Properties for a live rectangle, rounded rectangle, ellipse or polygon; with nothing selected, or
/// any other selection, those tools show no bar. Selected type also shows the font family, font
/// any other selection, those tools show no bar. While the Smooth tool has a path it can smooth
/// selected, the bar shows the tool's Fidelity field and Tool Options…; with no such path it shows
/// no bar. Selected type also shows the font family, font
/// style and Font Size controls before its Fill.
/// While a Type tool edits type, or has type selected, the bar shows only those controls and the
/// Fill. When one row would be wider than the canvas, the font controls take a row of their own,
@@ -150,7 +173,10 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
// A basic shape tool with its shape selected offers Edit Path, Fill, Stroke and the shape's
// properties, as the same help page describes.
let shaping = shape_tool_on_shape(app);
if !(on_artboards || typing || blank || shaping || selection_tool_with_selection(app)) {
// The Smooth tool with a path to smooth offers its option, Fidelity, as the same help page
// describes for the Smooth tool, and Tool Options… for its dialog.
let smoothing = smooth_tool_on_paths(app, ui.ctx());
if !(on_artboards || typing || blank || shaping || smoothing || selection_tool_with_selection(app)) {
return;
}
// Selected type has the font controls too, as the help page describes
@@ -197,15 +223,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
// Edit Path: the Direct Selection tool, to edit the path's anchors.
let edit_path = || (tl!("Edit Path"), "dc-direct", "tool.select", json!({"tool": "directSelection"}));
if on_artboards {
// As in the reference app, the Artboard tool offers to resize, duplicate and export the
// artboards (<https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580>),
// and to rearrange them (<https://creativepro.com/managing-artboards-in-illustrator/>).
items.push((tl!("Resize"), "move-diagonal-2", TASK_BAR_ARTBOARD_OPTIONS, none()));
items.push((tl!("Duplicate"), "copy", TASK_BAR_DUPLICATE_ARTBOARDS, none()));
if st.doc.artboards.len() > 1 {
items.push((tl!("Rearrange All"), "dc-rearrange", TASK_BAR_REARRANGE_ARTBOARDS, none()));
}
items.push((tl!("Export"), "share-2", "file.exportForScreens", none()));
artboard_items(&mut items, st.doc.artboards.len());
} else if blank {
// Draw (Pencil) switches to the Pencil tool; Place Image picks files for the Place dialog.
items.push((tl!("Draw (Pencil)"), "pencil", "tool.select", json!({"tool": "pencil"})));
@@ -213,6 +231,10 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
} else if shaping {
// A basic shape tool's one action; its Fill, Stroke and Shape Properties follow as chips.
items.push(edit_path());
} else if smoothing {
// The Fidelity field, then Tool Options… for the dialog that shows it with its slider.
items.push((tl!("Fidelity:"), "", TASK_BAR_FIDELITY, none()));
items.push((tl!("Tool Options…"), "settings", "tool.options", json!({"tool": "smooth"})));
} else if !typing {
// The selection tools' object actions. A Type tool's bar leaves them out, Create Outlines
// and the type conversions included.
@@ -319,8 +341,9 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
// The selected objects' Fill, Stroke, Recolor Artwork (when they have colours to recolour) and
// Lock (a basic shape's properties instead), left out with the Artboard tool and on a blank
// canvas, which have no selected objects. A Type tool's bar has the Fill alone. A basic shape
// tool's bar has the Fill, the Stroke and a live shape's Shape Properties.
let objects = !on_artboards && !blank;
// tool's bar has the Fill, the Stroke and a live shape's Shape Properties. The Smooth tool's bar
// has none of them.
let objects = !on_artboards && !blank && !smoothing;
let chips: Vec<Chip> = [
(objects, Chip::Fill),
(objects && !typing, Chip::Stroke),
@@ -330,7 +353,8 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
.into_iter()
.filter_map(|(shown, chip)| shown.then_some(chip))
.collect();
let widths: Vec<f32> = items.iter().map(|(label, _, cmd, _)| if icon_only(cmd) { BAR_MORE } else { action_width(ui, label) }).collect();
let widths: Vec<f32> = items.iter().map(|(label, _, cmd, _)| item_width(ui, label, cmd)).collect();
let fidelity = app.session.tool_options()["fidelity"].as_f64();
let font_w = fonts.then(|| {
let w = crate::panels::character::TASK_BAR_WIDTHS;
w.iter().sum::<f32>() + BAR_GAP * (w.len() - 1) as f32
@@ -408,16 +432,7 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
if *at != Some(row) {
continue;
}
// The convert buttons show their icon alone, named by their hover text, as
// the Control bar's do.
let clicked = if icon_only(cmd) {
widgets::icon_button(ui, icon, label, false, BAR_MORE).clicked()
} else {
action_button(ui, &t, label, icon)
};
if clicked {
run = Some((*cmd, params.clone()));
}
run = bar_item(ui, &t, (*label, *icon, *cmd, params), fidelity).or(run.take());
}
// Type's font family, font style and Font Size, which act as the Character
// panel's do: on the characters a Type tool selects, else on the selected type.
@@ -510,6 +525,9 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
// The convert buttons convert the direct-selected anchors, as the Control bar's buttons do.
Some((TASK_BAR_TO_CORNER, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "corner"})),
Some((TASK_BAR_TO_SMOOTH, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "smooth"})),
// Fidelity listed in More Options, on a canvas too narrow for its field: the Smooth tool's
// Tool Options, which show it.
Some((TASK_BAR_FIDELITY, _)) => crate::menus::invoke(app, "tool.options", json!({"tool": "smooth"})),
Some((c, p)) => crate::menus::invoke(app, c, p),
None => {}
}
@@ -658,6 +676,61 @@ fn action_width(ui: &Ui, label: &str) -> f32 {
ui.painter().layout_no_wrap(label.to_string(), egui::FontId::proportional(13.0), Color32::PLACEHOLDER).size().x + 38.0
}
/// The width of the Smooth tool's Fidelity on the Contextual Task Bar: its label, the gap and the
/// field.
fn fidelity_width(ui: &Ui) -> f32 {
widgets::text_width(ui, fidelity_label(tl!("Fidelity:"), &Tokens::get(ui.ctx()))) + BAR_GAP + crate::dialogs::freehand::FIDELITY_FIELD_W
}
/// The Fidelity field's label, `label`, as the bar draws it.
fn fidelity_label(label: &str, t: &Tokens) -> egui::RichText {
egui::RichText::new(label).size(13.0).color(t.text_strong)
}
/// The width of the Contextual Task Bar's item `label` that runs `cmd`.
fn item_width(ui: &Ui, label: &str, cmd: &str) -> f32 {
match cmd {
TASK_BAR_FIDELITY => fidelity_width(ui),
_ if icon_only(cmd) => BAR_MORE,
_ => action_width(ui, label),
}
}
/// One of the Contextual Task Bar's items, `label` with `icon`, which runs `cmd` with `params`, and
/// the Smooth tool's `fidelity`. Returns the command to run when it was used.
fn bar_item(
ui: &mut Ui,
t: &Tokens,
(label, icon, cmd, params): (&str, &str, &'static str, &serde_json::Value),
fidelity: Option<f64>,
) -> Option<(&'static str, serde_json::Value)> {
// The Smooth tool's Fidelity, labeled, sets the tool's option as it is typed or stepped.
if cmd == TASK_BAR_FIDELITY {
return fidelity_slot(ui, t, label, fidelity);
}
// The convert buttons show their icon alone, named by their hover text, as the Control bar's do.
let clicked = if icon_only(cmd) { widgets::icon_button(ui, icon, label, false, BAR_MORE).clicked() } else { action_button(ui, t, label, icon) };
clicked.then(|| (cmd, params.clone()))
}
/// The Smooth tool's Fidelity on the Contextual Task Bar, `label` and a field showing the tool's
/// `fidelity` (nothing without one). Returns the command that sets a new value typed or stepped to.
fn fidelity_slot(ui: &mut Ui, t: &Tokens, label: &str, fidelity: Option<f64>) -> Option<(&'static str, serde_json::Value)> {
let v = fidelity?;
let x = fidelity_field(ui, t, label, v).filter(|x| *x != v)?;
Some(("tool.setOption", json!({"tool": "smooth", "values": {"fidelity": x}})))
}
/// The Smooth tool's Fidelity on the Contextual Task Bar: `label` and a field showing `v`, centered
/// in a row of the bar. Returns the value typed or stepped to.
fn fidelity_field(ui: &mut Ui, t: &Tokens, label: &str, v: f64) -> Option<f64> {
ui.allocate_ui_with_layout(vec2(fidelity_width(ui), BAR_ROW), egui::Layout::left_to_right(egui::Align::Center), |ui| {
widgets::field_label(ui, fidelity_label(label, t));
crate::dialogs::freehand::fidelity_field(ui, "task-bar-fidelity", v, "smooth")
})
.inner
}
/// One of the Contextual Task Bar's action buttons: `icon` and `label`, named for screen readers.
/// Returns whether it was clicked.
fn action_button(ui: &mut Ui, t: &Tokens, label: &str, icon: &str) -> bool {
@@ -743,6 +816,20 @@ fn paint_chip(ui: &mut Ui, t: &Tokens, paint: &vectorcraft_color::Paint, mixed:
resp.on_hover_text(tip).clicked()
}
/// The Contextual Task Bar's items with the Artboard tool in a document of `artboards` artboards:
/// Resize, Duplicate, Rearrange All (with two or more) and Export.
fn artboard_items(items: &mut Vec<(&'static str, &'static str, &'static str, serde_json::Value)>, artboards: usize) {
// As in the reference app, the Artboard tool offers to resize, duplicate and export the
// artboards (<https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580>),
// and to rearrange them (<https://creativepro.com/managing-artboards-in-illustrator/>).
items.push((tl!("Resize"), "move-diagonal-2", TASK_BAR_ARTBOARD_OPTIONS, json!({})));
items.push((tl!("Duplicate"), "copy", TASK_BAR_DUPLICATE_ARTBOARDS, json!({})));
if artboards > 1 {
items.push((tl!("Rearrange All"), "dc-rearrange", TASK_BAR_REARRANGE_ARTBOARDS, json!({})));
}
items.push((tl!("Export"), "share-2", "file.exportForScreens", json!({})));
}
/// The Contextual Task Bar's Center on Top and Center on Bottom, as in the Type menu, for type on a
/// closed path.
fn center_on_path_items(app: &VectorcraftApp, items: &mut Vec<(&'static str, &'static str, &'static str, serde_json::Value)>) {
@@ -755,6 +842,9 @@ fn center_on_path_items(app: &VectorcraftApp, items: &mut Vec<(&'static str, &'s
}
}
/// The Contextual Task Bar's Fidelity field with the Smooth tool.
const TASK_BAR_FIDELITY: &str = "taskBar.fidelity";
/// The Contextual Task Bar's Image Trace button: a trace with the Default preset.
const TASK_BAR_IMAGE_TRACE: &str = "taskBar.imageTrace";
+19 -4
View File
@@ -104,6 +104,23 @@ fn fidelity_step(v: f64, n: f64) -> f64 {
v
}
/// The unit of tool `tool`'s Fidelity. The Pencil's, the Paintbrush's and the Smooth tool's
/// Fidelity is on screen, the same at any zoom (#5729); the Blob Brush's is in points.
fn fidelity_unit(tool: &str) -> &'static str {
if matches!(tool, "pencil" | "paintbrush" | "smooth") { "px" } else { "pt" }
}
/// Width of the Fidelity field.
pub(crate) const FIDELITY_FIELD_W: f32 = FIELD_W;
/// The Fidelity field of tool `tool`, showing `v` to two decimals from the tool's lowest Fidelity to
/// 100 and stepping by [`fidelity_step`], named by the label drawn just before it. Returns the value
/// typed or stepped to. The Contextual Task Bar shows the Smooth tool's.
pub(crate) fn fidelity_field(ui: &mut egui::Ui, id: &str, v: f64, tool: &str) -> Option<f64> {
let min = vectorcraft_tools::draw2::min_fidelity(tool);
widgets::range_field_stepped(ui, id, v, min..=100.0, fidelity_unit(tool), 2, FIELD_W, fidelity_step)
}
/// Where Fidelity `v` sits on a slider over `(min, max)`, from 0 to 1. The rail is logarithmic, so
/// the low values, where the stroke is followed closely, get as much of it as the high ones
/// (0.01–1 takes most of a 0.01–20 rail, in the tool's unit).
@@ -211,9 +228,7 @@ fn fidelity(ui: &mut egui::Ui, d: &mut Dialog) {
let v = d.f64("fidelity", 1.5);
let tool = d.str("tool");
let rail = (vectorcraft_tools::draw2::min_fidelity(&tool), FIDELITY_MAX);
// The Pencil's, the Paintbrush's and the Smooth tool's Fidelity is on screen, the same at any
// zoom (#5729).
let unit = if matches!(tool.as_str(), "pencil" | "paintbrush" | "smooth") { "px" } else { "pt" };
let unit = fidelity_unit(&tool);
let rail_w = label_w(ui);
let mut new = None;
ui.label(egui::RichText::new(tl!("Fidelity:")).size(12.5).color(t.text));
@@ -222,7 +237,7 @@ fn fidelity(ui: &mut egui::Ui, d: &mut Dialog) {
let (x, _, keyed) =
widgets::color_slider_stepped(ui, "freehand-fidelity", tl!("Fidelity:"), Some(shown), fidelity_at(v, rail), rail_w, &|_| t.input_border);
new = x.map(|x| fidelity_from(x, rail)).or(keyed);
if let Some(x) = widgets::range_field_stepped(ui, "freehand-fidelity-field", v, rail.0..=100.0, unit, 2, FIELD_W, fidelity_step) {
if let Some(x) = fidelity_field(ui, "freehand-fidelity-field", v, &tool) {
new = Some(x);
}
});
+82 -1
View File
@@ -2,7 +2,8 @@
//! Path, compound paths and clipping masks their Release, images selected with vector art Make
//! Clipping Mask, type converts between point and area type, type on a path opens its options, and
//! a basic shape shows its properties in place of Lock. The basic shape tools show Edit Path, Fill,
//! Stroke and Shape Properties for the shape they draw. Type, and the Type tools while they edit it,
//! Stroke and Shape Properties for the shape they draw, and the Smooth tool its Fidelity and Tool
//! Options… for a path it can smooth. Type, and the Type tools while they edit it,
//! show the font family, font style and Font Size controls.
use egui::{Event, PointerButton, Pos2, Rect, vec2};
@@ -721,6 +722,86 @@ fn shape_tool_bar_retargets_after_another_shape_and_undo() {
assert_eq!(app.session.tool_id(), "directSelection");
}
/// Where the text the bar draws just right of `label`, on the same row, centers: the field after
/// the label.
fn field_after(texts: &[(String, Rect)], label: &str) -> Pos2 {
let l = texts.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("no `{label}` in {texts:?}")).1;
texts
.iter()
.filter(|(_, r)| r.left() > l.right() && (r.center().y - l.center().y).abs() < 8.0)
.min_by(|a, b| a.1.left().total_cmp(&b.1.left()))
.unwrap_or_else(|| panic!("no field after `{label}` in {texts:?}"))
.1
.center()
}
/// The Smooth tool, with a path it can smooth selected, shows its Fidelity field and Tool Options…,
/// and none of the selection tools' actions or chips. A value typed in the field sets the tool's
/// Fidelity, and Tool Options… opens the tool's options with that value.
#[test]
fn the_smooth_tool_shows_fidelity_and_tool_options_for_a_path() {
let mut app = app();
let path = id(app.run("shape.ellipse", json!({"x": 60, "y": 60, "width": 120, "height": 80})).unwrap());
select(&mut app, &[path]);
app.run("tool.select", json!({"tool": "smooth"})).unwrap();
let (_, named) = buttons(&mut app);
let names = names(&named);
for name in ["Tool Options…", "More Options"] {
assert!(names.contains(&name), "{name} in {names:?}");
}
for name in ["Fill", "Stroke", "Recolor Artwork", "Lock (⌘2)", "Duplicate", "Edit Path", "Offset Path", "Simplify", "Smooth"] {
assert!(!names.contains(&name), "{name} is left out of {names:?}");
}
let (ctx, texts) = bar(&mut app);
let p = field_after(&texts, "Fidelity:");
let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, vec![Event::PointerMoved(p), press(true), press(false)]);
frame(&mut app, &ctx, vec![]);
frame(&mut app, &ctx, vec![Event::Text("7".into())]);
let enter = Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
frame(&mut app, &ctx, vec![enter]);
frame(&mut app, &ctx, vec![]);
assert_eq!(app.session.tool_options()["fidelity"].as_f64(), Some(7.0));
assert_eq!(app.session.tool_id(), "smooth");
assert_eq!(app.session.active().unwrap().selection.objects, [NodeId(path)], "the selection is kept");
click_button(&mut app, "Tool Options…");
let d = app.ui.dialog.as_ref().expect("the Smooth tool's options");
assert_eq!((d.kind.as_str(), d.str("tool").as_str()), (crate::dialogs::freehand::KIND, "smooth"));
assert_eq!(d.f64("fidelity", 0.0), 7.0);
}
/// The Smooth tool shows no bar without a selected path it can smooth: with nothing selected, a
/// straight line or type. With a selection tool the bar has the selection's actions again, without
/// Fidelity.
#[test]
fn the_smooth_tool_shows_no_bar_without_a_path_to_smooth() {
let mut app = app();
let path = id(app.run("shape.ellipse", json!({"x": 60, "y": 60, "width": 120, "height": 80})).unwrap());
let line = id(app.run("shape.line", json!({"x1": 150, "y1": 200, "x2": 300, "y2": 240})).unwrap());
let text = id(app.run("text.create", json!({"x": 220, "y": 60, "text": "Type"})).unwrap());
app.run("tool.select", json!({"tool": "smooth"})).unwrap();
for (what, ids) in [("nothing", vec![]), ("a straight line", vec![line]), ("type", vec![text])] {
select(&mut app, &ids);
let (ctx, _) = bar(&mut app);
assert!(canvas::task_bar_rect(&ctx).is_none(), "{what}: no bar");
}
select(&mut app, &[path, line]);
let (ctx, texts) = bar(&mut app);
assert!(canvas::task_bar_rect(&ctx).is_some() && has(&texts, "Fidelity:"), "a path with a line: the bar, {texts:?}");
// On one canvas the bar follows each change of the selection.
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for (ids, shown) in [(vec![path], true), (vec![line], false), (vec![], false), (vec![path], true)] {
select(&mut app, &ids);
frame(&mut app, &ctx, vec![]);
frame(&mut app, &ctx, vec![]);
assert_eq!(canvas::task_bar_rect(&ctx).is_some(), shown, "{ids:?}");
}
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let (_, texts) = bar(&mut app);
assert!(has(&texts, "Duplicate") && !has(&texts, "Fidelity:"), "{texts:?}");
}
/// A compound path under the Direct Selection tool isn't offered Edit Path, which would only switch
/// to the tool already in use; the Selection tool still offers it.
#[test]
+2 -1
View File
@@ -902,7 +902,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- With automation read roots in force, `svgInteractivity.info`, `addFile` and `removeFile`, the JavaScript Files dialog in a frame a request drives and `ui_inspect`'s selected file name a linked JavaScript file outside the roots by its file name alone, as `document.json` does; `removeFile` unlinks it by that name and refuses a name that fits more than one linked file (`tests/interactivity.rs`).
- Numeric fields keep the pointer's pressed character as the text-drag start when movement exceeds the click threshold (#1326); a completed focus click selects the unit too. Pixel/point, plain and optional fields are regression-tested in both directions, before/after focus, with replacement text; keyboard focus, double-click, arrows and wheel keep their prior behavior. Native egui input confirms partial replacement gives 17 px instead of 700 px, one Undo/Redo, window captures and an exact saved-document read in a separate CLI process. A focus click released more than 1 pt from its press keeps the caret, and a tap within 1 pt still selects all (#6659; short-drag and jitter-tap regressions in `tests/numfields.rs`).
- Unit fields and length strings take `%` in arithmetic: `3cm*50%` reads as 1.5 cm, and `50pt+25%` reads as 62.5 pt.
- The contextual task bar's handle drags it anywhere on the canvas (kept whole on it as the window resizes); unpinned it keeps that offset as it follows the selection, and its More Options menu hides it, pins its position across selection changes (`window.taskBar.pin`), resets it under the selection (`window.taskBar.reset`) and shows the Properties panel. As in Illustrator the position isn't kept across launches. Not done: the bar's variants for other tools (only the selection tools, the Type tools, the basic shape tools and the Artboard tool show it).
- The contextual task bar's handle drags it anywhere on the canvas (kept whole on it as the window resizes); unpinned it keeps that offset as it follows the selection, and its More Options menu hides it, pins its position across selection changes (`window.taskBar.pin`), resets it under the selection (`window.taskBar.reset`) and shows the Properties panel. As in Illustrator the position isn't kept across launches. Not done: the bar's variants for other tools (only the selection tools, the Type tools, the basic shape tools, the Smooth tool and the Artboard tool show it).
- A click on another object moves the contextual task bar under it without hiding it for the press; it gives way only to a drag (#4681).
- With one image selected, the contextual task bar offers Image Trace, which traces the image at once with the Default preset (the Image Trace panel is left as it was), and Crop Image; with an Image Trace object it offers Expand, which keeps only the traced paths. This follows Illustrator's bar: for a selected image it includes Image Trace, Retype and Crop Image, and traced artwork can be expanded from it ([Contextual Task Bar announcement](https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580)). Offset Path and Simplify, which don't apply to an image, aren't offered for one. Not done: Retype, which the app doesn't have.
- With the Artboard tool, the contextual task bar sits under the selected artboards and offers Resize (Artboard Options for the active artboard), Duplicate (each selected artboard, in one undo step, the copies then selected), Rearrange All (with two or more artboards) and Export (Export for Screens with the selected artboards checked). This follows Illustrator's bar: while the Artboard tool is in use, it has options for resizing, duplicating and exporting artboards (the [Contextual Task Bar announcement](https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580); Rearrange and Export on the bar: [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/)).
@@ -912,6 +912,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- The help page describes the contextual task bar as one horizontal row (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/workspace-overview.html>) and gives no rule for a canvas narrower than that row. VectorCraft's own rule: the bar stays whole on the canvas. When its actions don't fit in one row, they wrap, in order, onto rows under the row with the handle, the chips and More Options; an action wider than any row, or one that would need a row past the canvas's height, is listed in More Options instead; when the chips don't fit beside the handle and More Options, they take rows after the actions; with no row to spare for the font controls, the bar leaves them out; a canvas too small for the handle and More Options shows no bar. Evidence: `a_task_bars_actions_wrap_on_a_narrow_canvas` and `a_task_bar_action_wider_than_the_canvas_goes_in_more_options` in `crates/ui-egui/src/tests/taskbar.rs`.
- With a live rectangle, rounded rectangle, ellipse or polygon selected with the Selection tool, the contextual task bar shows a Shape Properties button in place of Lock. It opens a popup with the shape's rows from the Transform panel (corner radius, pie angles, or sides, angle, corner and radius), which edit it through `object.setLiveShape`. The overview describes this: when a shape drawn with a shape tool (Rectangle, Star) is selected, the bar offers to change its properties where Lock Object would be ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Object › Lock and ⌘2 still lock the shape. Not done: stars, which the app draws as plain paths. Evidence: `a_basic_shape_shows_its_properties_in_place_of_lock`, `the_shape_properties_popup_edits_the_shape` and `other_selections_keep_lock` in `crates/ui-egui/src/tests/taskbar.rs`.
- While the Rectangle, Rounded Rectangle, Ellipse, Polygon or Star tool is in use, the contextual task bar offers Edit Path (the Direct Selection tool), the Fill and Stroke chips, and Shape Properties for the shape it draws, as the help page lists for those five tools (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>). VectorCraft's own rules: the bar shows while one of those tools has one closed path selected whole, as right after it draws one, and not with nothing selected, several objects, a group, an open path, type or picked-out anchors; it shows nothing during the drag that draws the shape; it leaves out the selection tools' Duplicate, Offset Path, Simplify, Recolor Artwork and Lock; the Line Segment tool shows no bar. Not done: Shape Properties for a star, which the app draws as a plain path, and Gen Shape Fill and Generate Vectors, which the app doesn't have. Evidence: `shape_tools_show_edit_path_paint_and_properties_after_drawing`, `shape_tools_have_no_shape_bar_without_a_single_shape` and `shape_tool_bar_retargets_after_another_shape_and_undo` in `crates/ui-egui/src/tests/taskbar.rs`.
- While the Smooth tool is in use with a path it can smooth selected (a path with three or more anchors, or two joined by a curve, on its own or inside a selected group), the contextual task bar shows the tool's Fidelity field and Tool Options…, which opens the Smooth tool's options. The overview says the Smooth tool's bar has its tool options (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>), and the tool's help page names Fidelity as its option (<https://helpx.adobe.com/illustrator/using/tool-techniques/smooth-tool.html>). A value typed or stepped in the field sets the tool's Fidelity through `tool.setOption`, with the same range, unit (screen pixels) and steps as the field in its options. VectorCraft's own rules: with nothing selected, or only straight lines or type selected, the tool shows no bar; the bar leaves out the selection tools' actions, the Fill and Stroke chips, Recolor Artwork and Lock; on a canvas too narrow for the field, More Options lists Fidelity, which opens the tool's options. Evidence: `the_smooth_tool_shows_fidelity_and_tool_options_for_a_path` and `the_smooth_tool_shows_no_bar_without_a_path_to_smooth` in `crates/ui-egui/src/tests/taskbar.rs`.
- The contextual task bar has a Stroke chip beside the Fill chip, drawn with the paint the Stroke proxy shows (a "?" when the selected objects' strokes differ, as on the Fill chip); a click brings the Stroke proxy forward and shows the Swatches panel wherever it is docked, floating or closed, as a click on the Fill chip does for the Fill proxy. The reference app's bar has Fill and Stroke controls for changing an object's colors ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Evidence: `the_task_bar_shows_a_stroke_chip_beside_fill` and `the_task_bar_chips_follow_the_proxies_and_show_swatches_anywhere` in `crates/ui-egui/src/canvas/tests.rs`.
- With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, and Convert Selected Anchor Points to Corner and to Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons; the two convert buttons show their icons alone, named by their tool tips as on the Control bar (VectorCraft's own choice). The overview lists items that delete the selected anchors, join selected end points, split the path at the selected anchors, and make the selected anchors corner or smooth points ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). With a path selected whole with Direct Selection, the bar also offers Simplify and Smooth, which open the Simplify and Smooth sliders as Object › Path › Simplify… and Smooth… do; the same overview names Smooth as adjusting the path's curvature (#6784). VectorCraft's own rule: while anchors are picked out, the bar offers the anchor items without Simplify and Smooth. Evidence: `direct_selection_smooth_opens_the_smooth_slider` in `crates/ui-egui/src/tests/taskbar.rs`.
- Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it.