Type: paragraph direction buttons in the Paragraph panel and options bar (TYPE-218-2, #218)

The engine has had TextDirection (Auto / LTR / RTL) and type.setStyle's "direction" key, but
nothing in the UI set it. Now:

- Window > Paragraph and the Properties Paragraph section have a row of three buttons under the
  alignment row: Automatic, Left-to-right and Right-to-left paragraph direction. They set the
  selected layers' direction (or the selected text's paragraphs while typing) in one undo step,
  and show the current one.
- The Type options bar has the same three buttons after its alignment buttons. Like alignment,
  they also set the direction new type starts with (new tool option `type_direction`, "auto" by
  default, reset with the tool's other options).
- The glyph is drawn (a pilcrow over an arrow), so no new icon asset.
- The three tooltips are translated in all 20 catalogs (every_tl_literal_is_translated).

Tests: paragraph_direction_buttons_set_and_show_the_direction and
options_bar_direction_sets_selected_layers_and_new_type.

TYPE-218-2 goes from missing to partial: the visual RTL caret (arrow keys moving on screen
rather than in logical order) is still to do. scorecard/type.toml, docs/scorecard.md and
docs/gaps.md (G8) are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpQQNxTPBzSLvgAhX27Vtv
This commit is contained in:
tareqziad1998
2026-10-16 15:58:53 +00:00
co-authored by Claude Opus 5.5
parent 2a952f164c
commit 8dcc64bd2f
27 changed files with 236 additions and 9 deletions
+5
View File
@@ -3816,3 +3816,8 @@ navigator Zoom Zvětšení
This document's layers: {needed} of {budget} for layers Vrstvy tohoto dokumentu: {needed} z {budget} pro vrstvy
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Vrstvy tohoto dokumentu potřebují {needed}, ale mohou zabrat {budget}: překreslení celého dokumentu probíhá na CPU
GPU Memory (MB, 0 = Automatic) Paměť GPU (MB, 0 = Automaticky)
# 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
@@ -3210,3 +3210,8 @@ curves Smooth Glätten
This document's layers: {needed} of {budget} for layers Ebenen dieses Dokuments: {needed} von {budget} für Ebenen
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Die Ebenen dieses Dokuments brauchen {needed}, dürfen aber {budget} belegen: Das ganze Dokument wird auf der CPU neu gezeichnet
GPU Memory (MB, 0 = Automatic) GPU-Speicher (MB, 0 = Automatisch)
# 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
@@ -3908,3 +3908,8 @@ curves Smooth Εξομάλυνση
This document's layers: {needed} of {budget} for layers Επίπεδα αυτού του εγγράφου: {needed} από {budget} για επίπεδα
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Τα επίπεδα αυτού του εγγράφου χρειάζονται {needed} αλλά μπορούν να καταλάβουν {budget}: η επανασχεδίαση όλου του εγγράφου γίνεται στη CPU
GPU Memory (MB, 0 = Automatic) Μνήμη GPU (MB, 0 = Αυτόματο)
# 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
@@ -4170,3 +4170,8 @@ navigator Zoom Zoom
This document's layers: {needed} of {budget} for layers Capas de este documento: {needed} de {budget} para capas
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Las capas de este documento necesitan {needed} pero pueden ocupar {budget}: los redibujados de todo el documento usan la CPU
GPU Memory (MB, 0 = Automatic) Memoria de GPU (MB, 0 = Automático)
# 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
@@ -3213,3 +3213,8 @@ curves Smooth Lisser
This document's layers: {needed} of {budget} for layers Calques de ce document : {needed} sur {budget} pour les calques
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Les calques de ce document ont besoin de {needed} mais peuvent occuper {budget} : le document entier se redessine sur le CPU
GPU Memory (MB, 0 = Automatic) Mémoire GPU (Mo, 0 = Automatique)
# 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
@@ -4127,3 +4127,8 @@ cameraRaw Yellow Sárga
This document's layers: {needed} of {budget} for layers A dokumentum rétegei: {needed} / {budget} a rétegeknek
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU A dokumentum rétegeinek {needed} kell, de {budget} foglalhatnak: a teljes dokumentum újrarajzolása a CPU-n fut
GPU Memory (MB, 0 = Automatic) GPU-memória (MB, 0 = Automatikus)
# 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
@@ -3200,3 +3200,8 @@ curves Smooth Haluskan
This document's layers: {needed} of {budget} for layers Layer dokumen ini: {needed} dari {budget} untuk layer
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Layer dokumen ini membutuhkan {needed} tetapi boleh memakai {budget}: penggambaran ulang seluruh dokumen memakai CPU
GPU Memory (MB, 0 = Automatic) Memori GPU (MB, 0 = Otomatis)
# 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
@@ -4329,3 +4329,8 @@ shape Appearance Aspetto
This document's layers: {needed} of {budget} for layers Livelli di questo documento: {needed} su {budget} per i livelli
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU I livelli di questo documento richiedono {needed} ma possono occupare {budget}: i ridisegni dell'intero documento usano la CPU
GPU Memory (MB, 0 = Automatic) Memoria GPU (MB, 0 = Automatico)
# 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 2653 and column 53.
+5
View File
@@ -3884,3 +3884,8 @@ curves Smooth スムーズ
This document's layers: {needed} of {budget} for layers このドキュメントのレイヤー: レイヤー用 {budget} のうち {needed}
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU このドキュメントのレイヤーには {needed} が必要ですが、使えるのは {budget} です: ドキュメント全体の再描画は CPU で行われます
GPU Memory (MB, 0 = Automatic) GPU メモリ(MB、0 = 自動)
# 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
@@ -3386,3 +3386,8 @@ curves Smooth 매끄럽게
This document's layers: {needed} of {budget} for layers 이 문서의 레이어: 레이어용 {budget} 중 {needed}
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU 이 문서의 레이어에 {needed}가 필요하지만 {budget}만 사용할 수 있습니다: 문서 전체 다시 그리기는 CPU를 사용합니다
GPU Memory (MB, 0 = Automatic) GPU 메모리(MB, 0 = 자동)
# 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 3165 and column 53.
+5
View File
@@ -3968,3 +3968,8 @@ curves Smooth Vloeiend maken
This document's layers: {needed} of {budget} for layers Lagen van dit document: {needed} van {budget} voor lagen
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU De lagen van dit document hebben {needed} nodig maar mogen {budget} gebruiken: het hele document wordt op de CPU opnieuw getekend
GPU Memory (MB, 0 = Automatic) GPU-geheugen (MB, 0 = Automatisch)
# 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
@@ -3806,3 +3806,8 @@ curves Smooth Wygładź
This document's layers: {needed} of {budget} for layers Warstwy tego dokumentu: {needed} z {budget} na warstwy
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Warstwy tego dokumentu potrzebują {needed}, ale mogą zająć {budget}: odświeżanie całego dokumentu używa CPU
GPU Memory (MB, 0 = Automatic) Pamięć GPU (MB, 0 = Automatycznie)
# 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
@@ -3998,3 +3998,8 @@ layerLabel No Color Sem cor
This document's layers: {needed} of {budget} for layers Camadas deste documento: {needed} de {budget} para camadas
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU As camadas deste documento precisam de {needed} mas podem ocupar {budget}: o documento inteiro é redesenhado na CPU
GPU Memory (MB, 0 = Automatic) Memória da GPU (MB, 0 = Automático)
# 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
@@ -3178,3 +3178,8 @@ curves Smooth Сгладить
This document's layers: {needed} of {budget} for layers Слои этого документа: {needed} из {budget} для слоёв
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Слоям этого документа нужно {needed}, но занимать они могут {budget}: весь документ перерисовывается на CPU
GPU Memory (MB, 0 = Automatic) Память GPU (МБ, 0 = автоматически)
# 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
@@ -4281,3 +4281,8 @@ shape Appearance Izgled
This document's layers: {needed} of {budget} for layers Slojevi ovog dokumenta: {needed} od {budget} za slojeve
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Slojevima ovog dokumenta treba {needed}, a smeju da zauzmu {budget}: ceo dokument se ponovo iscrtava na CPU-u
GPU Memory (MB, 0 = Automatic) GPU memorija (MB, 0 = Automatski)
# 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
@@ -3938,3 +3938,8 @@ cameraData Lens Lens
This document's layers: {needed} of {budget} for layers Bu belgenin katmanları: katmanlar için {budget} içinden {needed}
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Bu belgenin katmanlarının {needed} belleğe ihtiyacı var ama en fazla {budget} kullanabilirler: tüm belgenin yeniden çizimi CPU'yu kullanır
GPU Memory (MB, 0 = Automatic) GPU Belleği (MB, 0 = Otomatik)
# 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
@@ -3907,3 +3907,8 @@ curves Smooth Згладити
This document's layers: {needed} of {budget} for layers Шари цього документа: {needed} з {budget} для шарів
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Шарам цього документа потрібно {needed}, але займати вони можуть {budget}: увесь документ перемальовується на CPU
GPU Memory (MB, 0 = Automatic) Пам'ять GPU (МБ, 0 = автоматично)
# 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 1388 and column 18.
+5
View File
@@ -3694,3 +3694,8 @@ curves Smooth Làm mượt
This document's layers: {needed} of {budget} for layers Các lớp của tài liệu này: {needed} trên {budget} dành cho lớp
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU Các lớp của tài liệu này cần {needed} nhưng chỉ được dùng {budget}: vẽ lại toàn bộ tài liệu dùng CPU
GPU Memory (MB, 0 = Automatic) Bộ nhớ GPU (MB, 0 = Tự động)
# 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
@@ -2824,3 +2824,8 @@ curves Smooth 平滑
This document's layers: {needed} of {budget} for layers 此文档的图层:图层可用 {budget} 中的 {needed}
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU 此文档的图层需要 {needed},但只能占用 {budget}:整个文档的重绘使用 CPU
GPU Memory (MB, 0 = Automatic) GPU 内存(MB,0 = 自动)
# 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 2599 and column 53.
+5
View File
@@ -2809,3 +2809,8 @@ curves Smooth 平滑
This document's layers: {needed} of {budget} for layers 此文件的圖層:圖層可用 {budget} 中的 {needed}
This document's layers need {needed} but may hold {budget}: whole-document redraws use the CPU 此文件的圖層需要 {needed},但只能佔用 {budget}:整份文件的重繪使用 CPU
GPU Memory (MB, 0 = Automatic) GPU 記憶體(MB,0 = 自動)
# 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
@@ -539,6 +539,9 @@ pub struct ToolOptions {
pub type_size: f32,
pub type_aa: String,
pub type_align: String,
/// 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.
@@ -784,6 +787,7 @@ impl Default for ToolOptions {
type_size: 48.0,
type_aa: "sharp".into(),
type_align: "left".into(),
type_direction: "auto".into(),
clone_aligned: true,
pattern_stamp_aligned: true,
pattern_stamp_impressionist: false,
+3 -1
View File
@@ -150,7 +150,9 @@ pub fn reset_options(o: &mut ToolOptions, tool: Tool) {
Tool::MagicEraser => reset!(tolerance, anti_alias, contiguous, sample_all_layers, magic_eraser_opacity),
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, sample_all_layers),
Tool::Type | Tool::VerticalType | Tool::HorizontalTypeMask | Tool::VerticalTypeMask => reset!(type_font, type_style, type_size, type_aa, type_align),
Tool::Type | Tool::VerticalType | Tool::HorizontalTypeMask | Tool::VerticalTypeMask => {
reset!(type_font, type_style, type_size, type_aa, type_align, type_direction)
}
Tool::Zoom => reset!(zoom_scrubby),
Tool::SpotHealing => reset!(spot_type, sample_all_layers),
Tool::Healing => reset!(clone_aligned, clone_sample),
+64
View File
@@ -317,6 +317,7 @@ pub fn pointer_up(app: &mut PhotocraftApp, start: [f64; 2], end: [f64; 2]) {
"fontStyle": o.type_style,
"size": o.type_size,
"align": o.type_align,
"direction": o.type_direction,
"color": hex(app.session.tools.foreground),
});
if w >= min && h >= min {
@@ -1780,6 +1781,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()
@@ -1983,6 +1993,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());
@@ -2305,6 +2356,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, -1296.0..=1296.0, "pt", w) {
apply(app, ui.ctx(), json!({"startIndent": v}));
+56
View File
@@ -1438,6 +1438,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();
+5 -4
View File
@@ -1,6 +1,6 @@
# Where PhotoCraft falls short of Photoshop
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
@@ -78,13 +78,13 @@ what an agent can't finish alone.
- **Estimate:** 80–140 h.
### G8. Type depth
- **Missing:** type on a path (TYPE-218-1), paragraph direction UI (TYPE-218-2), complex-script
fallback fonts and golden tests (TYPE-218-3), OpenType Numerals and Stylistic Sets submenus
- **Missing:** type on a path (TYPE-218-1), the visual RTL caret (TYPE-218-2; the paragraph
direction buttons landed 2026-10-16), 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), live Warp Text preview
(TYPE-218-7, #2689), "rasterize and continue" for distort/perspective on type
(TYPE-218-9), text free transform (#2630). Done since the re-measure: manual and optical kerning
with PSD round trip (TYPE-218-6, #206), variable-font axes (TYPE-218-4, #2985).
- **Evidence:** scorecard Type: 3 done, 2 partial, 4 missing. 2026-10-11: 2/3/4/5 clicks select
- **Evidence:** scorecard Type: 4 done, 4 partial, 1 missing. 2026-10-11: 2/3/4/5 clicks select
word, line, paragraph and all text, with tests (TYPE-218-8, #3493). The Type tool froze the app for
60 s on one macOS machine (#2518).
- **Estimate:** 120–200 h.
@@ -270,6 +270,7 @@ what an agent can't finish alone.
| Date | Change | Summary |
|---|---|---|
| 2026-10-16 | 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-14 | minor | G1: a PSD smart object's filter mask shows when the file opens (Photoshop stores the filtered pixels unmasked); export stores them that way too; Photoshop-oracle floor 185 → 188 of 258 |
| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open |
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
+2 -2
View File
@@ -10,7 +10,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
| [File compatibility](#file-compatibility) | 7 | 14 | 6 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 188/256, round trip ≥ 258/256 |
| [Tools](#tools) | 33 | 11 | 0 | |
| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 168 (target 0) |
| [Type](#type) | 4 | 3 | 2 | |
| [Type](#type) | 4 | 4 | 1 | |
| [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 29 of 29 crates; panic_hunt present |
| [Distribution](#distribution) | 5 | 4 | 2 | |
@@ -214,7 +214,7 @@ Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_w
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| TYPE-218-1 | Type on a path: path shape, glyphs along the curve, caret follows, PSD round trip | partial | [#218](https://github.com/storytold/photocraft/issues/218) | TextShape::Path (doc text.rs) with glyphs placed rigidly along the curve by arc length (text warp.rs PathFrame), type.attachToPath and type.detachFromPath (#4365), .pcraft round trip. Missing: clicking a path with the Type tool, the caret and selection following the curve, and PSD path text (it saves as point text) |
| TYPE-218-2 | Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret | missing | [#218](https://github.com/storytold/photocraft/issues/218) | Engine has TextDirection (text.rs:155); no UI |
| TYPE-218-2 | Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret | partial | [#218](https://github.com/storytold/photocraft/issues/218) | 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) |
| TYPE-218-3 | Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Fallback fonts are covered for Thai, Khmer, Lao, Myanmar (THAI_FAMILIES, KHMER_FAMILIES, LAO_FAMILIES, MYANMAR_FAMILIES in fonts.rs; #1909, #3259), the nine Indic scripts (INDIC_FAMILIES: Nirmala UI, Kohinoor and Sangam MN, Noto Sans) and Arabic and Hebrew. Still missing: per-script golden shaping tests against reference output (only smoke tests that an installed font shapes without .notdef, plus a bidi test) |
| TYPE-218-4 | Variable font axis sliders from the font's axes; opsz follows the point size | done | [#218](https://github.com/storytold/photocraft/issues/218) | One slider per axis of the style's font in the Character section (Properties and Window > Character), setting type.setStyle variations; opsz follows the type size unless set (layout.rs variation_list); a style or weight resets the wght axis (#2985). PSD does not round-trip axis values yet |
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Type > OpenType toggles (type_extra_cmds.rs:512). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Still missing: the Numerals and Stylistic Sets submenus, and greying out features the font lacks |
+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"