Merge pull request #7139 from dotCooCoo/feat/ctb-type-tools-font-fill

Contextual Task Bar: the Type tools show font family, style, size and Fill
This commit is contained in:
BFlat
2026-11-14 01:54:39 +07:00
committed by GitHub
9 changed files with 520 additions and 48 deletions
+3 -1
View File
@@ -67,8 +67,10 @@ pub(crate) use panes::{consolidate, pane, tile, tiled_area};
use raster::{changed_pixels, pixel_region, upload};
pub(crate) use raster::{color_image, color_image_owned, render_job};
pub use selection_marks::SelectionMarks;
#[cfg(test)]
pub(crate) use task_bar::chip_paints;
use task_bar::task_bar;
pub(crate) use task_bar::{has_colors, task_bar_rect};
pub(crate) use task_bar::{has_colors, task_bar_rect, task_bar_shows_fonts, task_bar_shows_type};
#[cfg(test)]
use tool_overlays::{DIAL_RADIUS, HiddenMarks};
pub use tool_overlays::{HiddenChars, SpellingMarks};
+123 -27
View File
@@ -13,6 +13,12 @@ pub(crate) fn task_bar_rect(ctx: &egui::Context) -> Option<egui::Rect> {
ctx.memory(|m| m.areas().is_visible(&egui::LayerId::new(egui::Order::Middle, id)).then(|| m.area_rect(id)).flatten())
}
/// Whether the Contextual Task Bar, where it was last drawn, held the font controls: on a canvas
/// too narrow for them it leaves them out.
pub(crate) fn task_bar_shows_fonts(ctx: &egui::Context) -> bool {
task_bar_rect(ctx).is_some() && ctx.data(|d| d.get_temp::<bool>(task_bar_id().with("fonts")).unwrap_or(false))
}
/// Whether the objects `ids` (or anything inside them) are painted with a colour: a solid colour,
/// a gradient's stops or a mesh's points, which Recolor Artwork can change.
pub(crate) fn has_colors(doc: &vectorcraft_doc::Document, ids: &[vectorcraft_doc::NodeId]) -> bool {
@@ -30,21 +36,59 @@ fn picked_artboards_bounds(app: &VectorcraftApp) -> Option<Rect> {
picked.iter().filter_map(|i| st.doc.artboards.get(*i)).map(|a| a.rect).reduce(|a, b| a.union(b))
}
/// The Type tools whose Contextual Task Bar shows the text formatting controls. The Touch Type
/// tool has no bar: its own handles take the place of one.
const TYPE_TOOLS: [&str; 6] = ["type", "areaType", "typeOnPath", "verticalType", "verticalAreaType", "verticalTypeOnPath"];
/// Whether every selected object is a type object, with no anchors picked out.
fn only_type_selected(app: &VectorcraftApp) -> bool {
app.session.active().is_some_and(|st| {
!st.selection.is_empty()
&& st.selection.anchors.is_empty()
&& st.selection.objects.iter().all(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Text(_))))
})
}
/// Whether a selection tool is in use with something selected.
fn selection_tool_with_selection(app: &VectorcraftApp) -> bool {
matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection")
&& app.session.active().is_some_and(|st| !st.selection.is_empty())
}
/// Whether a Type tool edits type, or has type selected: its bar then shows only the text
/// formatting controls.
fn type_tool_on_type(app: &VectorcraftApp) -> bool {
TYPE_TOOLS.contains(&app.session.tool_id()) && (crate::panels::character::text_editing(app).is_some() || only_type_selected(app))
}
/// Whether the Contextual Task Bar shows the font family, font style and Font Size controls: a
/// Type tool edits or has selected type, or a selection tool has only type selected.
pub(crate) fn task_bar_shows_type(app: &VectorcraftApp) -> bool {
type_tool_on_type(app) || (selection_tool_with_selection(app) && only_type_selected(app))
}
/// The Contextual Task Bar: a floating pill under the selection with the most likely next actions.
/// With the Artboard tool it sits under the selected artboards and offers Resize (Artboard
/// Options), Duplicate, Rearrange All (with two or more artboards) and Export (Export for Screens).
/// A basic shape selected with the Selection tool shows a Shape Properties button in place of Lock.
/// 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. The font controls take a second row when one row would be wider than the canvas, and
/// narrow there to fit it.
/// Its handle drags it anywhere on the canvas; unpinned it keeps that offset as it follows the
/// selection, pinned it stays put ([`crate::state::TaskBarPlace`]).
pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
let t = Tokens::get(ui.ctx());
let on_artboards = app.session.tool_id() == "artboard";
if !on_artboards
&& (app.session.active().is_none_or(|st| st.selection.is_empty())
|| !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection"))
{
// Text added with a Type tool shows its formatting controls, as the help page describes
// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>).
let typing = type_tool_on_type(app);
if !(on_artboards || typing || selection_tool_with_selection(app)) {
return;
}
// Selected type has the font controls too, as the help page describes
// (<https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/font-options-overview.html>).
let fonts = task_bar_shows_type(app);
let Some(b) = (if on_artboards { picked_artboards_bounds(app) } else { app.selection_bounds() }) else { return };
let Some(st) = app.session.active() else { return };
let n = st.selection.len();
@@ -93,7 +137,9 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
items.push((tl!("Rearrange All"), "dc-rearrange", TASK_BAR_REARRANGE_ARTBOARDS, none()));
}
items.push((tl!("Export"), "share-2", "file.exportForScreens", none()));
} else {
} else if !typing {
// The selection tools' object actions. A Type tool's bar leaves them out, Create Outlines
// and the type conversions included.
// 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"}));
// As in the reference app, an image offers Image Trace, Crop Image and Remove Background, and
@@ -185,22 +231,31 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
}
items.push((tl!("Duplicate"), "copy", "edit.duplicate", none()));
}
// The paints the Fill and Stroke proxies show, "?" when the selected objects' paints differ.
let (fill, stroke) = crate::panels::current_paints(app);
let (fill_mixed, stroke_mixed) = crate::panels::mixed_paints(app, ui.ctx());
let (fill, stroke, fill_mixed, stroke_mixed) = chip_paints(app, ui.ctx());
let colored = has_colors(&st.doc, &st.selection.objects);
let doc = st.uid;
// Under the selection's lowest corner on screen, centered on the selection (the view may be
// rotated).
let lowest = xf.quad(b).iter().map(|c| c.y).fold(f32::NEG_INFINITY, f32::max);
let anchor = pos2(xf.to_screen(b.center()).x, lowest);
let est_w = 150.0 + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::<f32>();
let bounds = xf.rect.shrink(8.0);
// On a canvas too narrow for the type controls at their narrowest, the bar leaves them out.
let fonts = fonts && bounds.width() - 18.0 >= crate::panels::character::task_bar_min_room();
ui.ctx().data_mut(|d| d.insert_temp(task_bar_id().with("fonts"), fonts));
// The width of the bar's frame (margins and stroke), its handle, Fill and Stroke chips,
// Recolor Artwork, Lock or Shape Properties, and More Options, each after a 6-point gap.
let chips: f32 =
[(!on_artboards, 26.0), (!on_artboards && !typing, 26.0), (!on_artboards && !typing && colored, 30.0), (!on_artboards && !typing, 30.0)]
.iter()
.filter(|(shown, _)| *shown)
.map(|(_, w)| w + 6.0)
.sum();
let (font_row, est) = size_estimate(&items, fonts, 18.0 + 4.0 + chips + 36.0, bounds.width());
// Its place under the selection, then where the handle moved it, kept on the canvas (with the
// bar's size last frame, the estimate before it first shows).
let under = pos2(anchor.x - est_w / 2.0, anchor.y + 28.0);
let under = pos2(anchor.x - est.x / 2.0, anchor.y + 28.0);
let id = task_bar_id();
let size = egui::AreaState::load(ui.ctx(), id).and_then(|s| s.size).unwrap_or(vec2(est_w, 44.0));
let bounds = xf.rect.shrink(8.0);
let size = egui::AreaState::load(ui.ctx(), id).and_then(|s| s.size).unwrap_or(est);
let keep_in = |p: Pos2| p.clamp(bounds.min, (bounds.max - size).max(bounds.min));
let place = &mut app.ui.task_bar_place;
// Just unpinned: follow the selection from the pinned spot.
@@ -255,23 +310,21 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
run = Some((*cmd, params.clone()));
}
}
// 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.
if fonts && !font_row {
crate::panels::character::task_bar(app, ui, f32::INFINITY);
}
// The selected objects' Fill, Stroke, Recolor Artwork and Lock (a basic shape's
// properties instead), left out with the Artboard tool.
if !on_artboards {
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
let chip = egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0));
widgets::proxy_chip(ui, chip, &fill, fill_mixed, None);
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.button_border), StrokeKind::Outside);
if resp.on_hover_text(tl!("Fill")).clicked() {
app.session.fill_active = true;
show_swatches = true;
}
// properties instead), left out with the Artboard tool. A Type tool's bar has
// the Fill alone.
if !on_artboards && paint_chip(ui, &t, &fill, fill_mixed, None, tl!("Fill")) {
app.session.fill_active = true;
show_swatches = true;
}
if !on_artboards && !typing {
// The Stroke chip, drawn as a ring as the Control bar's is.
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
let chip = egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0));
widgets::proxy_chip(ui, chip, &stroke, stroke_mixed, Some(4.0));
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.button_border), StrokeKind::Outside);
if resp.on_hover_text(tl!("Stroke")).clicked() {
if paint_chip(ui, &t, &stroke, stroke_mixed, Some(4.0), tl!("Stroke")) {
app.session.fill_active = false;
show_swatches = true;
}
@@ -316,6 +369,13 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
}
});
});
if font_row {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
// The canvas's width less the frame's margins and stroke.
crate::panels::character::task_bar(app, ui, bounds.width() - 18.0);
});
}
});
});
if moved != Vec2::ZERO {
@@ -353,6 +413,42 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
}
}
/// Whether the type controls go on a second row, and the bar's estimated size before it first
/// shows: `items` and, when `fonts`, the font family, font style and Font Size controls, which take
/// a second row when one row, with the bar's other controls `fixed` points wide, would be wider than
/// `room` (and narrow to fit it there).
fn size_estimate(items: &[(&str, &str, &str, serde_json::Value)], fonts: bool, fixed: f32, room: f32) -> (bool, Vec2) {
let font_w: f32 = crate::panels::character::TASK_BAR_WIDTHS.iter().filter(|_| fonts).map(|w| w + 6.0).sum();
let items_w = if fonts { fixed } else { 150.0 } + items.iter().map(|(l, _, _, _)| l.len() as f32 * 7.2 + 44.0).sum::<f32>();
if fonts && items_w + font_w > room { (true, vec2(items_w.max((font_w + 16.0).min(room)), 80.0)) } else { (false, vec2(items_w + font_w, 44.0)) }
}
/// The paints the Fill and Stroke chips show, and whether each is mixed ("?"): the proxies', or
/// while a Type tool edits type, the edited characters' fill, which a swatch picked then paints.
pub(crate) fn chip_paints(app: &VectorcraftApp, ctx: &egui::Context) -> (vectorcraft_color::Paint, vectorcraft_color::Paint, bool, bool) {
let (fill, stroke) = crate::panels::current_paints(app);
let (fill_mixed, stroke_mixed) = crate::panels::mixed_paints(app, ctx);
if crate::panels::character::text_editing(app).is_some()
&& let Some((chars, _)) = crate::panels::character::text_style(app)
{
let mixed = crate::panels::character::shared(app, |s| s.fill.clone()).is_none();
return (chars.fill, stroke, mixed, stroke_mixed);
}
(fill, stroke, fill_mixed, stroke_mixed)
}
/// One of the Contextual Task Bar's Fill and Stroke chips, showing `paint` ("?" when `mixed`, a
/// ring of `ring` points for the stroke), named `tip` for screen readers. Returns whether it was
/// clicked.
fn paint_chip(ui: &mut Ui, t: &Tokens, paint: &vectorcraft_color::Paint, mixed: bool, ring: Option<f32>, tip: &str) -> bool {
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
let chip = egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0));
widgets::proxy_chip(ui, chip, paint, mixed, ring);
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.button_border), StrokeKind::Outside);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tip));
resp.on_hover_text(tip).clicked()
}
/// 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)>) {
+23 -2
View File
@@ -55,6 +55,8 @@ pub enum WheelOwner {
Control,
/// The Character popover attached to this Control bar link.
ControlPopover(egui::Id),
/// The Contextual Task Bar's type controls.
TaskBar,
}
impl WheelOwner {
@@ -68,6 +70,12 @@ impl WheelOwner {
Self::ControlPopover(anchor) => {
return app.ui.control_bar && app.ui.screen_mode < 2 && widgets::popover_is_open(ctx, anchor);
}
Self::TaskBar => {
return app.ui.task_bar
&& app.ui.screen_mode < 3
&& crate::canvas::task_bar_shows_type(app)
&& crate::canvas::task_bar_shows_fonts(ctx);
}
Self::Character => "character",
Self::Properties => {
let selected = app.session.active().and_then(|document| document.selection.objects.first().and_then(|id| document.doc.node(*id)));
@@ -106,12 +114,24 @@ pub struct MenuLook<'a> {
pub wheel: bool,
/// Its owner, so a field hidden before the next layout cannot capture wheel input.
pub wheel_owner: Option<WheelOwner>,
/// The name screen readers give the closed field when no label stands beside it.
pub name: Option<&'a str>,
}
impl Default for MenuLook<'static> {
/// Samples on in the theme's colors, English names, medium rows, no favorites, no wheel.
fn default() -> Self {
Self { samples: true, on_white: false, english_names: true, row: ROW, favorites: &[], recent: &[], wheel: false, wheel_owner: None }
Self {
samples: true,
on_white: false,
english_names: true,
row: ROW,
favorites: &[],
recent: &[],
wheel: false,
wheel_owner: None,
name: None,
}
}
}
@@ -131,6 +151,7 @@ impl<'a> MenuLook<'a> {
recent: app.recent_fonts(),
wheel: false,
wheel_owner: None,
name: None,
}
}
}
@@ -382,7 +403,7 @@ pub fn font_menu(
// The search field and filters stay in place over the rows, which scroll. The rows are all
// one height (a family's styles too), so only those in view are laid out: there are thousands
// of families.
let (field, shown) = widgets::combo_menu_field(ui, &id, current, width, |menu| {
let (field, shown) = widgets::combo_menu_field_named(ui, &id, look.name, current, width, |menu| {
menu.height(height).show_rows_with_header(
|ui| {
// Wider than a narrow field, so the filters keep their rows and long names show.
+40 -5
View File
@@ -929,8 +929,13 @@ fn set_anti_alias(app: &mut VectorcraftApp, m: TypeAntiAlias) {
/// The Font Size combo (Character and Properties panels, Control bar): typed, stepped, scrubbed or
/// picked from the presets, in the type unit, blank where the selected text's sizes differ.
pub(crate) fn size_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: f32) {
size_field_named(app, ui, id, None, width);
}
/// [`size_field`] named `name` for screen readers, where no label stands beside it.
fn size_field_named(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, name: Option<&str>, width: f32) {
let size = shared_effective(app, |s| s.size);
if let Some(v) = widgets::spin_field(ui, id, size, app.session.type_unit(), width, 1.0, 0.1, &SIZE_PRESETS) {
if let Some(v) = widgets::spin_field_named(ui, id, name, size, app.session.type_unit(), width, 1.0, 0.1, &SIZE_PRESETS) {
style(app, json!({"size": v}));
}
}
@@ -1043,7 +1048,7 @@ pub(crate) fn font_pickers(app: &mut VectorcraftApp, ui: &mut Ui, s: &CharStyle,
"cb-font" => Some(crate::font_menu::WheelOwner::Control),
_ => None,
};
font_pickers_owned(app, ui, s, ids, widths, wheel_owner);
font_pickers_owned(app, ui, s, ids, widths, wheel_owner, (None, None));
}
fn font_pickers_owned(
@@ -1053,16 +1058,17 @@ fn font_pickers_owned(
ids: (&str, &str),
widths: (f32, f32),
wheel_owner: Option<crate::font_menu::WheelOwner>,
a11y: (Option<&str>, Option<&str>),
) {
crate::font_menu::sync_wheel_target(app, ui.ctx(), ui.ctx().viewport_id());
let sample = crate::font_menu::sample_text(app);
let look = crate::font_menu::MenuLook { wheel: true, wheel_owner, ..crate::font_menu::MenuLook::of(app) };
let look = crate::font_menu::MenuLook { wheel: true, wheel_owner, name: a11y.0, ..crate::font_menu::MenuLook::of(app) };
if let Some(pick) = crate::font_menu::font_menu(ui, ids.0, &s.font_family, widths.0, sample.as_deref(), look) {
crate::font_menu::apply(app, ui.ctx(), pick);
}
let styles = vectorcraft_text::FontDb::global().styles(&s.font_family);
let names: Vec<&str> = styles.iter().map(String::as_str).collect();
if let Some(name) = widgets::dropdown_names(ui, ids.1, &s.font_style, &names, widths.1).and_then(|i| names.get(i)) {
if let Some(name) = widgets::dropdown_names_named(ui, ids.1, a11y.1, &s.font_style, &names, widths.1).and_then(|i| names.get(i)) {
style(app, json!({"style": name}));
}
}
@@ -1157,6 +1163,35 @@ pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
crate::chrome::group_separator(ui);
}
/// The Contextual Task Bar's widths of the font family menu, the font style dropdown and the
/// Font Size combo.
pub(crate) const TASK_BAR_WIDTHS: [f32; 3] = [140.0, 90.0, 76.0];
/// The smallest share of [`TASK_BAR_WIDTHS`] the Contextual Task Bar's type controls narrow to.
const TASK_BAR_MIN_SCALE: f32 = 0.4;
/// The gaps between the Contextual Task Bar's type controls.
fn task_bar_gaps() -> f32 {
6.0 * (TASK_BAR_WIDTHS.len() - 1) as f32
}
/// The narrowest room the Contextual Task Bar's type controls fit in, with their spacing.
pub(crate) fn task_bar_min_room() -> f32 {
TASK_BAR_MIN_SCALE * TASK_BAR_WIDTHS.iter().sum::<f32>() + task_bar_gaps()
}
/// The Contextual Task Bar's type controls while type is selected or edited: the font family and
/// style menus and the Font Size combo, as the Control bar has them, named for screen readers.
/// They narrow, down to 40% of their widths, to fit in `room` points with their spacing.
pub(crate) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, room: f32) {
let Some((s, _)) = text_style(app) else { return };
let k = ((room - task_bar_gaps()) / TASK_BAR_WIDTHS.iter().sum::<f32>()).clamp(TASK_BAR_MIN_SCALE, 1.0);
let [family, style, size] = TASK_BAR_WIDTHS.map(|w| w * k);
let owner = Some(crate::font_menu::WheelOwner::TaskBar);
font_pickers_owned(app, ui, &s, ("tb-font", "tb-font-style"), (family, style), owner, (Some(tl!("Font Family")), Some(tl!("Style"))));
size_field_named(app, ui, "tb-font-size", Some(tl!("Font Size")), size);
}
/// The panel state key of the panel menu's Show Snap to Glyph Options.
const SNAP_TO_GLYPH_OPTIONS: &str = "ch-snap-to-glyph-options";
/// Set when Show Snap to Glyph Options is chosen: the next frame scrolls the panel to the options,
@@ -1227,7 +1262,7 @@ fn attributes(app: &mut VectorcraftApp, ui: &mut Ui, owner: crate::font_menu::Wh
if let Some(f) = app.character_focus {
widgets::arm_field_focus(ui.ctx(), f.salt());
}
font_pickers_owned(app, ui, &s, ("ch-font", "ch-style"), (w - 4.0, w - 4.0), Some(owner));
font_pickers_owned(app, ui, &s, ("ch-font", "ch-style"), (w - 4.0, w - 4.0), Some(owner), (None, None));
variable_font(app, ui, &s, w - 4.0);
ui.add_space(2.0);
let hidden = pstate::<bool>(ui.ctx(), "ch-hide-options");
+300 -1
View File
@@ -1,7 +1,8 @@
//! The Contextual Task Bar's actions for each kind of selection: open paths offer Join and Edit
//! 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.
//! a basic shape shows its properties in place of Lock. 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};
use serde_json::json;
@@ -535,6 +536,304 @@ fn the_task_bar_follows_the_active_document() {
assert!(!locked(&app, rect), "the button doesn't lock the shape");
}
/// The Type tools whose bar shows the font and Fill controls.
const TYPE_TOOLS: [&str; 6] = ["type", "areaType", "typeOnPath", "verticalType", "verticalAreaType", "verticalTypeOnPath"];
/// The labels drawn inside the task bar (none while it isn't shown), with the context.
fn bar_texts(app: &mut VectorcraftApp) -> (egui::Context, Vec<(String, Rect)>) {
let (ctx, texts) = bar(app);
let Some(r) = canvas::task_bar_rect(&ctx) else { return (ctx, vec![]) };
(ctx, texts.into_iter().filter(|(_, t)| r.contains(t.center())).collect())
}
/// Point type "Hello" at 20 pt, made with `tool` and edited by it as after a click with it.
fn edited_type(app: &mut VectorcraftApp, tool: &str) -> u64 {
app.run("tool.select", json!({ "tool": tool })).unwrap();
let t = id(app.run("text.create", json!({"x": 60, "y": 80, "text": "Hello", "size": 20})).unwrap());
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
assert!(crate::panels::character::text_editing(app).is_some(), "{tool} edits the type");
t
}
/// The font family and style of type `t`'s first character.
fn font_of(app: &VectorcraftApp, t: u64) -> (String, String) {
match kind(app, t) {
NodeKind::Text(t) => (t.first_style().font_family, t.first_style().font_style),
k => panic!("{k:?}"),
}
}
/// The size of each run of type `t`, with its text.
fn sizes(app: &VectorcraftApp, t: u64) -> Vec<(String, f64)> {
match kind(app, t) {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.size)).collect(),
k => panic!("{k:?}"),
}
}
/// Each Type tool that adds or edits type shows the font family, font style and Font Size controls
/// and the Fill, without the selection tools' object actions, the Stroke chip or Lock.
#[test]
fn the_type_tools_show_font_and_fill_controls() {
for tool in TYPE_TOOLS {
let mut app = app();
let t = edited_type(&mut app, tool);
let (family, style) = font_of(&app, t);
let (_, texts) = bar_texts(&mut app);
for label in [family.as_str(), style.as_str(), "20 pt"] {
assert!(has(&texts, label), "{tool}: {label} in {texts:?}");
}
for label in ["Create Outlines", "Convert To Area Type", "Dynamic Text", "Duplicate"] {
assert!(!has(&texts, label), "{tool}: {label} in {texts:?}");
}
let tree = crate::widgets::accessibility_tests::tree(|ui| canvas::show(&mut app, ui));
let named = |label: &str| tree.iter().any(|n| n.label().is_some_and(|l| l.starts_with(label)));
assert!(named("Fill"), "{tool}: the Fill shows");
for label in ["Stroke", "Recolor Artwork", "Lock"] {
assert!(!named(label), "{tool}: {label} is left out");
}
}
}
/// A Type tool shows no bar before it adds type, nor with other art selected; the Touch Type tool
/// shows none with type selected.
#[test]
fn the_type_tools_show_no_bar_without_type() {
let mut app = app();
app.run("tool.select", json!({"tool": "type"})).unwrap();
let (ctx, _) = bar(&mut app);
assert!(canvas::task_bar_rect(&ctx).is_none(), "nothing to format");
let rect = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap());
select(&mut app, &[rect]);
app.run("tool.select", json!({"tool": "type"})).unwrap();
let (ctx, _) = bar(&mut app);
assert!(canvas::task_bar_rect(&ctx).is_none(), "a rectangle is not type");
let t = id(app.run("text.create", json!({"x": 60, "y": 180, "text": "Hello", "size": 20})).unwrap());
app.run("tool.select", json!({"tool": "touchType"})).unwrap();
select(&mut app, &[t]);
let (ctx, _) = bar(&mut app);
assert!(canvas::task_bar_rect(&ctx).is_none(), "Touch Type has its own handles");
}
/// Selected type keeps its actions and gains the font controls, for one type object or several;
/// type selected with other art does not.
#[test]
fn selected_type_shows_font_controls_beside_its_actions() {
let mut app = app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let a = id(app.run("text.create", json!({"x": 60, "y": 80, "text": "Hello", "size": 20})).unwrap());
let b = id(app.run("text.create", json!({"x": 60, "y": 160, "text": "World", "size": 20})).unwrap());
let rect = id(app.run("shape.rectangle", json!({"x": 200, "y": 40, "width": 80, "height": 60})).unwrap());
let (family, _) = font_of(&app, a);
select(&mut app, &[a]);
let (_, texts) = bar_texts(&mut app);
for label in ["Create Outlines", "Convert To Area Type", "Duplicate", family.as_str(), "20 pt"] {
assert!(has(&texts, label), "{label} in {texts:?}");
}
select(&mut app, &[a, b]);
let (_, texts) = bar_texts(&mut app);
assert!(has(&texts, family.as_str()) && has(&texts, "20 pt"), "{texts:?}");
select(&mut app, &[a, rect]);
let (_, texts) = bar_texts(&mut app);
assert!(has(&texts, "Group") && !has(&texts, family.as_str()) && !has(&texts, "20 pt"), "{texts:?}");
select(&mut app, &[rect]);
let (_, texts) = bar_texts(&mut app);
assert!(!has(&texts, family.as_str()) && !has(&texts, "20 pt"), "{texts:?}");
}
/// The bar's Font Size sets the characters a Type tool selects, one undo step; with type selected
/// it sets every selected type object, as the Character panel's does.
#[test]
fn the_task_bars_font_size_sets_what_the_character_panel_would() {
let mut app = app();
let t = edited_type(&mut app, "type");
app.session.set_tool_option("select", &json!({"start": 0, "end": 2}));
let (ctx, texts) = bar_texts(&mut app);
type_into(&mut app, &ctx, &texts, "20 pt", "30");
assert_eq!(sizes(&app, t), [("He".to_string(), 30.0), ("llo".to_string(), 20.0)]);
// Mixed sizes leave the field blank, as in the Character panel.
app.session.set_tool_option("select", &json!({"start": 0, "end": 5}));
let (_, texts) = bar_texts(&mut app);
assert!(!has(&texts, "20 pt") && !has(&texts, "30 pt"), "{texts:?}");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(sizes(&app, t), [("Hello".to_string(), 20.0)]);
let mut app = self::app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let a = id(app.run("text.create", json!({"x": 60, "y": 80, "text": "Hello", "size": 20})).unwrap());
let b = id(app.run("text.create", json!({"x": 60, "y": 160, "text": "World", "size": 20})).unwrap());
select(&mut app, &[a, b]);
let (ctx, texts) = bar_texts(&mut app);
type_into(&mut app, &ctx, &texts, "20 pt", "14");
assert_eq!(sizes(&app, a), [("Hello".to_string(), 14.0)]);
assert_eq!(sizes(&app, b), [("World".to_string(), 14.0)]);
}
/// At a caret, with no characters selected, the bar's Font Size sets the edited type as the
/// Character panel's does, and the characters typed next take it.
#[test]
fn the_task_bars_font_size_at_a_caret_sets_what_is_typed_next() {
let mut app = app();
let t = edited_type(&mut app, "type");
app.session.set_tool_option("select", &json!({"start": 5, "end": 5}));
assert_eq!(crate::panels::character::text_editing(&app), Some((NodeId(t), 5, 5)), "a caret after the type");
let (ctx, texts) = bar_texts(&mut app);
type_into(&mut app, &ctx, &texts, "20 pt", "30");
app.session.tool_typed_text("!", app.view_info()).unwrap();
assert_eq!(sizes(&app, t), [("Hello!".to_string(), 30.0)]);
}
/// The Type tool's Fill chip makes the Fill active and brings up the Swatches panel.
#[test]
fn the_type_tools_fill_chip_activates_the_fill() {
let mut app = app();
edited_type(&mut app, "type");
app.session.fill_active = false;
let (ctx, _) = bar_texts(&mut app);
let r = canvas::task_bar_rect(&ctx).expect("the task bar shows");
// Right to left: the frame's margin, More Options, then the Fill chip.
let p = Pos2::new(r.right() - 8.0 - 30.0 - 6.0 - 13.0, r.center().y);
let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(&mut app, &ctx, vec![Event::PointerMoved(p)]);
frame(&mut app, &ctx, vec![press(true)]);
frame(&mut app, &ctx, vec![press(false)]);
assert!(app.session.fill_active, "the Fill is active");
assert!(crate::panels::character::text_editing(&app).is_some(), "the type is still edited");
}
/// Screen readers name the Type tool bar's controls, and the bar stays on the canvas for type at
/// its lower right corner.
#[test]
fn the_type_tools_bar_is_named_and_stays_on_the_canvas() {
use crate::widgets::accessibility_tests::{named, tree};
let mut app = app();
edited_type(&mut app, "type");
let tree = tree(|ui| canvas::show(&mut app, ui));
for label in ["Font Family", "Style", "Font Size", "Fill"] {
named(&tree, label);
}
let mut app = self::app();
app.run("tool.select", json!({"tool": "type"})).unwrap();
app.run("text.create", json!({"x": 380, "y": 290, "text": "Corner", "size": 20})).unwrap();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
// The type near the canvas's lower edge, so the bar's place under it is off the canvas.
let view = app.view_mut().unwrap();
(view.center, view.zoom) = (vectorcraft_geom::Point::new(200.0, 20.0), 1.0);
let (ctx, _) = bar(&mut app);
let r = canvas::task_bar_rect(&ctx).expect("the task bar shows");
let canvas = app.canvas_rect.expect("the canvas is laid out");
assert!(canvas.contains_rect(r), "{r:?} in {canvas:?}");
}
/// The task bar's rect after a few frames on a `width` by 700 screen, and the canvas's rect.
fn bar_on_screen(app: &mut VectorcraftApp, width: f32) -> (Rect, Rect) {
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let screen = Rect::from_min_size(Pos2::ZERO, vec2(width, 700.0));
for _ in 0..3 {
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| canvas::show(app, ui));
out.textures_delta.clear();
}
(canvas::task_bar_rect(&ctx).expect("the task bar shows"), app.canvas_rect.expect("the canvas is laid out"))
}
/// On a canvas narrower than one row of the bar, the font controls go on a second row, narrowed to
/// fit, and the bar stays whole on the canvas: selected type's actions, and a Type tool's Fill.
#[test]
fn selected_types_font_controls_take_a_second_row_on_a_narrow_canvas() {
let mut app = app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let t = id(app.run("text.create", json!({"x": 20, "y": 60, "text": "Hello", "size": 20})).unwrap());
select(&mut app, &[t]);
let (r, canvas) = bar_on_screen(&mut app, 760.0);
assert!(canvas.contains_rect(r), "{r:?} in {canvas:?}");
assert!(r.height() > 60.0, "the font controls take a second row: {r:?}");
let mut app = self::app();
app.run("tool.select", json!({"tool": "type"})).unwrap();
app.run("text.create", json!({"x": 20, "y": 60, "text": "Hello", "size": 20})).unwrap();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
// Narrower than the font controls at their full widths too.
let (r, canvas) = bar_on_screen(&mut app, 300.0);
assert!(canvas.contains_rect(r), "{r:?} in {canvas:?}");
assert!(r.height() > 60.0, "the font controls take a second row: {r:?}");
let (r, _) = bar_on_screen(&mut app, 1000.0);
assert!(r.height() < 60.0, "one row where it fits: {r:?}");
// The Type tool's bar has only the Fill and More Options beside the controls: one row fits.
let (r, canvas) = bar_on_screen(&mut app, 460.0);
assert!(canvas.contains_rect(r) && r.height() < 60.0, "one row on a 460 px screen: {r:?} in {canvas:?}");
// Too narrow for the controls at their narrowest: the bar leaves them out.
let (r, canvas) = bar_on_screen(&mut app, 160.0);
assert!(canvas.contains_rect(r) && r.height() < 60.0, "no font controls on a 160 px screen: {r:?} in {canvas:?}");
}
/// When the canvas narrows so far that the bar leaves its font controls out, the font family
/// field it drew before no longer takes the mouse wheel.
#[test]
fn a_task_bar_without_its_font_controls_leaves_their_wheel() {
let mut app = app();
app.run("tool.select", json!({"tool": "type"})).unwrap();
app.run("text.create", json!({"x": 20, "y": 60, "text": "Hello", "size": 20})).unwrap();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let draw = |app: &mut VectorcraftApp, width: f32| {
let screen = Rect::from_min_size(Pos2::ZERO, vec2(width, 700.0));
for _ in 0..3 {
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| canvas::show(app, ui));
out.textures_delta.clear();
}
};
draw(&mut app, 1000.0);
let bar = canvas::task_bar_rect(&ctx).expect("the task bar shows");
let viewport = egui::ViewportId::ROOT;
let points = (0..(bar.width() / 4.0) as usize).flat_map(|x| (0..(bar.height() / 4.0) as usize).map(move |y| (x, y)));
let field = points
.map(|(x, y)| bar.min + vec2(x as f32 * 4.0, y as f32 * 4.0))
.find(|p| crate::font_menu::wheel_field_layer_for_test(&ctx, viewport, *p).is_some())
.expect("the bar's font family field takes the wheel");
draw(&mut app, 160.0);
assert!(canvas::task_bar_rect(&ctx).is_some(), "the bar still shows, without its font controls");
crate::font_menu::sync_wheel_target(&app, &ctx, viewport);
assert!(crate::font_menu::wheel_field_layer_for_test(&ctx, viewport, field).is_none(), "the left-out field at {field:?} still takes the wheel");
// Hiding the bar (Window › Contextual Task Bar) releases the field before the next layout.
draw(&mut app, 1000.0);
assert!(crate::font_menu::wheel_field_layer_for_test(&ctx, viewport, field).is_some(), "the field is back");
app.run("window.taskBar", json!({})).unwrap();
crate::font_menu::sync_wheel_target(&app, &ctx, viewport);
assert!(
crate::font_menu::wheel_field_layer_for_test(&ctx, viewport, field).is_none(),
"the hidden bar's field at {field:?} still takes the wheel"
);
}
/// While a Type tool edits type, the Fill chip shows the selected characters' fill, and "?" where
/// their fills differ.
#[test]
fn the_type_tools_fill_chip_shows_the_edited_characters_fill() {
let mut app = app();
let t = edited_type(&mut app, "type");
app.session.set_tool_option("select", &json!({"start": 2, "end": 5}));
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
let fills: Vec<_> = match kind(&app, t) {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.fill.clone())).collect(),
k => panic!("{k:?}"),
};
let [(he, he_fill), (llo, llo_fill)] = &fills[..] else { panic!("{fills:?}") };
assert_eq!((he.as_str(), llo.as_str()), ("He", "llo"));
assert_ne!(he_fill, llo_fill);
let ctx = egui::Context::default();
let (fill, _, mixed, _) = canvas::chip_paints(&app, &ctx);
assert!(fill == *llo_fill && !mixed, "{fill:?} {mixed}");
app.session.set_tool_option("select", &json!({"start": 0, "end": 2}));
let (fill, _, mixed, _) = canvas::chip_paints(&app, &ctx);
assert!(fill == *he_fill && !mixed, "{fill:?} {mixed}");
app.session.set_tool_option("select", &json!({"start": 0, "end": 5}));
let (_, _, mixed, _) = canvas::chip_paints(&app, &ctx);
assert!(mixed, "the fills differ");
}
/// The task bar shows the selection as it is after an edit and after Undo: Join leaves once the
/// open path is closed and comes back with Undo, and Shape Properties shows a rectangle's new
/// corner radius and then its old one.
+1 -1
View File
@@ -45,7 +45,7 @@ pub use drag::{
};
#[cfg(test)]
use dropdowns::combo_button_id;
pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named, field_focused_in, popover_is_open};
pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field_named, combo_named, field_focused_in, popover_is_open};
pub use dropdowns::{
dropdown, dropdown_icon_names, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style,
menu_room, menu_scroll, panel_scroll, popover, set_menu_floor, strip_scroll,
+13 -1
View File
@@ -196,6 +196,18 @@ pub(crate) fn combo_menu_field<R>(
current: &str,
width: f32,
menu: impl FnOnce(craft_ui::combo::Menu<'_>) -> Option<R>,
) -> (Response, Option<R>) {
combo_menu_field_named(ui, id, None, current, width, menu)
}
/// [`combo_menu_field`] for a field whose visible label is not an adjacent label widget.
pub(crate) fn combo_menu_field_named<R>(
ui: &mut Ui,
id: impl std::hash::Hash,
name: Option<&str>,
current: &str,
width: f32,
menu: impl FnOnce(craft_ui::combo::Menu<'_>) -> Option<R>,
) -> (Response, Option<R>) {
let t = Tokens::get(ui.ctx());
let label = take_previous_label(ui);
@@ -205,7 +217,7 @@ pub(crate) fn combo_menu_field<R>(
.corner_radius(CornerRadius::same(3))
.show(ui, |ui| {
let armed = take_armed_focus(ui, &id);
let resp = combo_field(ui, id_with(ui.scope_id(), id), label, None, current, width - 4.0);
let resp = combo_field(ui, id_with(ui.scope_id(), id), label, name, current, width - 4.0);
if armed {
egui::Popup::open_id(ui.ctx(), egui::Popup::default_response_id(&resp));
ui.memory_mut(|m| m.stop_text_input());
+15 -9
View File
@@ -90,18 +90,24 @@ fn a_font_field_that_appears_under_a_still_pointer_leaves_the_wheel_until_the_po
#[test]
fn artboard_controls_leave_replaced_font_fields_wheel_available() {
let (ctx, mut app) = selected_type_with_control();
let mut texts = Vec::new();
for _ in 0..8 {
texts = frame(&mut app, &ctx, vec![]);
}
let fields: Vec<_> = texts.iter().filter(|(t, _)| t == "Inter").map(|(_, point)| *point).collect();
assert_eq!(fields.len(), 2, "Control and Properties each show a font field");
app.select_tool("artboard");
let laid_out = || {
let (ctx, mut app) = selected_type_with_control();
let mut texts = Vec::new();
for _ in 0..8 {
texts = frame(&mut app, &ctx, vec![]);
}
let fields: Vec<_> = texts.iter().filter(|(t, _)| t == "Inter").map(|(_, point)| *point).collect();
(ctx, app, fields)
};
let (_, _, fields) = laid_out();
assert_eq!(fields.len(), 3, "Control, Properties and the Contextual Task Bar each show a font field");
// Each field gets its own app, so a wheel stream begun over one field cannot decide another.
for point in fields {
let (ctx, mut app, _) = laid_out();
app.select_tool("artboard");
let mut raw = wheel_input(point);
app.raw_input_hook(&ctx, &mut raw);
assert!(raw.events.iter().any(|e| matches!(e, Event::MouseWheel { .. })), "the Artboard tool replaces this font field");
assert!(raw.events.iter().any(|e| matches!(e, Event::MouseWheel { .. })), "the Artboard tool replaces the font field at {point:?}");
}
}
+2 -1
View File
@@ -833,11 +833,12 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Window › SVG Interactivity attaches JavaScript to the selected objects' events (`onclick`, `onmouseover` and 17 more) and links JavaScript files to the document (Link JavaScript Files…); SVG export writes the events as event attributes and the files as `<script xlink:href>`, SVG import reads both back as text without running them, and the native format keeps them (`svgInteractivity.*`).
- 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 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 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/)).
- The contextual task bar offers more next steps by selection (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>): an open path Join and Edit Path (the Direct Selection tool), a closed path Edit Path, a compound path Edit Path and Release Compound Path, a clipping group Edit Clipping Path and Release Clipping Mask instead of Ungroup and Isolate, an image selected with vector art Make Clipping Mask instead of Unite, point or area type Convert To Area Type or Convert To Point Type beside Create Outlines, and type on a path, for which the overview names an item to add an effect and rearrange the text, Type on a Path Options (`type.pathOptions`; its options: <https://helpx.adobe.com/illustrator/using/creating-type-path.html>) beside Create Outlines. Evidence: `crates/ui-egui/src/tests/taskbar.rs`.
- While a Type tool (Type, Area Type, Type on a Path and their vertical versions) adds or edits type, the contextual task bar shows the font family menu, the font style menu, the Font Size combo and the Fill chip (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>); the Touch Type tool shows no bar, as the same page describes. Type selected with a selection tool shows the same three font controls beside its Create Outlines, conversion and Type on a Path items (<https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/font-options-overview.html>). The controls act as the Character panel's do: on the characters a Type tool selects, else on every selected type object, with the size left blank where the sizes differ. VectorCraft's own rules: a Type tool's bar leaves out the object actions, the Stroke chip, Recolor Artwork and Lock; a Type tool shows no bar before it adds type or with other art selected; type selected with other art shows no font controls; the font controls go on a second row when one row would be wider than the canvas, and narrow to fit a canvas narrower than they are; while a Type tool edits type, the Fill chip shows the edited characters' fill ("?" where it differs). Evidence: `the_type_tools_show_font_and_fill_controls`, `the_type_tools_show_no_bar_without_type`, `selected_type_shows_font_controls_beside_its_actions`, `the_task_bars_font_size_sets_what_the_character_panel_would`, `the_type_tools_fill_chip_activates_the_fill`, `the_type_tools_bar_is_named_and_stays_on_the_canvas`, `selected_types_font_controls_take_a_second_row_on_a_narrow_canvas` and `the_type_tools_fill_chip_shows_the_edited_characters_fill` 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`.
- 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, Corner and Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons. As in Illustrator, whose bar has 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)). Not done: Simplify, which the bar offers only while no anchors are selected, and the bar's Smooth item that adjusts a path's curvature, a command the app doesn't have (Object › Path › Smooth).