Merge PR #6793: Type: paragraph direction buttons in the Paragraph panel and options bar

crates/ui-egui/src/i18n/*.tsv (20 catalogs): both sides appended rows at the same place; union, no duplicate keys.
crates/ui-egui/src/state.rs: both new ToolOptions fields (type_style_defaults, type_direction) and defaults.
crates/ui-egui/src/tool_reset.rs: Type tools reset both type_direction and type_style_defaults.
crates/ui-egui/src/type_tool.rs: new type is created from default_style_params (main); direction added to it, and apply_defaults stores a direction edit in type_direction.
docs/gaps.md: header and G8 evidence from main, G8 missing text reworded for direction buttons, change-log lines of both.
docs/scorecard.md: ours (regenerated at combine).
This commit is contained in:
aiongg
2026-10-22 02:25:37 +00:00
26 changed files with 234 additions and 4 deletions
+5
View File
@@ -3911,3 +3911,8 @@ navigator Zoom Zvětšení
# Graphics Adapter preference (#1885)
GPU adapter Adaptér GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatický směr odstavce
Left-to-right paragraph direction Směr odstavce zleva doprava
Right-to-left paragraph direction Směr odstavce zprava doleva
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3309,3 +3309,8 @@ curves Smooth Glätten
# Graphics Adapter preference (#1885)
GPU adapter GPU-Adapter
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatische Absatzrichtung
Left-to-right paragraph direction Absatzrichtung von links nach rechts
Right-to-left paragraph direction Absatzrichtung von rechts nach links
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4000,3 +4000,8 @@ curves Smooth Εξομάλυνση
# Graphics Adapter preference (#1885)
GPU adapter Προσαρμογέας GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Αυτόματη κατεύθυνση παραγράφου
Left-to-right paragraph direction Κατεύθυνση παραγράφου από αριστερά προς τα δεξιά
Right-to-left paragraph direction Κατεύθυνση παραγράφου από δεξιά προς τα αριστερά
Can't render this file because it contains an unexpected character in line 1030 and column 18.
+5
View File
@@ -4262,3 +4262,8 @@ navigator Zoom Zoom
# Graphics Adapter preference (#1885)
GPU adapter Adaptador de GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Dirección de párrafo automática
Left-to-right paragraph direction Dirección de párrafo de izquierda a derecha
Right-to-left paragraph direction Dirección de párrafo de derecha a izquierda
Can't render this file because it contains an unexpected character in line 4 and column 47.
+5
View File
@@ -4342,3 +4342,8 @@ layers Fill Remplissage
layers Fill: Remplissage :
navigator Zoom Zoom
shape Appearance Apparence
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Sens du paragraphe automatique
Left-to-right paragraph direction Sens du paragraphe de gauche à droite
Right-to-left paragraph direction Sens du paragraphe de droite à gauche
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4217,3 +4217,8 @@ cameraRaw Yellow Sárga
# Graphics Adapter preference (#1885)
GPU adapter GPU-adapter
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatikus bekezdésirány
Left-to-right paragraph direction Balról jobbra irányuló bekezdés
Right-to-left paragraph direction Jobbról balra irányuló bekezdés
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3298,3 +3298,8 @@ curves Smooth Haluskan
# Graphics Adapter preference (#1885)
GPU adapter Adaptor GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Arah paragraf otomatis
Left-to-right paragraph direction Arah paragraf kiri ke kanan
Right-to-left paragraph direction Arah paragraf kanan ke kiri
Can't render this file because it contains an unexpected character in line 4 and column 47.
+5
View File
@@ -4417,3 +4417,8 @@ shape Appearance Aspetto
# Graphics Adapter preference (#1885)
GPU adapter Adattatore GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Direzione del paragrafo automatica
Left-to-right paragraph direction Direzione del paragrafo da sinistra a destra
Right-to-left paragraph direction Direzione del paragrafo da destra a sinistra
Can't render this file because it contains an unexpected character in line 2660 and column 53.
+5
View File
@@ -3978,3 +3978,8 @@ curves Smooth スムーズ
# Graphics Adapter preference (#1885)
GPU adapter GPU アダプター
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction 段落の方向を自動判別
Left-to-right paragraph direction 左から右への段落方向
Right-to-left paragraph direction 右から左への段落方向
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3490,3 +3490,8 @@ curves Smooth 매끄럽게
# Graphics Adapter preference (#1885)
GPU adapter GPU 어댑터
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction 자동 단락 방향
Left-to-right paragraph direction 왼쪽에서 오른쪽 단락 방향
Right-to-left paragraph direction 오른쪽에서 왼쪽 단락 방향
Can't render this file because it contains an unexpected character in line 3174 and column 53.
+5
View File
@@ -4060,3 +4060,8 @@ curves Smooth Vloeiend maken
# Graphics Adapter preference (#1885)
GPU adapter GPU-adapter
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatische alinearichting
Left-to-right paragraph direction Alinearichting van links naar rechts
Right-to-left paragraph direction Alinearichting van rechts naar links
Can't render this file because it contains an unexpected character in line 8 and column 34.
+5
View File
@@ -3900,3 +3900,8 @@ curves Smooth Wygładź
# Graphics Adapter preference (#1885)
GPU adapter Karta GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatyczny kierunek akapitu
Left-to-right paragraph direction Kierunek akapitu od lewej do prawej
Right-to-left paragraph direction Kierunek akapitu od prawej do lewej
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4090,3 +4090,8 @@ layerLabel No Color Sem cor
# Graphics Adapter preference (#1885)
GPU adapter Adaptador de GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Direção automática do parágrafo
Left-to-right paragraph direction Direção do parágrafo da esquerda para a direita
Right-to-left paragraph direction Direção do parágrafo da direita para a esquerda
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4505,3 +4505,8 @@ layers Fill Заливка
layers Fill: Заливка:
navigator Zoom Масштаб
shape Appearance Оформление
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Автоматическое направление абзаца
Left-to-right paragraph direction Направление абзаца слева направо
Right-to-left paragraph direction Направление абзаца справа налево
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4371,3 +4371,8 @@ shape Appearance Izgled
# Graphics Adapter preference (#1885)
GPU adapter GPU adapter
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Automatski smer pasusa
Left-to-right paragraph direction Smer pasusa sleva nadesno
Right-to-left paragraph direction Smer pasusa zdesna nalevo
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4028,3 +4028,8 @@ cameraData Lens Lens
# Graphics Adapter preference (#1885)
GPU adapter GPU bağdaştırıcısı
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Otomatik paragraf yönü
Left-to-right paragraph direction Soldan sağa paragraf yönü
Right-to-left paragraph direction Sağdan sola paragraf yönü
Can't render this file because it contains an unexpected character in line 8 and column 34.
+5
View File
@@ -4009,3 +4009,8 @@ curves Smooth Згладити
# Graphics Adapter preference (#1885)
GPU adapter Адаптер GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Автоматичний напрямок абзацу
Left-to-right paragraph direction Напрямок абзацу зліва направо
Right-to-left paragraph direction Напрямок абзацу справа наліво
Can't render this file because it contains an unexpected character in line 1395 and column 18.
+5
View File
@@ -3791,3 +3791,8 @@ curves Smooth Làm mượt
# Graphics Adapter preference (#1885)
GPU adapter Bộ điều hợp GPU
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction Hướng đoạn văn tự động
Left-to-right paragraph direction Hướng đoạn văn từ trái sang phải
Right-to-left paragraph direction Hướng đoạn văn từ phải sang trái
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -2938,3 +2938,8 @@ curves Smooth 平滑
# Graphics Adapter preference (#1885)
GPU adapter GPU 适配器
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction 自动段落方向
Left-to-right paragraph direction 从左到右的段落方向
Right-to-left paragraph direction 从右到左的段落方向
Can't render this file because it contains an unexpected character in line 2611 and column 53.
+5
View File
@@ -2920,3 +2920,8 @@ curves Smooth 平滑
# Graphics Adapter preference (#1885)
GPU adapter GPU 介面卡
# Paragraph direction (TYPE-218-2)
Automatic paragraph direction 自動段落方向
Left-to-right paragraph direction 由左至右的段落方向
Right-to-left paragraph direction 由右至左的段落方向
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -619,6 +619,9 @@ pub struct ToolOptions {
/// OpenType features and variable-font axes are merged per tag/axis.
#[serde(default)]
pub type_style_defaults: serde_json::Map<String, serde_json::Value>,
/// Type tool: paragraph direction of new type, "auto" (from the first strong character),
/// "ltr" or "rtl" (TYPE-218-2).
pub type_direction: String,
/// Clone Stamp / Healing Brush.
pub clone_aligned: bool,
/// Pattern Stamp: lock the tile origin in document space across strokes.
@@ -920,6 +923,7 @@ impl Default for ToolOptions {
type_aa: "sharp".into(),
type_align: "left".into(),
type_style_defaults: serde_json::Map::new(),
type_direction: "auto".into(),
clone_aligned: true,
pattern_stamp_aligned: true,
pattern_stamp_impressionist: false,
+1 -1
View File
@@ -192,7 +192,7 @@ pub fn option_keys(tool: Tool) -> &'static [&'static str] {
Tool::Gradient => reset!(gradient_style, gradient_reverse, gradient_dither, gradient_classic, gradient_blend_mode, fill_opacity),
Tool::PaintBucket => reset!(bucket_fill_pattern, fill_opacity, tolerance, anti_alias, contiguous),
Tool::Type | Tool::VerticalType | Tool::HorizontalTypeMask | Tool::VerticalTypeMask => {
reset!(type_font, type_style, type_size, type_aa, type_align, type_style_defaults)
reset!(type_font, type_style, type_size, type_aa, type_align, type_direction, type_style_defaults)
}
Tool::Zoom => reset!(zoom_scrubby),
Tool::SpotHealing => reset!(spot_type),
+69
View File
@@ -1996,6 +1996,7 @@ fn default_style_params(app: &PhotocraftApp) -> serde_json::Value {
p["fontStyle"] = json!(o.type_style);
p["size"] = json!(o.type_size);
p["align"] = json!(o.type_align);
p["direction"] = json!(o.type_direction);
p["color"] = json!(hex(app.session.tools.foreground));
p
}
@@ -2068,6 +2069,11 @@ fn apply_defaults(app: &mut PhotocraftApp, props: &serde_json::Value) {
o.type_align = v.into();
}
}
"direction" => {
if let Some(v) = value.as_str() {
o.type_direction = v.into();
}
}
"metricsAsShown" => {} // Defaults have no layer transform.
"features" | "variations" => {
if let Some(patch) = value.as_object() {
@@ -2219,6 +2225,15 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
}
ui.spacing_mut().item_spacing.x = 8.0;
crate::widgets::vline(ui, 22.0);
ui.spacing_mut().item_spacing.x = 2.0;
for (key, tip, d) in direction_items() {
if direction_button(ui, d, o.type_direction == key, tip).clicked() {
app.ui.tool_options.type_direction = key.into();
apply(app, ui.ctx(), json!({"direction": key}));
}
}
ui.spacing_mut().item_spacing.x = 8.0;
crate::widgets::vline(ui, 22.0);
// Text colour swatch with a picker popup.
let c = shown
.as_ref()
@@ -2430,6 +2445,47 @@ fn align_glyph(p: &egui::Painter, r: egui::Rect, a: photocraft_doc::text::TextAl
}
}
/// Paragraph direction choices for the Paragraph panel and the options bar (TYPE-218-2): the
/// `direction` value `type.setStyle` takes, the tooltip, and the direction it sets.
fn direction_items() -> [(&'static str, &'static str, photocraft_doc::text::TextDirection); 3] {
use photocraft_doc::text::TextDirection as D;
[
("auto", tl!("Automatic paragraph direction"), D::Auto),
("ltr", tl!("Left-to-right paragraph direction"), D::Ltr),
("rtl", tl!("Right-to-left paragraph direction"), D::Rtl),
]
}
/// A pilcrow over an arrow pointing the way the paragraph runs; Auto points both ways.
fn direction_glyph(p: &egui::Painter, r: egui::Rect, d: photocraft_doc::text::TextDirection, c: Color32) {
use photocraft_doc::text::TextDirection as D;
let g = p.layout_no_wrap("".to_string(), egui::FontId::proportional(11.0), c);
p.galley(egui::pos2(r.center().x - g.size().x / 2.0, r.top() - 2.0), g, c);
let (y, s) = (r.bottom() - 1.5, Stroke::new(1.3, c));
p.line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], s);
let head = |x: f32, towards: f32| {
for dy in [-3.0, 3.0] {
p.line_segment([egui::pos2(x, y), egui::pos2(x - towards * 3.0, y + dy)], s);
}
};
if d != D::Rtl {
head(r.right(), 1.0);
}
if d != D::Ltr {
head(r.left(), -1.0);
}
}
/// The options bar's 24 px square button for one paragraph direction, styled like its icon buttons.
fn direction_button(ui: &mut egui::Ui, d: photocraft_doc::text::TextDirection, selected: bool, tip: &str) -> egui::Response {
let (r, resp) = ui.allocate_exact_size(egui::vec2(24.0, 24.0), egui::Sense::click());
let tint = crate::icons::button_chrome(ui, r, selected, resp.hovered());
direction_glyph(ui.painter(), egui::Rect::from_center_size(r.center(), egui::vec2(14.0, 16.0)), d, tint);
let tip = tip.to_string();
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, true, selected, &tip));
resp.on_hover_text(tip)
}
/// A toggle button (faux styles, paragraph alignment) of width `w`, painted by `paint`.
fn toggle_cell(ui: &mut egui::Ui, w: f32, on: bool, tip: &str, paint: impl FnOnce(&egui::Ui, egui::Rect, Color32)) -> bool {
let t = crate::theme::Tokens::get(ui.ctx());
@@ -2788,6 +2844,19 @@ fn type_sections(app: &mut PhotocraftApp, ui: &mut egui::Ui, character: bool, pa
}
});
ui.add_space(ROW_GAP);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
for (key, tip, d) in direction_items() {
let clicked = toggle_cell(ui, cw, para.direction == d, tip, |ui, r, col| {
let g = egui::Rect::from_center_size(r.center(), egui::vec2(14.0, 16.0));
direction_glyph(ui.painter(), g, d, col);
});
if clicked {
apply(app, ui.ctx(), json!({"direction": key}));
}
}
});
ui.add_space(ROW_GAP);
row(ui, &mut |ui| {
if let Some(v) = num_field(ui, "→|", "Indent left margin", para.start_indent_pt * u, -1296.0 * u..=1296.0 * u, unit, w) {
apply(app, ui.ctx(), json!({"startIndent": v / u}));
+56
View File
@@ -1826,6 +1826,62 @@ mod multi_layer_formatting {
});
}
/// The Paragraph section's direction buttons set the selected layers' paragraph direction in
/// one undo step, show the current one, and leave unselected layers and the tool's default for
/// new type alone (TYPE-218-2).
#[test]
fn paragraph_direction_buttons_set_and_show_the_direction() {
use egui::accesskit::Toggled;
use egui_kittest::kittest::NodeT;
use photocraft_doc::text::TextDirection;
let (app, ids) = fixture();
let dir = |app: &PhotocraftApp, id: LayerId| text(app, id).paragraph_runs()[0].style.direction;
let shown = |h: &Harness<'static, PhotocraftApp>, label: &str| h.get_by_label(label).accesskit_node().toggled();
let mut h = controls(app, true);
assert_eq!(shown(&h, "Automatic paragraph direction"), Some(Toggled::True), "new type follows its text");
assert_eq!(shown(&h, "Right-to-left paragraph direction"), Some(Toggled::False));
let steps = h.state().session.active().unwrap().history.entries().len();
h.get_by_label("Right-to-left paragraph direction").click();
h.run();
for id in &ids[..2] {
assert_eq!(dir(h.state(), *id), TextDirection::Rtl, "selected layers get the direction");
}
assert_eq!(dir(h.state(), ids[2]), TextDirection::Auto, "an unselected layer is untouched");
assert_eq!(h.state().session.active().unwrap().history.entries().len(), steps + 1, "one undo step");
assert_eq!(h.state().ui.tool_options.type_direction, "auto", "the panel edits layers, not new type");
h.run();
assert_eq!(shown(&h, "Right-to-left paragraph direction"), Some(Toggled::True), "the button shows it");
assert_eq!(shown(&h, "Automatic paragraph direction"), Some(Toggled::False));
h.get_by_label("Left-to-right paragraph direction").click();
h.run();
assert_eq!(dir(h.state(), ids[0]), TextDirection::Ltr);
assert!(h.state_mut().session.undo());
assert!(h.state_mut().session.undo());
for id in &ids[..2] {
assert_eq!(dir(h.state(), *id), TextDirection::Auto, "undo restores it");
}
}
/// The options bar's direction buttons set the selected layers' direction and the one new
/// type starts with, as its alignment buttons do (TYPE-218-2).
#[test]
fn options_bar_direction_sets_selected_layers_and_new_type() {
use photocraft_doc::text::TextDirection;
let (app, ids) = fixture();
let mut h = controls(app, false);
h.get_by_label("Right-to-left paragraph direction").click();
h.run();
assert_eq!(h.state().ui.tool_options.type_direction, "rtl");
for id in &ids[..2] {
assert_eq!(text(h.state(), *id).paragraph_runs()[0].style.direction, TextDirection::Rtl);
}
assert_eq!(text(h.state(), ids[2]).paragraph_runs()[0].style.direction, TextDirection::Auto);
super::super::pointer_up(h.state_mut(), [40.0, 60.0], [40.0, 60.0]);
let id = h.state().session.active().unwrap().active_layer.unwrap();
assert!(!ids.contains(&id), "a new type layer");
assert_eq!(text(h.state(), id).paragraph_runs()[0].style.direction, TextDirection::Rtl, "new type starts right to left");
}
#[test]
fn options_font_family_preserves_peer_faces_and_batches_layer_flags() {
let (app, ids) = fixture();
+2 -1
View File
@@ -85,7 +85,7 @@ what an agent can't finish alone.
### G8. Type depth
- **Missing:** type on a path beyond rigid glyph placement (TYPE-218-1 partial, #4365; the Type tool's click on a shown path landed),
paragraph direction UI (TYPE-218-2), complex-script fallback fonts and golden tests
the visual RTL caret (TYPE-218-2; the paragraph direction buttons landed 2026-10-20), complex-script fallback fonts and golden tests
(TYPE-218-3), OpenType Numerals and Stylistic Sets submenus
(TYPE-218-5; the Character panel buttons landed 2026-10-11, #3752), text free transform
(#2630). Done since the re-measure: manual and optical kerning with PSD round trip
@@ -297,6 +297,7 @@ what an agent can't finish alone.
| Date | Change | Summary |
|---|---|---|
| 2026-10-20 | minor | G8: Auto / Left-to-right / Right-to-left paragraph direction buttons in the Paragraph panel, Properties and the Type options bar; TYPE-218-2 partial (the visual RTL caret is still missing) |
| 2026-10-18 | minor | G13: Russian gained the 1,229 dynamic UI labels other catalogs already had (filter and adjustment parameters, Camera Raw, lighting, History/Layers/paths menus, metadata, System Info, status templates, dialogs); engine messages and native review remain open |
| 2026-10-15 | minor | G1 / #5972 A1: render only provably constant-RGB `GdFl` noise fills; variable noise synthesis remains open |
| 2026-10-18 | minor | G9: with no selection, Copy takes fill and adjustment layers whole and Paste recreates them (#2344); UI-217-23 stays partial (a single pixel layer copies its pixels only); estimate unchanged |
+2 -2
View File
@@ -14,9 +14,9 @@ note = "TextShape::Path (doc text.rs) with glyphs placed rigidly along the curve
[[item]]
id = "TYPE-218-2"
target = "Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret"
status = "missing"
status = "partial"
issue = 218
note = "Engine has TextDirection (text.rs:155); no UI"
note = "Paragraph section (Window > Paragraph and Properties) and the options bar have Auto / Left-to-right / Right-to-left buttons setting the paragraph direction through type.setStyle; the options bar's choice is also the direction new type starts with (type_tool.rs direction_items; paragraph_direction_buttons_set_and_show_the_direction, options_bar_direction_sets_selected_layers_and_new_type). Still missing: visual RTL caret (the arrow keys move in logical order)"
[[item]]
id = "TYPE-218-3"