Merge pull request #5847 from ZIDAN44/fix/5372-control-bar-paragraph

Control bar: Paragraph options while type is selected (#5372)
This commit is contained in:
BFlat
2026-11-06 05:03:39 +07:00
committed by GitHub
35 changed files with 410 additions and 29 deletions
+85 -22
View File
@@ -172,13 +172,95 @@ fn inline_label(t: &Tokens, lbl: &str) -> egui::RichText {
/// language, the four fields, the W/H link and the item spacing ahead of each of them. With less,
/// only the Transform link shows.
fn inline_transform_width(ui: &Ui, t: &Tokens) -> f32 {
let measure = |text: egui::WidgetText| text.into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x;
let labels: f32 = INLINE_FIELDS.iter().map(|(_, lbl)| measure(inline_label(t, lbl).into())).sum();
let labels: f32 = INLINE_FIELDS.iter().map(|(_, lbl)| widgets::text_width(ui, inline_label(t, lbl))).sum();
let fields = INLINE_FIELD_WIDTH * INLINE_FIELDS.len() as f32;
let items = 2 * INLINE_FIELDS.len() + 1;
labels + fields + crate::panels::transform::CONSTRAIN_LINK_SIZE + ui.spacing().item_spacing.x * items as f32
}
/// The size of the Control bar's Align, Distribute and area type vertical alignment buttons.
const BUTTON: f32 = 24.0;
/// The space ahead of the separator that ends a group of the Control bar's controls.
const GROUP_GAP: f32 = 4.0;
/// The width of the separators of the Control bar's [`group_end`] and ahead of the Transform link.
const SEPARATOR: f32 = 6.0;
/// The widths of the Document Setup and Preferences buttons, shown with nothing selected.
const DOCUMENT_SETUP_WIDTH: f32 = 112.0;
const PREFERENCES_WIDTH: f32 = 90.0;
/// The room `n` of the bar's buttons take, with the spacing after each.
fn buttons_width(ui: &Ui, n: usize) -> f32 {
(BUTTON + ui.spacing().item_spacing.x) * n as f32
}
/// The separator that ends a group of the Control bar's controls, as [`group_end_width`] measures it.
fn group_end(ui: &mut Ui) {
ui.add_space(GROUP_GAP);
separator(ui);
}
fn group_end_width(ui: &Ui) -> f32 {
GROUP_GAP + separator_width(ui)
}
/// A separator [`SEPARATOR`] wide.
fn separator(ui: &mut Ui) {
ui.add(egui::Separator::default().spacing(SEPARATOR));
}
fn separator_width(ui: &Ui) -> f32 {
SEPARATOR + ui.spacing().item_spacing.x
}
/// The room the controls at the end of the Control bar need: the Transform link, or Document Setup
/// and Preferences with nothing selected. The groups ahead of them (the Paragraph section, area
/// type's vertical alignment, the Align and Distribute buttons) show only while it is left, so
/// the bar trims them from its right end (#5372), after the inline X/Y/W/H fields.
fn end_width(ui: &Ui, nothing_selected: bool) -> f32 {
if nothing_selected { DOCUMENT_SETUP_WIDTH + ui.spacing().item_spacing.x + PREFERENCES_WIDTH } else { crate::panels::transform::link_width(ui) }
}
/// Draws the Control bar's groups between its Character controls and the Transform link, and
/// returns whether the Transform link and X/Y/W/H follow (false: nothing is selected, and Document
/// Setup and Preferences, drawn here, end the bar). The groups are the Paragraph section and area
/// type's vertical alignment (#3759), then the Align buttons and, for two or more objects, the
/// Distribute buttons (a single object keeps the bar as it was); each shows only while the end of
/// the bar keeps its room.
fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize) -> bool {
let nothing = objects == 0;
if crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end_width(ui, nothing)) {
group_end(ui);
}
if crate::panels::properties::vertical_align::shown(app).is_some()
&& ui.available_width() >= buttons_width(ui, 4) + group_end_width(ui) + end_width(ui, nothing)
&& crate::panels::properties::vertical_align::buttons(app, ui, BUTTON)
{
group_end(ui);
}
if nothing {
if widgets::flat_button(ui, tl!("Document Setup"), DOCUMENT_SETUP_WIDTH).clicked() {
app.run("file.documentSetup", json!({})).ok();
}
if widgets::flat_button(ui, tl!("Preferences"), PREFERENCES_WIDTH).clicked() {
app.run("edit.preferences", json!({})).ok();
}
return false;
}
let end = separator_width(ui) + end_width(ui, false);
if ui.available_width() >= buttons_width(ui, 7) + end {
crate::panels::align::align_buttons(app, ui, BUTTON);
if objects > 1 && ui.available_width() >= GROUP_GAP + buttons_width(ui, 6) + end {
ui.add_space(GROUP_GAP);
crate::panels::align::distribute_buttons(app, ui, BUTTON);
}
separator(ui);
}
true
}
/// The Control bar (Window → Control), context-sensitive like Illustrator's.
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
@@ -283,28 +365,9 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
}
ui.separator();
crate::panels::character::control_bar(app, ui);
// Area type: its vertical alignment in the frame (#3759).
if crate::panels::properties::vertical_align::buttons(app, ui, 24.0) {
ui.add_space(4.0);
ui.separator();
}
if sel.is_empty() {
if widgets::flat_button(ui, tl!("Document Setup"), 112.0).clicked() {
app.run("file.documentSetup", json!({})).ok();
}
if widgets::flat_button(ui, tl!("Preferences"), 90.0).clicked() {
app.run("edit.preferences", json!({})).ok();
}
if !groups_before_transform(app, ui, sel.len()) {
return;
}
crate::panels::align::align_buttons(app, ui, 24.0);
// Two or more objects get the six Distribute Objects buttons too; a single
// object keeps the bar as it was.
if sel.len() > 1 {
ui.add_space(4.0);
crate::panels::align::distribute_buttons(app, ui, 24.0);
}
ui.separator();
// 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);
+46 -1
View File
@@ -1939,7 +1939,8 @@ fn the_control_bar_shows_the_global_edit_options() {
/// At any width the Control bar shows the inline X, Y, W and H fields whole inside its right margin,
/// or leaves all four out and keeps the Transform link; a wider bar never leaves them out once a
/// narrower one showed them.
/// narrower one showed them. The Align buttons, and for two objects the Distribute buttons, are
/// trimmed the same way ahead of the Transform link (#5372).
#[test]
fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
crate::i18n::set_current(crate::i18n::Lang::EN);
@@ -1954,6 +1955,33 @@ fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
ctx.enable_accesskit();
crate::theme::install_fonts(&ctx);
let mut seen = false;
// The Align group, then for two objects the Distribute group (#5372): each by its controls.
let mut groups: Vec<(&str, Vec<&str>)> = vec![(
"Align",
vec![
"Horizontal Align Left",
"Horizontal Align Center",
"Horizontal Align Right",
"Horizontal & Vertical Align Center",
"Vertical Align Top",
"Vertical Align Center",
"Vertical Align Bottom",
],
)];
if count == 2 {
groups.push((
"Distribute",
vec![
"Vertical Distribute Top",
"Vertical Distribute Center",
"Vertical Distribute Bottom",
"Horizontal Distribute Left",
"Horizontal Distribute Center",
"Horizontal Distribute Right",
],
));
}
let mut groups_seen = vec![false; groups.len()];
for width in (600..=2400).step_by(10) {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width as f32, 300.0));
let mut nodes = Vec::new();
@@ -1980,7 +2008,24 @@ fn the_control_bar_shows_the_inline_transform_fields_only_when_they_fit() {
}
assert!(!seen || fields[3].is_some(), "{count} objects, {width}: left out after a narrower bar showed them");
seen |= fields[3].is_some();
// The Align and Distribute buttons show whole inside the margin, with the Transform
// link after them, or are left out; once shown, a wider bar keeps them.
let end = |name: &str| nodes.iter().find(|(_, n)| n.label() == Some(name)).and_then(|(_, n)| n.bounds()).map(|b| b.x1);
let inside = |x: f64| x <= f64::from(width) - 10.0;
for ((group, names), was) in groups.iter().zip(groups_seen.iter_mut()) {
let ends: Vec<Option<f64>> = names.iter().map(|n| end(n)).collect();
let shown = ends.iter().all(Option::is_some);
assert!(shown || ends.iter().all(Option::is_none), "{count} objects, {width}: {group} {ends:?}");
if let Some(Some(last)) = ends.last() {
assert!(inside(*last), "{count} objects, {width}: {group} ends at {last}");
let transform = end("Transform");
assert!(transform.is_some_and(inside), "{count} objects, {width}: Transform after {group} ends at {transform:?}");
}
assert!(!*was || shown, "{count} objects, {width}: {group} left out after a narrower bar showed it");
*was |= shown;
}
}
assert!(seen, "{count} objects: a wide bar has room for the fields");
assert!(groups_seen.iter().all(|s| *s), "{count} objects: a wide bar shows every group: {groups_seen:?}");
}
}
+2
View File
@@ -3207,7 +3207,9 @@ digits Malayalam Малаялам
Patterns Мостри
Stroke Options Опции на щриха
Character: Знак:
Paragraph: Абзац:
Character options Опции на знака
Paragraph options Опции за абзац
Stroke options Опции на щриха
Transform options Опции за трансформации
Stroke Weight Дебелина на щриха
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1469,7 +1469,9 @@ digits Malayalam Malajalam
Standard Vertical Roman Alignment Standardno okomito latinično poravnanje
Strikethrough Precrtano
Character: Znak:
Paragraph: Pasus:
Character options Opcije znakova
Paragraph options Opcije pasusa
Subscript Indeks
Subscript (on the roadmap) Indeks (u planu)
Superscript Eksponent
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -615,12 +615,14 @@
Character Alignment Alineació de caràcters
Character Direction Direcció de caràcters
Character options Opcions de caràcter
Paragraph options Opcions de paràgraf
Character Rotation Rotació de caràcters
Character Set: Joc de caràcters:
Character Style Estil de caràcter
Character Style Options Opcions de l’estil de caràcter
Character Styles Estils de caràcter
Character: Caràcter:
Paragraph: Paràgraf:
Charcoal Carbó
Charcoal… Carbó…
Check an artboard to save. Marca un tauler per desar.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1493,7 +1493,9 @@ digits Malayalam Malajálamské
Standard Vertical Roman Alignment Standardní svislé zarovnání latinky
Strikethrough Přeškrtnutí
Character: Znak:
Paragraph: Odstavec:
Character options Možnosti znaků
Paragraph options Možnosti odstavce
Subscript Dolní index
Subscript (on the roadmap) Dolní index (v plánu)
Superscript Horní index
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1403,7 +1403,9 @@ digits Malayalam Malayalam
Standard Vertical Roman Alignment Standard lodret romersk justering
Strikethrough Gennemstreget
Character: Tegn:
Paragraph: Afsnit:
Character options Tegnindstillinger
Paragraph options Afsnitsindstillinger
Subscript Sænket skrift
Subscript (on the roadmap) Sænket skrift (på vejkortet)
Superscript Hævet skrift
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3252,7 +3252,9 @@ effect Points Punkte
Patterns Muster
Stroke Options Konturoptionen
Character: Zeichen:
Paragraph: Absatz:
Character options Zeichenoptionen
Paragraph options Absatzoptionen
Stroke options Konturoptionen
Transform options Transformationsoptionen
Stroke Weight Konturstärke
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1991,6 +1991,7 @@ font-technology Variable Μεταβλητές
Patterns Μοτίβα
Stroke Options Επιλογές περιγράμματος
Character: Χαρακτήρας:
Paragraph: Παράγραφος:
Stroke: Περίγραμμα:
Style Στυλ
Style: Στυλ:
@@ -3655,6 +3656,7 @@ digits Malayalam Μαλαγιάλαμ
Stray Points Μεμονωμένα σημεία
Stroke (X), double-click for the Color Picker Περίγραμμα (X), διπλό κλικ για επιλογέα χρώματος
Character options Επιλογές χαρακτήρων
Paragraph options Επιλογές παραγράφου
Stroke options Επιλογές περιγράμματος
Transform options Επιλογές μετασχηματισμού
Stroke Weight Πάχος περιγράμματος
Can't render this file because it contains an unexpected character in line 163 and column 5.
+2
View File
@@ -3291,7 +3291,9 @@ digits Malayalam Malayálam
Patterns Motivos
Stroke Options Opciones de trazo
Character: Carácter:
Paragraph: Párrafo:
Character options Opciones de carácter
Paragraph options Opciones de párrafo
Stroke options Opciones de trazo
Transform options Opciones de transformación
Stroke Weight Grosor de trazo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1466,7 +1466,9 @@ digits Malayalam Malajalam
Standard Vertical Roman Alignment Tavallinen pystysuuntainen latinalainen tasaus
Strikethrough Yliviivaus
Character: Merkki:
Paragraph: Kappale:
Character options Merkkiasetukset
Paragraph options Kappaleasetukset
Subscript Alaindeksi
Subscript (on the roadmap) Alaindeksi (suunnitteilla)
Superscript Yläindeksi
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3283,7 +3283,9 @@ digits Malayalam Malayalam
Patterns Motifs
Stroke Options Options de contour
Character: Caractère :
Paragraph: Paragraphe :
Character options Options de caractère
Paragraph options Options de paragraphe
Stroke options Options de contour
Transform options Options de transformation
Stroke Weight Épaisseur du contour
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -554,12 +554,14 @@ font-technology Variable Változó
Character / Paragraph Karakter / bekezdés
Character Alignment Karakterigazítás
Character options Karakterbeállítások
Paragraph options Bekezdésbeállítások
Character Rotation Karakterforgatás
Character Set: Karakterkészlet:
Character Style Karakterstílus
Character Style Options Karakterstílus beállításai
Character Styles Karakterstílusok
Character: Karakter:
Paragraph: Bekezdés:
Check Links Csatolások ellenőrzése
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Szkript ellenőrzése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1403,7 +1403,9 @@ digits Malayalam Malayalam
Standard Vertical Roman Alignment Perataan Romawi vertikal standar
Strikethrough Coret
Character: Karakter:
Paragraph: Paragraf:
Character options Opsi karakter
Paragraph options Opsi paragraf
Subscript Subskrip
Subscript (on the roadmap) Subskrip (masih dalam roadmap)
Superscript Superskrip
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1403
1404
1405
1406
1407
1408
1409
1410
1411
+2
View File
@@ -3327,7 +3327,9 @@ digits Malayalam Malayalam
Patterns Motivi
Stroke Options Opzioni traccia
Character: Carattere:
Paragraph: Paragrafo:
Character options Opzioni carattere
Paragraph options Opzioni di paragrafo
Stroke options Opzioni traccia
Transform options Opzioni di trasformazione
Stroke Weight Spessore traccia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3090,7 +3090,9 @@ mark-style Japanese 日本式
Strikethrough 打ち消し線
Stroke Options 線オプション
Character: 文字:
Paragraph: 段落:
Character options 文字オプション
Paragraph options 段落オプション
Stroke options 線オプション
Transform options 変形オプション
Stroke: 線:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1329,7 +1329,9 @@ digits Malayalam მალაიალამური
Standard Vertical Roman Alignment სტანდარტული ვერტიკალური ლათინური სწორება
Strikethrough გადახაზული
Character: სიმბოლო:
Paragraph: აბზაცი:
Character options სიმბოლოს პარამეტრები
Paragraph options აბზაცის პარამეტრები
Subscript ქვედა ინდექსი
Subscript (on the roadmap) ქვედა ინდექსი (სამომავლო გეგმაშია)
Superscript ზედა ინდექსი
Can't render this file because it is too large.
+2
View File
@@ -3079,7 +3079,9 @@ mark-style Japanese 일본식
Strikethrough 취소선
Stroke Options 선 옵션
Character: 문자:
Paragraph: 단락:
Character options 문자 옵션
Paragraph options 단락 옵션
Stroke options 선 옵션
Transform options 변형 옵션
Stroke: 선:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1417,7 +1417,9 @@ digits Malayalam Malayalam
Standard Vertical Roman Alignment Standaard verticale Romeinse uitlijning
Strikethrough Doorhalen
Character: Teken:
Paragraph: Alinea:
Character options Tekenopties
Paragraph options Alineaopties
Subscript Subscript
Subscript (on the roadmap) Subscript (op de roadmap)
Superscript Superscript
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1406,7 +1406,9 @@ digits Malayalam Malajalam
Standard Vertical Roman Alignment Standardowe pionowe wyrównanie łacińskie
Strikethrough Przekreślenie
Character: Znak:
Paragraph: Akapit:
Character options Opcje znaków
Paragraph options Opcje akapitu
Subscript Indeks dolny
Subscript (on the roadmap) Indeks dolny (w planach)
Superscript Indeks górny
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2748,7 +2748,9 @@ digits Malayalam Malaiala
Stroke Options Opções de Traçado
Stroke Weight Peso do Traçado
Character: Caractere:
Paragraph: Parágrafo:
Character options Opções de caractere
Paragraph options Opções de parágrafo
Stroke options Opções de traçado
Transform options Opções de transformação
Stroke: Traçado:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -3322,7 +3322,9 @@ digits Malayalam Малаялам
Patterns Узоры
Stroke Options Параметры обводки
Character: Символ:
Paragraph: Абзац:
Character options Параметры символа
Paragraph options Параметры абзаца
Stroke options Параметры обводки
Transform options Параметры трансформирования
Stroke Weight Толщина обводки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3213,7 +3213,9 @@ digits Malayalam Malayalam
Patterns Mönster
Stroke Options Konturalternativ
Character: Tecken:
Paragraph: Stycke:
Character options Teckenalternativ
Paragraph options Styckealternativ
Stroke options Konturalternativ
Transform options Transformeringsalternativ
Stroke Weight Konturtjocklek
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -3115,7 +3115,9 @@ digits Malayalam Malayalam
Patterns Desenler
Stroke Options Kontur Seçenekleri
Character: Karakter:
Paragraph: Paragraf:
Character options Karakter seçenekleri
Paragraph options Paragraf seçenekleri
Stroke options Kontur seçenekleri
Transform options Dönüştürme seçenekleri
Stroke Weight Kontur Kalınlığı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -3320,7 +3320,9 @@ effect Points Точки
Patterns Візерунки
Stroke Options Параметри обведення
Character: Символ:
Paragraph: Абзац:
Character options Параметри символу
Paragraph options Параметри абзацу
Stroke options Параметри обведення
Transform options Параметри трансформування
Stroke Weight Товщина обведення
Can't render this file because it contains an unexpected character in line 177 and column 5.
+2
View File
@@ -468,12 +468,14 @@ font-technology Variable Biến thiên
Character / Paragraph Ký tự / Đoạn văn
Character Alignment Căn chỉnh ký tự
Character options Tùy chọn ký tự
Paragraph options Tùy chọn đoạn văn
Character Rotation Xoay ký tự
Character Set: Bộ ký tự:
Character Style Kiểu ký tự
Character Style Options Tùy chọn kiểu ký tự
Character Styles Các kiểu ký tự
Character: Ký tự:
Paragraph: Đoạn văn:
Check Links Kiểm tra liên kết
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Kiểm tra script
Can't render this file because it contains an unexpected character in line 199 and column 5.
+2
View File
@@ -3158,7 +3158,9 @@ digits Malayalam 马拉雅拉姆数字
Patterns 图案
Stroke Options 描边选项
Character: 字符:
Paragraph: 段落:
Character options 字符选项
Paragraph options 段落选项
Stroke options 描边选项
Transform options 变换选项
Stroke Weight 描边粗细
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4185,7 +4185,9 @@ digits Malayalam 馬來亞拉姆數字
Patterns 圖樣
Stroke Options 筆畫選項
Character: 字元:
Paragraph: 段落:
Character options 字元選項
Paragraph options 段落選項
Stroke options 筆畫選項
Transform options 變形選項
Stroke Weight 筆畫寬度
Can't render this file because it contains an unexpected character in line 6 and column 47.
+53 -1
View File
@@ -380,14 +380,66 @@ fn selected_alignment(app: &VectorcraftApp, fallback: &ParaStyle) -> Option<Just
/// The seven alignment buttons shared by Paragraph and Properties; no button is selected
/// when the paragraphs they act on have differing displayed alignments.
pub fn alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32) {
first_alignment_buttons(app, ui, para, size, ALIGNMENTS.len());
}
/// The first `count` of the [`ALIGNMENTS`] buttons.
fn first_alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32, count: usize) {
let shown = selected_alignment(app, para);
for (j, icon, tip, id) in ALIGNMENTS {
for (j, icon, tip, id) in ALIGNMENTS.into_iter().take(count) {
if widgets::icon_button(ui, icon, tip, shown == Some(j), size).clicked() {
para_cmd(app, "text.setStyle", json!({"justify": id}));
}
}
}
/// The Control bar's alignment buttons: Align left, Align center and Align right. The public
/// documentation has the Control panel format paragraphs while type is selected, with a way to the
/// Paragraph panel (<https://helpx.adobe.com/illustrator/using/formatting-paragraphs.html>), and
/// its alignment buttons align text as the Paragraph panel's do
/// (<https://helpx.adobe.com/illustrator/using/align-text.html>). The bar keeps to the three that
/// don't justify, for its room; all seven are in the Paragraph options the link opens.
const CONTROL_BAR_ALIGNMENTS: usize = 3;
/// The spacing between the Control bar's alignment buttons, as in the Paragraph panel's row.
const CONTROL_BAR_BUTTON_SPACING: f32 = 3.0;
/// The width of the Paragraph panel in the Control bar's popover, as the Character panel's.
const CONTROL_BAR_POPOVER_WIDTH: f32 = 260.0;
/// The Control bar's Paragraph link, as it is drawn and measured.
fn control_bar_link_text(ui: &Ui) -> egui::RichText {
egui::RichText::new(tl!("Paragraph:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline()
}
/// The Control bar's paragraph controls while type is selected or edited (#5372), after its
/// Character controls: the Paragraph link, which opens the Paragraph panel in a popover under it,
/// and the Align left, Align center and Align right buttons, `size` points square as the bar's others. `reserve` gives the room the controls
/// after them keep, asked only when type is selected: short of room, the buttons are left out,
/// then the link too. Returns whether the link shows.
pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui, size: f32, reserve: impl FnOnce(&Ui) -> f32) -> bool {
let Some((_, para)) = text_style(app) else { return false };
let text = control_bar_link_text(ui);
let link_width = widgets::text_width(ui, text.clone());
let room = ui.available_width() - reserve(ui);
let link_room = link_width + ui.spacing().item_spacing.x;
if room < link_room {
return false;
}
let link = ui.link(text).on_hover_text(tl!("Paragraph options"));
widgets::popover(&link, link.clicked(), |ui| {
ui.set_width(CONTROL_BAR_POPOVER_WIDTH);
show(app, ui);
});
if room >= link_room + (size + CONTROL_BAR_BUTTON_SPACING) * CONTROL_BAR_ALIGNMENTS as f32 {
let spacing = ui.spacing().item_spacing.x;
ui.spacing_mut().item_spacing.x = CONTROL_BAR_BUTTON_SPACING;
first_alignment_buttons(app, ui, &para, size, CONTROL_BAR_ALIGNMENTS);
ui.spacing_mut().item_spacing.x = spacing;
}
true
}
/// Type › Bullets and Numbering › Apply Bullets (`type.applyBullets {bullet?}`): the paragraphs of
/// the selected type, or those the Type tool's selection touches, become a bulleted list with
/// `bullet`; without it, a list keeps its bullet and other paragraphs take the dot.
@@ -543,3 +543,150 @@ fn reset_panel_resets_the_hyphenation_options() {
assert_eq!(serde_json::to_value(&para).unwrap().get("justification"), None, "saves no justification options");
}
}
type Nodes = Vec<(egui::accesskit::NodeId, egui::accesskit::Node)>;
/// An English context for headless Control bar frames, with AccessKit on to find its controls.
fn control_bar_ctx() -> egui::Context {
crate::i18n::set_current(crate::i18n::Lang::EN);
let ctx = egui::Context::default();
ctx.enable_accesskit();
crate::theme::install_fonts(&ctx);
ctx
}
/// A document with the type `texts` (`text.create` params) makes, all of it selected.
fn control_bar_app(texts: &[Value]) -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 800, "height": 400})).unwrap();
let ids: Vec<Value> = texts.iter().map(|p| app.run("text.create", p.clone()).unwrap()["id"].clone()).collect();
app.run("select.set", json!({ "ids": ids })).unwrap();
app
}
/// The second of two headless frames of the whole Control bar, `width` wide (the first lays it out).
fn control_bar_nodes(ctx: &egui::Context, app: &mut VectorcraftApp, width: f32, events: Vec<egui::Event>) -> Nodes {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(width, 300.0));
let mut nodes = Vec::new();
for events in [events, vec![]] {
let mut out =
ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), ..Default::default() }, |ui| crate::chrome::control_bar(app, ui));
out.textures_delta.clear();
nodes = out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default();
}
nodes
}
/// The control named `name` (a button's or link's label, a label's text).
fn control_bar_node(nodes: &Nodes, name: &str) -> Option<egui::accesskit::Node> {
nodes.iter().find(|(_, n)| n.label() == Some(name) || n.value() == Some(name)).map(|(_, n)| n.clone())
}
/// Where the control named `name` ends, if it shows.
fn control_bar_end(nodes: &Nodes, name: &str) -> Option<f64> {
control_bar_node(nodes, name).and_then(|n| n.bounds()).map(|b| b.x1)
}
/// The Control bar's Paragraph alignment buttons.
const BAR_ALIGNMENTS: [&str; 3] = ["Align left", "Align center", "Align right"];
/// #5372: with type selected, the Control bar shows a Paragraph section after the Character one, as
/// the public documentation's Control panel does: a Paragraph: link that opens the Paragraph panel
/// in a popover under it, and the Align left, Align center and Align right buttons. A click aligns
/// the selected type, and the button of its alignment shows selected. The buttons show on a bar of
/// common window widths.
#[test]
fn the_control_bar_shows_the_paragraph_alignment_of_the_selected_type() {
let ctx = control_bar_ctx();
let mut app = control_bar_app(&[json!({"x": 20, "y": 50, "text": "Point"})]);
// Pinned, so that the room the buttons need can't creep up past common window widths.
for width in [1440.0, 1920.0] {
let nodes = control_bar_nodes(&ctx, &mut app, width, vec![]);
let ends = BAR_ALIGNMENTS.map(|n| control_bar_end(&nodes, n));
assert!(ends.iter().all(|e| e.is_some_and(|x| x <= f64::from(width) - 10.0)), "pinned: the buttons show on a {width} pt window: {ends:?}");
}
let width = 1920.0;
let nodes = control_bar_nodes(&ctx, &mut app, width, vec![]);
let center = |n: &egui::accesskit::Node| {
let b = n.bounds().unwrap_or_else(|| panic!("{n:?}"));
egui::pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
};
let click = |app: &mut VectorcraftApp, at: egui::Pos2| {
let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
control_bar_nodes(&ctx, app, width, vec![egui::Event::PointerMoved(at), press(true)]);
control_bar_nodes(&ctx, app, width, vec![press(false)]);
control_bar_nodes(&ctx, app, width, vec![])
};
// A selected button shows pressed.
let on = |n: &egui::accesskit::Node| n.toggled() == Some(egui::accesskit::Toggled::True);
let button = control_bar_node(&nodes, "Align center").unwrap_or_else(|| panic!("the Control bar has Align center"));
assert!(!on(&button), "left-aligned type: Align center is not selected");
assert!(control_bar_node(&nodes, "Align left").is_some_and(|n| on(&n)), "Align left shows selected");
let undo = app.session.active().unwrap().history.undo.len();
let nodes = click(&mut app, center(&button));
assert_eq!(text_style(&app).unwrap().1.justify, Justify::Center, "{}", app.ui.status);
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "one undo step");
assert!(control_bar_node(&nodes, "Align center").is_some_and(|n| on(&n)), "Align center shows selected");
assert!(!control_bar_node(&nodes, "Align left").is_some_and(|n| on(&n)), "Align left no longer selected");
// The Paragraph: link opens the Paragraph panel in a popover: its justifications show there,
// no panel docks, and a second click closes it.
let link = control_bar_node(&nodes, "Paragraph:").unwrap_or_else(|| panic!("the Paragraph: link"));
assert_eq!(link.description(), Some("Paragraph options"));
assert!(control_bar_node(&nodes, "Justify all lines").is_none(), "closed");
let nodes = click(&mut app, center(&link));
assert!(control_bar_node(&nodes, "Justify all lines").is_some(), "the popover shows the Paragraph panel");
assert_eq!(app.ui.open_panel, None, "no panel docks");
let nodes = click(&mut app, center(&link));
assert!(control_bar_node(&nodes, "Justify all lines").is_none(), "a second click closes it");
}
/// #5372: at any width the Control bar trims itself from its right end, ending inside its margin.
/// With type selected the inline X/Y/W/H fields go first, then the Distribute and Align buttons,
/// area type's vertical alignment, the Paragraph buttons and last the Paragraph: link, while the
/// Transform link keeps its room; a wider bar never leaves out what a narrower one showed. For one point
/// type object, area type (its vertical alignment) and two objects (the Distribute buttons).
#[test]
fn the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right() {
let ctx = control_bar_ctx();
let point = |x: i32| json!({"x": x, "y": 50, "text": "Point"});
let area = json!({"x": 20, "y": 20, "text": "Area", "area": {"width": 100, "height": 120}});
const VERTICAL: [&str; 4] = ["Align Top", "Align Center", "Align Bottom", "Justify Vertically"];
for (case, texts) in [("point type", vec![point(20)]), ("area type", vec![area]), ("two objects", vec![point(20), point(300)])] {
let mut app = control_bar_app(&texts);
// Each group of controls in the bar's order, by its first and last control.
let mut groups: Vec<(&str, Vec<&str>)> = vec![("link", vec!["Paragraph:"]), ("buttons", BAR_ALIGNMENTS.to_vec())];
if case == "area type" {
groups.push(("vertical alignment", VERTICAL.to_vec()));
}
groups.push(("Align", vec!["Horizontal Align Left", "Vertical Align Bottom"]));
if case == "two objects" {
groups.push(("Distribute", vec!["Vertical Distribute Top", "Horizontal Distribute Right"]));
}
groups.push(("X/Y/W/H", vec!["X:", "H:"]));
let mut seen = vec![false; groups.len()];
for width in (600..=2400).step_by(10) {
let nodes = control_bar_nodes(&ctx, &mut app, width as f32, vec![]);
let inside = |x: f64| x <= f64::from(width) - 10.0;
let shown: Vec<bool> = groups
.iter()
.map(|(group, names)| {
let ends: Vec<Option<f64>> = names.iter().map(|n| control_bar_end(&nodes, n)).collect();
assert!(ends.iter().all(Option::is_some) || ends.iter().all(Option::is_none), "{case}, {width}: {group} {ends:?}");
if let Some(Some(end)) = ends.last() {
assert!(inside(*end), "{case}, {width}: {group} ends at {end}");
}
ends.iter().all(Option::is_some)
})
.collect();
// Narrower bars are already full before the section (the Character controls).
let transform = control_bar_end(&nodes, "Transform");
assert!(!shown[0] || transform.is_some_and(inside), "{case}, {width}: Transform ends at {transform:?}");
for (i, ((group, _), on)) in groups.iter().zip(&shown).enumerate() {
assert!(!seen[i] || *on, "{case}, {width}: {group} left out after a narrower bar showed it");
assert!(!*on || shown[..i].iter().all(|s| *s), "{case}, {width}: {group} shows while a group before it is left out");
seen[i] |= on;
}
}
assert!(seen.iter().all(|s| *s), "{case}: a wide bar shows every group: {seen:?}");
}
}
+11 -2
View File
@@ -461,14 +461,23 @@ pub(crate) fn polygon_rows(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, live
/// The Control bar's underlined "Transform" link: a click toggles the Transform panel in a
/// popover under it, as the Stroke link does with the Stroke panel.
pub fn link(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let resp = ui.link(egui::RichText::new(tl!("Transform")).size(12.0).color(t.text).underline()).on_hover_text(tl!("Transform options"));
let resp = ui.link(link_text(ui)).on_hover_text(tl!("Transform options"));
widgets::popover(&resp, resp.clicked(), |ui| {
ui.set_width(300.0);
show(app, ui);
});
}
/// The Transform link's text, as [`link`] draws it and [`link_width`] measures it.
fn link_text(ui: &Ui) -> egui::RichText {
egui::RichText::new(tl!("Transform")).size(12.0).color(Tokens::get(ui.ctx()).text).underline()
}
/// The width of the Transform [`link`] in `ui`.
pub fn link_width(ui: &Ui) -> f32 {
widgets::text_width(ui, link_text(ui))
}
/// The width and height of [`constrain_link`]'s button.
pub const CONSTRAIN_LINK_SIZE: f32 = 22.0;
+1 -1
View File
@@ -49,7 +49,7 @@ pub use dropdowns::{
panel_scroll, popover, set_menu_floor, strip_scroll,
};
pub use labels::{dim_label, dim_name, divider, field_label, label_row, link_label, section_header, section_toggle, section_toggle_name, subheader};
pub(crate) use labels::{label_field, take_previous_label};
pub(crate) use labels::{label_field, take_previous_label, text_width};
pub(crate) use numeric::{ANY, round_to, set_bare_numbers_are_points, typed_unit};
pub use numeric::{
SpinPick, mixed_field, mixed_field_named, num_field, num_field_live, num_field_named, num_field_within, num_field_within_empty, opt_field,
+6
View File
@@ -1,5 +1,11 @@
use super::{Pos2, Rect, Response, Sense, Stroke, Tokens, Ui, icons, pos2, scrub, theme, vec2};
/// The width `text` takes on one line, as a label or link in `ui` draws it: its galley, which has
/// no padding around it.
pub(crate) fn text_width(ui: &Ui, text: egui::RichText) -> f32 {
egui::WidgetText::from(text).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Body).size().x
}
/// Bold section header (Properties panel).
pub fn section_header(ui: &mut Ui, text: &str) {
let t = Tokens::get(ui.ctx());
+6 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; Libraries panel colours keep their spot ink in every document, #5812; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Control bar Paragraph section with type selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -1672,6 +1672,10 @@ While a corner widget is pressed or dragged with the Selection or Direct Selecti
A double-click on a Repeat (Radial, Grid or Mirror) with the Selection tool enters isolation mode on it ([isolate objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/isolate-objects.html): a double-click with the Selection tool isolates an object; [repeat patterns overview](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/repeat-patterns-overview.html): every instance follows edits to the original object); there the source art is selected and edited as usual (a double-click on type in it switches to the Type tool on that text) and every copy follows, with the repeat's settings kept (#5054). Test: `tests::repeat_edit::a_double_click_on_a_repeat_isolates_it_to_edit_its_text`.
## Control bar: Paragraph section while type is selected (2026-11-06)
With type selected, the Control bar shows a Paragraph: link and the Align left, Align center and Align right buttons after its Character controls, as the public documentation has the Control panel format paragraphs while the Type tool is in use, with a way to the Paragraph panel ([Paragraph panel overview](https://helpx.adobe.com/illustrator/using/formatting-paragraphs.html)), and its alignment buttons align text as the Paragraph panel's do ([Align text](https://helpx.adobe.com/illustrator/using/align-text.html)). A button aligns the selected type (`text.setStyle {justify}`, one undo step) and the shared alignment shows selected; the link opens the Paragraph panel in a popover under it, as Character: opens the Character panel. Keeping to the three alignments that don't justify is our choice, for the bar's room; the popover has all seven. Short of room the bar leaves controls out from its right end: the inline X/Y/W/H fields, the Distribute and Align buttons, area type's vertical alignment, then these buttons and last the link, keeping room for the Transform link; on a 1440 pt bar with one point type object, the buttons show (pinned by a headless test, #5372).
## Area type: vertical alignment in the Properties and Control panels (2026-11-04)
With area type selected, the Properties panel's Paragraph section and the Control panel show Align Top, Align Center, Align Bottom and Justify Vertically buttons, as the public documentation places them ([Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html)). They set the type's vertical alignment in its frame (`text.areaOptions {verticalAlign}`, as Area Type Options does) on every selected frame in one undo step, each frame keeping its other options; the shared alignment shows selected, none when the frames differ (#3759). As that page limits vertical alignment to area type without rows, columns or threads, the buttons are turned off while a selected frame has rows or columns or is threaded; Area Type Options and `text.areaOptions` still align each row/column cell (a VectorCraft extension, unchanged here). Tests: `panels::properties::tests::vertical_alignment_buttons_align_the_selected_area_type`.
@@ -1684,6 +1688,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s
| Date | Change | Summary |
|---|---|---|
| 2026-11-06 | minor | Control bar: Paragraph: link and Align left/center/right while type is selected, trimmed from the bar's right end after X/Y/W/H and the Align buttons (#5372); evidence added; scores unchanged. |
| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. |
| 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. |
| 2026-11-05 | minor | View & navigation: document tabs are reordered by dragging them along the tab strip (`document.move`, #5663); evidence added; scores unchanged. |
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
## Feature submission evidence
@@ -359,6 +359,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Select › Start Global Edit / Stop Global Edit; Properties › Quick Actions with the Global Edit Options popover (Match: Appearance, Size; On Artboards: All Artboards or a range, Include Objects On Canvas; Result(s)); Escape stops it; the matched objects are highlighted in their layer colour while it runs; a Shift-click on a highlighted match leaves it out | partial | #3521, `select.globalEdit` / `select.globalEditOptions` / `select.globalEditExclude`, `crates/engine/src/tests/globaledit.rs`; paths, compound paths, symbol instances and groups of them (similar groups, #4939: edits inside one group reach the same object in the others; [public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html)); Shift-click to leave a match out ([public source](https://helpx.adobe.com/ph_fil/illustrator/how-to/global-editing-sensei.html)); the Control bar's Global Edit Options dropdown opens the same options; rotated copies and highlighting matches before the start remain ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)) |
| Contextual task bar | done | |
| Control bar with Transform and Stroke popovers | done | |
| Control bar: Paragraph: link and Align left/center/right while type is selected (#5372) | done | After the Character controls, as the public documentation has the Control panel format paragraphs ([Paragraph panel overview](https://helpx.adobe.com/illustrator/using/formatting-paragraphs.html), [Align text](https://helpx.adobe.com/illustrator/using/align-text.html)); the link opens the Paragraph panel in a popover, as Character: does with the Character panel. The bar trims from its right end: X/Y/W/H, Distribute, Align, area type vertical alignment, then these buttons, then the link. The three-button set is our choice for room; all seven alignments are in the popover. `panels/paragraph/tests.rs` headless tests (`the_control_bar_shows_the_paragraph_alignment_of_the_selected_type`, `the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right`) |
| Pathfinder shape results keep their source names and SVG IDs | done | #1293; Minus Front keeps the back name, Minus Back the front name; Unite/Intersect/Exclude use the backmost named input; compound output carries it once; geometry and paint unchanged |
| Attributes fill-rule controls with a compound member selected | done | Edits the compound's rule; direction still edits only the member; rendered-hole and Undo/Redo regression tests (#1197) |
| Make Compound Path on a group (such as outlined type) keeps its backmost path's paint; compound paths joined into one keep their holes | done | #1537 |
@@ -456,6 +457,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-11-06 | minor | Control bar: with type selected, a Paragraph: link (the Paragraph panel in a popover) and the Align left, Align center and Align right buttons follow the Character controls; the bar leaves them out from its right end when short of room, after X/Y/W/H and the Align buttons (#5372); scores and estimates unchanged |
| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. |
| 2026-11-04 | minor | Contour tool (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `object.contour` is the command behind it. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. |
| 2026-11-05 | minor | Document tabs are reordered by dragging them along the tab strip (`document.move`, #5663). |