Type tool: spelling suggestions in the edit menu (#4370) (#4375)

* Type tool: spelling suggestions in the edit menu (#4370)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Type spelling menu: cap suggestions at 3 so the menu stays short (#4370)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: regenerate scorecard after merging main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-12 21:47:09 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 72ef526d8e
commit f1855caf73
28 changed files with 223 additions and 17 deletions
+1 -1
View File
@@ -3183,7 +3183,7 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect:
crate::canvas_tool_menu::open_view(app, &ctx, tool, [p.x, p.y], xf.to_doc(p));
}
} else {
crate::canvas_tool_menu::open(app, &ctx, tool, [p.x, p.y]);
crate::canvas_tool_menu::open_at(app, &ctx, tool, [p.x, p.y], Some(xf.to_doc(p)));
}
}
// The (temporary) Hand pans above; its gestures never reach the tool underneath. While a
+153 -4
View File
@@ -31,8 +31,30 @@ pub struct CanvasToolMenu {
/// Opened while a Type tool edits text: the text and type rows instead of the tool's actions.
#[serde(default)]
pub text: bool,
/// The misspelled word under the pointer when the type edit menu opened: its suggestions head
/// the menu.
#[serde(default)]
pub spelling: Option<Spelling>,
}
/// A misspelled word in the type layer being edited (`edit.checkSpelling`'s `list` entry).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Spelling {
pub layer: u64,
/// The word's characters `start..end` in the layer's text.
pub start: usize,
pub end: usize,
pub word: String,
pub suggestions: Vec<String>,
}
/// Most suggestions the type edit menu lists.
const MAX_SUGGESTIONS: usize = 3;
/// The spelling rows' ids: `spelling.replace.<n>` picks the n-th suggestion.
const SPELLING_REPLACE: &str = "spelling.replace.";
const SPELLING_NONE: &str = "spelling.none";
const SPELLING_ADD: &str = "spelling.add";
/// One menu row: label and command id. `None` is a separator.
pub type Row = Option<(&'static str, &'static str)>;
@@ -275,7 +297,7 @@ pub fn rows(menu: &CanvasToolMenu) -> &'static [Row] {
/// Is `command` one of the open menu's rows, and enabled?
pub fn available(app: &PhotocraftApp, menu: &CanvasToolMenu, command: &str) -> bool {
rows(menu).iter().flatten().any(|(_, id)| *id == command) && entry_enabled(app, menu, command)
(rows(menu).iter().flatten().any(|(_, id)| *id == command) || is_spelling(menu, command)) && entry_enabled(app, menu, command)
}
/// Is `command` the transform box's current mode (checked in the transform menu)?
@@ -311,6 +333,49 @@ pub fn editing_text(app: &PhotocraftApp, tool: Tool) -> bool {
tool.is_type() && app.ui.text_edit.is_some()
}
/// The misspelled word of the edited text under document point `doc`, with its suggestions.
fn spelling_at(app: &mut PhotocraftApp, doc: [f64; 2]) -> Option<Spelling> {
let layer = app.ui.text_edit.as_ref()?.layer;
if !doc.iter().all(|v| v.is_finite()) {
return None;
}
let at = crate::type_tool::offset_at(app, photocraft_doc::LayerId(layer), doc[0], doc[1]);
let found = app.session.execute("edit.checkSpelling", json!({"action": "list", "layer": layer, "suggestions": MAX_SUGGESTIONS})).ok()?;
let hit = found["misspellings"].as_array()?.iter().find(|m| {
let (a, b) = (m["start"].as_u64().unwrap_or(u64::MAX) as usize, m["end"].as_u64().unwrap_or(0) as usize);
a <= at && at <= b
})?;
Some(Spelling {
layer,
start: hit["start"].as_u64()? as usize,
end: hit["end"].as_u64()? as usize,
word: hit["word"].as_str()?.to_string(),
suggestions: hit["suggestions"].as_array()?.iter().filter_map(|s| s.as_str().map(str::to_string)).take(MAX_SUGGESTIONS).collect(),
})
}
/// The spelling rows heading the type edit menu (label, id): the suggestions, or a greyed note
/// when there are none, then Add to Dictionary. Empty away from a misspelled word.
pub fn spelling_rows(menu: &CanvasToolMenu) -> Vec<(String, String)> {
let Some(sp) = menu.spelling.as_ref().filter(|_| menu.text) else { return Vec::new() };
let mut rows: Vec<(String, String)> = sp.suggestions.iter().enumerate().map(|(i, s)| (s.clone(), format!("{SPELLING_REPLACE}{i}"))).collect();
if rows.is_empty() {
rows.push(("No Spelling Suggestions".into(), SPELLING_NONE.into()));
}
rows.push(("Add to Dictionary".into(), SPELLING_ADD.into()));
rows
}
/// Is `command` one of the menu's spelling rows?
fn is_spelling(menu: &CanvasToolMenu, command: &str) -> bool {
spelling_rows(menu).iter().any(|(_, id)| id == command)
}
/// Does a spelling row's label come from the UI (translated) rather than from the dictionary?
pub fn spelling_label_is_ui(id: &str) -> bool {
matches!(id, SPELLING_NONE | SPELLING_ADD)
}
/// Right-click over a Free Transform box: its modes.
pub fn open_transform(app: &mut PhotocraftApp, ctx: &Context, pos: [f32; 2]) -> bool {
if !pos.iter().all(|v| v.is_finite()) {
@@ -328,6 +393,7 @@ pub fn open_transform(app: &mut PhotocraftApp, ctx: &Context, pos: [f32; 2]) ->
pasteboard: false,
anchor: None,
text: false,
spelling: None,
});
crate::canvas_popup::mark_open(ctx, menu_id());
true
@@ -346,10 +412,24 @@ pub fn engine_call(menu: &CanvasToolMenu, command: &str) -> (String, Value) {
Some(c) if menu.pasteboard => ("prefs.set".into(), json!({"values": {"interface.canvasColor": c.name()}})),
_ if menu.pasteboard => ("prefs.set".into(), json!({})),
_ if menu.text && matches!(command, FAUX_BOLD | FAUX_ITALIC) => ("type.setStyle".into(), json!({})),
_ if is_spelling(menu, command) => spelling_call(menu, command),
_ => (command.into(), json!({})),
}
}
/// The `edit.checkSpelling` call a spelling row makes: replace the word with a suggestion, or add
/// it to the user dictionary.
fn spelling_call(menu: &CanvasToolMenu, command: &str) -> (String, Value) {
let Some(sp) = menu.spelling.as_ref() else { return ("edit.checkSpelling".into(), json!({})) };
let replace = command.strip_prefix(SPELLING_REPLACE).and_then(|n| n.parse::<usize>().ok()).and_then(|n| sp.suggestions.get(n));
match replace {
Some(to) => {
("edit.checkSpelling".into(), json!({"action": "change", "layer": sp.layer, "start": sp.start, "end": sp.end, "word": sp.word, "replace": to}))
}
None => ("edit.checkSpelling".into(), json!({"action": "addToDictionary", "word": sp.word})),
}
}
/// Does a right-click with `tool` at document point `doc` open the pasteboard menu? Outside the
/// image, for the tools whose right-click has no job there: not the painting tools (their Brush
/// Preset picker or right-button erase), the Pen (its path menu) or text being edited.
@@ -377,6 +457,7 @@ pub fn open_pasteboard(app: &mut PhotocraftApp, ctx: &Context, tool: Tool, pos:
pasteboard: true,
anchor: None,
text: false,
spelling: None,
});
crate::canvas_popup::mark_open(ctx, menu_id());
true
@@ -406,6 +487,8 @@ pub fn entry_enabled(app: &PhotocraftApp, menu: &CanvasToolMenu, command: &str)
}
if menu.text {
return match command {
SPELLING_NONE => false,
c if is_spelling(menu, c) => app.ui.text_edit.is_some(),
FAUX_BOLD | FAUX_ITALIC => app.ui.text_edit.is_some(),
c => TYPE_EDIT_MENU.iter().flatten().any(|(_, id)| *id == c) && crate::menus::is_enabled(app, c),
};
@@ -449,6 +532,12 @@ pub fn entry_enabled(app: &PhotocraftApp, menu: &CanvasToolMenu, command: &str)
}
pub fn open(app: &mut PhotocraftApp, ctx: &Context, tool: Tool, pos: [f32; 2]) -> bool {
open_at(app, ctx, tool, pos, None)
}
/// [`open`] for a right-click at document point `doc`: over a misspelled word of the text being
/// edited, the menu starts with its spelling suggestions.
pub fn open_at(app: &mut PhotocraftApp, ctx: &Context, tool: Tool, pos: [f32; 2], doc: Option<[f64; 2]>) -> bool {
let text = editing_text(app, tool);
if !(applies(tool) || text) || !pos.iter().all(|v| v.is_finite()) {
return false;
@@ -458,7 +547,9 @@ pub fn open(app: &mut PhotocraftApp, ctx: &Context, tool: Tool, pos: [f32; 2]) -
let has_selection = app.session.active().is_some_and(|s| s.doc.selection.is_some());
let has_path = crate::vector_ui::active_path_name(app).is_some() || app.ui.pen.as_ref().is_some_and(|p| p.knots.len() >= 2);
let path_name = crate::vector_ui::active_path_name(app);
app.ui.canvas_tool_menu = Some(CanvasToolMenu { pos, tool, has_selection, has_path, path_name, transform: false, pasteboard: false, anchor: None, text });
let spelling = if text { doc.and_then(|d| spelling_at(app, d)) } else { None };
app.ui.canvas_tool_menu =
Some(CanvasToolMenu { pos, tool, has_selection, has_path, path_name, transform: false, pasteboard: false, anchor: None, text, spelling });
crate::canvas_popup::mark_open(ctx, menu_id());
true
}
@@ -481,6 +572,7 @@ pub fn open_view(app: &mut PhotocraftApp, ctx: &Context, tool: Tool, pos: [f32;
pasteboard: false,
anchor: Some(doc),
text: false,
spelling: None,
});
crate::canvas_popup::mark_open(ctx, menu_id());
true
@@ -521,6 +613,8 @@ pub fn choose(app: &mut PhotocraftApp, ctx: &Context, command: &str) {
choose_pasteboard(app, &menu, command)
} else if menu.anchor.is_some() {
choose_view(app, ctx, &menu, command)
} else if menu.text && is_spelling(&menu, command) {
choose_spelling(app, &menu, command)
} else if menu.text {
choose_type(app, ctx, command)
} else if menu.tool == Tool::Pen && !menu.transform {
@@ -538,6 +632,19 @@ pub fn choose(app: &mut PhotocraftApp, ctx: &Context, command: &str) {
}
}
/// A spelling row: replace the word under the pointer (the caret lands after the replacement), or
/// add it to the user dictionary.
fn choose_spelling(app: &mut PhotocraftApp, menu: &CanvasToolMenu, command: &str) -> Result<(), String> {
let (id, params) = engine_call(menu, command);
let result = app.run(&id, params)?;
if let Some(end) = result["end"].as_u64().map(|e| e as usize)
&& let Some(e) = app.ui.text_edit.as_mut()
{
(e.caret, e.anchor) = (end, end);
}
Ok(())
}
/// A type edit row. Undo, Cut, Copy, Paste and Select All run as their Edit and Select menu items,
/// which act on the text while it is being edited (Paste asks the window for the clipboard text).
fn choose_type(app: &mut PhotocraftApp, ctx: &Context, command: &str) -> Result<(), String> {
@@ -668,6 +775,17 @@ pub fn show(app: &mut PhotocraftApp, ctx: &Context) {
ui.spacing_mut().item_spacing.y = 0.0;
ui.set_width(200.0);
crate::widgets::menu_scroll(ui, |ui| {
let spelling = spelling_rows(&menu);
for (label, command) in &spelling {
let text = if spelling_label_is_ui(command) { tl!(label).to_string() } else { label.clone() };
let item = egui::Button::new(text).min_size(egui::vec2(200.0, 20.0));
if ui.add_enabled(entry_enabled(app, &menu, command), item).clicked() {
selected = Some(command.clone());
}
}
if !spelling.is_empty() {
ui.separator();
}
for row in rows(&menu) {
let Some((label, command)) = *row else {
ui.separator();
@@ -675,14 +793,14 @@ pub fn show(app: &mut PhotocraftApp, ctx: &Context) {
};
let item = egui::Button::selectable(mode_checked(app, &menu, command), tl!(label)).min_size(egui::vec2(200.0, 20.0));
if ui.add_enabled(entry_enabled(app, &menu, command), item).clicked() {
selected = Some(command);
selected = Some((*command).to_string());
}
}
});
});
});
if let Some(command) = selected {
choose(app, ctx, command);
choose(app, ctx, &command);
} else if crate::canvas_popup::outside_pressed(ctx, id, area.response.rect) {
app.ui.canvas_tool_menu = None;
}
@@ -849,6 +967,37 @@ mod tests {
assert!(app.ui.text_edit.is_none() && text_of(&app, id).is_none(), "the type layer is pixels now");
}
#[test]
fn type_edit_menu_lists_spelling_suggestions_for_the_word_under_the_pointer() {
let mut app = app();
app.ui.tool = Tool::Type;
let id = app.run("type.create", json!({"text": "Teh cat", "size": 12, "x": 4, "y": 20})).unwrap()["layer"].as_u64().unwrap();
let ctx = Context::default();
crate::menus::invoke(&mut app, &ctx, "type.editText", json!({})).unwrap();
// Away from the misspelling (past the end of the line): no spelling rows.
assert!(open_at(&mut app, &ctx, Tool::Type, [10.0, 10.0], Some([600.0, 15.0])));
assert!(spelling_rows(app.ui.canvas_tool_menu.as_ref().unwrap()).is_empty());
// Over "Teh": its suggestions head the menu, then Add to Dictionary.
assert!(open_at(&mut app, &ctx, Tool::Type, [10.0, 10.0], Some([5.0, 15.0])));
let menu = app.ui.canvas_tool_menu.clone().unwrap();
let rows = spelling_rows(&menu);
assert!(rows.iter().any(|(l, _)| l == "The"), "{rows:?}");
assert_eq!(rows.last().map(|(_, id)| id.as_str()), Some(SPELLING_ADD));
assert!(rows.len() <= MAX_SUGGESTIONS + 1, "at most {MAX_SUGGESTIONS} suggestions and Add to Dictionary: {rows:?}");
assert!(available(&app, &menu, &rows[0].1));
let pick = rows.iter().find(|(l, _)| l == "The").unwrap().1.clone();
choose(&mut app, &ctx, &pick);
assert_eq!(text_of(&app, id).unwrap().text, "The cat");
assert_eq!(app.ui.text_edit.as_ref().map(|e| (e.caret, e.anchor)), Some((3, 3)), "the caret lands after the replacement");
// Add to Dictionary stops flagging a word.
app.run("type.insertText", json!({"layer": id, "text": " Zqx"})).unwrap();
assert!(open_at(&mut app, &ctx, Tool::Type, [10.0, 10.0], Some([600.0, 15.0])));
let menu = app.ui.canvas_tool_menu.clone().unwrap();
assert!(menu.spelling.as_ref().is_some_and(|s| s.word == "Zqx"));
choose(&mut app, &ctx, SPELLING_ADD);
assert!(app.session.prefs().user_dictionary.iter().any(|w| w == "Zqx"));
}
#[test]
fn right_click_outside_the_image_picks_the_pasteboard_color() {
let mut app = app();
+18 -5
View File
@@ -366,6 +366,22 @@ pub fn handle(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest
outcome
}
/// The open canvas context menu's rows for `ui.state`: the spelling suggestions first (type edit
/// menu over a misspelled word), then the menu's own rows.
fn context_entries(app: &PhotocraftApp, menu: &crate::canvas_tool_menu::CanvasToolMenu) -> Vec<Value> {
use crate::canvas_tool_menu::{entry_enabled, rows, spelling_rows};
let spelling = spelling_rows(menu);
let mut entries: Vec<Value> = spelling.iter().map(|(label, id)| json!({"label": label, "id": id, "enabled": entry_enabled(app, menu, id)})).collect();
if !spelling.is_empty() {
entries.push(json!({"separator": true}));
}
entries.extend(rows(menu).iter().map(|row| match row {
Some((label, id)) => json!({"label": label, "id": id, "enabled": entry_enabled(app, menu, id)}),
None => json!({"separator": true}),
}));
entries
}
fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
let p = &req.params;
let s = |k: &str| p.get(k).and_then(Value::as_str);
@@ -1027,7 +1043,7 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest)
}
if crate::canvas_tool_menu::applies(app.ui.tool) || crate::canvas_tool_menu::editing_text(app, app.ui.tool) {
if down {
crate::canvas_tool_menu::open(app, ctx, app.ui.tool, screen_point(app, x, y));
crate::canvas_tool_menu::open_at(app, ctx, app.ui.tool, screen_point(app, x, y), Some([x, y]));
}
continue;
}
@@ -1259,10 +1275,7 @@ pub fn inspect(app: &PhotocraftApp, ctx: &egui::Context) -> Value {
"pos": menu.pos,
"tool": menu.tool,
"pasteboard": menu.pasteboard,
"entries": crate::canvas_tool_menu::rows(menu).iter().map(|row| match row {
Some((label, id)) => json!({"label": label, "id": id, "enabled": crate::canvas_tool_menu::entry_enabled(app, menu, id)}),
None => json!({"separator": true}),
}).collect::<Vec<_>>()
"entries": context_entries(app, menu)
})
}),
"panels": app.ui.panels,
+2
View File
@@ -138,6 +138,8 @@
Search… Hledat…
Check Spelling… Kontrola pravopisu…
Find and Replace Text… Najít a nahradit text…
Add to Dictionary Přidat do slovníku
No Spelling Suggestions Žádné návrhy pravopisu
Fill… Vyplnit…
Stroke… Tah…
Content-Aware Fill… Výplň s ohledem na obsah…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -134,6 +134,8 @@
Search… Suchen…
Check Spelling… Rechtschreibung prüfen…
Find and Replace Text… Text suchen und ersetzen…
Add to Dictionary Zum Wörterbuch hinzufügen
No Spelling Suggestions Keine Rechtschreibvorschläge
Fill… Füllen…
Stroke… Nachziehen…
Content-Aware Fill… Inhaltsbasiert füllen…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -115,6 +115,8 @@
Search… Αναζήτηση…
Check Spelling… Ορθογραφικός έλεγχος…
Find and Replace Text… Εύρεση και αντικατάσταση κειμένου…
Add to Dictionary Προσθήκη στο λεξικό
No Spelling Suggestions Δεν υπάρχουν ορθογραφικές προτάσεις
Fill… Γέμισμα…
Stroke… Περίγραμμα…
Preferences… Προτιμήσεις…
1 # Greek (el) PhotoCraft interface catalog.
115
116
117
118
119
120
121
122
+2
View File
@@ -131,6 +131,8 @@
Search… Buscar…
Check Spelling… Revisar ortografía…
Find and Replace Text… Buscar y reemplazar texto…
Add to Dictionary Añadir al diccionario
No Spelling Suggestions Sin sugerencias ortográficas
Fill… Rellenar…
Stroke… Contornear…
Content-Aware Fill… Relleno según el contenido…
Can't render this file because it contains an unexpected character in line 4 and column 47.
+2
View File
@@ -136,6 +136,8 @@
Search… Rechercher…
Check Spelling… Vérifier l'orthographe…
Find and Replace Text… Rechercher et remplacer du texte…
Add to Dictionary Ajouter au dictionnaire
No Spelling Suggestions Aucune suggestion orthographique
Fill… Remplir…
Stroke… Contourner…
Content-Aware Fill… Remplissage d'après le contenu…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -126,6 +126,8 @@
File Info… Fájladatok…
Fill… Kitöltés…
Find and Replace Text… Szöveg keresése és cseréje…
Add to Dictionary Hozzáadás a szótárhoz
No Spelling Suggestions Nincs helyesírási javaslat
Fit Image… Kép méretre igazítása…
Flatten All Layer Effects Összes rétegeffektus egybesimítása
Flatten All Masks Összes maszk egybesimítása
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -129,6 +129,8 @@
Search… Cari…
Check Spelling… Periksa Ejaan…
Find and Replace Text… Cari dan Ganti Teks…
Add to Dictionary Tambahkan ke Kamus
No Spelling Suggestions Tidak Ada Saran Ejaan
Fill… Isi…
Stroke… Stroke…
Content-Aware Fill… Isi Content-Aware…
Can't render this file because it contains an unexpected character in line 4 and column 47.
+2
View File
@@ -131,6 +131,8 @@
Search… Cerca…
Check Spelling… Controlla ortografia…
Find and Replace Text… Trova e sostituisci testo…
Add to Dictionary Aggiungi al dizionario
No Spelling Suggestions Nessun suggerimento ortografico
Fill… Riempi…
Stroke… Traccia…
Content-Aware Fill… Riempimento in base al contenuto…
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
131
132
133
134
135
136
137
138
+2
View File
@@ -132,6 +132,8 @@
Search… 検索…
Check Spelling… スペルチェック…
Find and Replace Text… テキストを検索して置換…
Add to Dictionary 辞書に追加
No Spelling Suggestions スペルの候補はありません
Fill… 塗りつぶし…
Stroke… 境界線を描く…
Content-Aware Fill… コンテンツに応じた塗りつぶし…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -121,6 +121,8 @@
Search… 검색…
Check Spelling… 맞춤법 검사…
Find and Replace Text… 텍스트 찾기 및 바꾸기…
Add to Dictionary 사전에 추가
No Spelling Suggestions 맞춤법 제안 없음
Fill… 채우기…
Stroke… 획…
Content-Aware Fill… 내용 인식 채우기…
1 # Korean (ko) catalog. UTF-8: context<TAB>English source<TAB>translation.
121
122
123
124
125
126
127
128
+2
View File
@@ -124,6 +124,8 @@
Search… Zoeken…
Check Spelling… Spelling controleren…
Find and Replace Text… Tekst zoeken en vervangen…
Add to Dictionary Aan woordenboek toevoegen
No Spelling Suggestions Geen spellingsuggesties
Fill… Vullen…
Stroke… Omlijnen…
Content-Aware Fill… Inhoudsbewust vullen…
Can't render this file because it contains an unexpected character in line 8 and column 34.
+2
View File
@@ -131,6 +131,8 @@
Search… Szukaj…
Check Spelling… Sprawdź pisownię…
Find and Replace Text… Znajdź i zamień tekst…
Add to Dictionary Dodaj do słownika
No Spelling Suggestions Brak sugestii pisowni
Fill… Wypełnij…
Stroke… Obrys…
Content-Aware Fill… Wypełnienie z uwzględnieniem zawartości…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -132,6 +132,8 @@
Search… Pesquisar…
Check Spelling… Verificar ortografia…
Find and Replace Text… Localizar e substituir texto…
Add to Dictionary Adicionar ao dicionário
No Spelling Suggestions Sem sugestões de ortografia
Fill… Preencher…
Stroke… Traçado…
Content-Aware Fill… Preenchimento sensível ao conteúdo…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -270,6 +270,8 @@
Check Spelling Проверка орфографии
Language Options Языковые параметры
Find and Replace Text… Найти и заменить текст…
Add to Dictionary Добавить в словарь
No Spelling Suggestions Нет вариантов написания
Paste Lorem Ipsum Вставить условный текст
Glyphs Panel Панель «Глифы»
Type Styles Стили текста
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -121,6 +121,8 @@
Search… Ara…
Check Spelling… Yazımı Denetle…
Find and Replace Text… Metni Bul ve Değiştir…
Add to Dictionary Sözlüğe Ekle
No Spelling Suggestions Yazım Önerisi Yok
Fill… Doldur…
Stroke… Kontur…
Content-Aware Fill… İçeriğe Uygun Dolgu…
Can't render this file because it contains an unexpected character in line 8 and column 34.
+2
View File
@@ -248,6 +248,8 @@
Check Spelling Перевірити правопис
Language Options Мовні параметри
Find and Replace Text… Знайти та замінити текст…
Add to Dictionary Додати до словника
No Spelling Suggestions Немає варіантів написання
Paste Lorem Ipsum Вставити Lorem Ipsum
Glyphs Panel Панель гліфів
Type Styles Текстові стилі
1 # Ukrainian (uk) catalog. UTF-8: context<TAB>English source<TAB>translation.
248
249
250
251
252
253 # Select
254
255
+2
View File
@@ -73,6 +73,8 @@
Fill… Tô…
Filter Bộ lọc
Find and Replace Text… Tìm và thay chữ…
Add to Dictionary Thêm vào từ điển
No Spelling Suggestions Không có gợi ý chính tả
Fit Image… Vừa khung ảnh…
Flatten All Layer Effects Làm phẳng mọi hiệu ứng lớp
Flatten All Masks Làm phẳng mọi mặt nạ
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -130,6 +130,8 @@
Search… 搜索…
Check Spelling… 检查拼写…
Find and Replace Text… 查找和替换文本…
Add to Dictionary 添加到词典
No Spelling Suggestions 没有拼写建议
Fill… 填充…
Stroke… 描边…
Content-Aware Fill… 内容识别填充…
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
130
131
132
133
134
135
136
137
+2
View File
@@ -138,6 +138,8 @@
Search… 搜尋…
Check Spelling… 拼字檢查…
Find and Replace Text… 尋找並取代文字…
Add to Dictionary 加入字典
No Spelling Suggestions 沒有拼字建議
Fill… 填滿…
Stroke… 筆畫…
Content-Aware Fill… 內容感知填滿…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -17,9 +17,9 @@
//! caret to the pointer on any press), so Copy and Cut act on it.
//!
//! The right-click belongs to the field: the canvas and its tool menus only see pointer presses
//! on the canvas, which a field drawn above it takes. Spelling suggestions are not in this menu
//! yet: the spell checker (`edit.checkSpelling`, whose `suggest` action lists them) is not wired
//! into text fields.
//! on the canvas, which a field drawn above it takes. Spelling suggestions are not in this menu:
//! the spell checker (`edit.checkSpelling`) checks type layers, so they head the Type tool's edit
//! menu instead (`canvas_tool_menu`, over a misspelled word of the text being edited, #4370).
use egui::text::CCursorRange;
use egui::{Context, Event, Id, Key, Modifiers, PointerButton, Pos2, RawInput, SurrenderFocusOn, Ui};
@@ -52,6 +52,7 @@ fn type_layers_offer_free_transform_but_not_distort_or_perspective() {
pasteboard: false,
anchor: None,
text: false,
spelling: None,
};
assert!(crate::canvas_tool_menu::available(&app, &menu, "edit.freeTransform"));
assert!(!crate::canvas_tool_menu::available(&app, &menu, "edit.transform.distort"));
+5
View File
@@ -151,6 +151,11 @@ pub fn edit_at(app: &mut PhotocraftApp, x: f64, y: f64) -> bool {
}
fn hit_offset(app: &mut PhotocraftApp, id: LayerId, x: f64, y: f64) -> usize {
offset_at(app, id, x, y)
}
/// The character offset of the caret position nearest document point (`x`, `y`) in layer `id`.
pub(crate) fn offset_at(app: &mut PhotocraftApp, id: LayerId, x: f64, y: f64) -> usize {
let Some((l, aff, text)) = layout(app, id) else { return 0 };
let (tx, ty) = to_text(&aff, x, y);
photocraft_text::hit_char(&l, &text, tx, ty).0
+2 -2
View File
@@ -27,7 +27,7 @@ The “PhotoCraft at branch point” column records behavior before this PR. Pic
| Ruler, Note, Count | Unit / measurement, note edit/delete, count-group operations **(inferred)** | No tool menu | P2 |
| Brush, Pencil, Eraser, Background Eraser, Spot Healing, Healing, Clone Stamp, History Brush, Blur, Sharpen, Smudge, Dodge, Burn, Sponge | Brush preset, size and hardness where applicable | Picker | P0; [Adobe Brush Settings](https://helpx.adobe.com/photoshop/desktop/apply-painting-techniques/brushes-presets/create-brush-set-painting-options.html). Alt+secondary-drag on Windows and Control+Option-drag on macOS adjust size/hardness ([Adobe cursor shortcuts](https://helpx.adobe.com/in/photoshop/desktop/get-started/settings-and-preferences/change-tool-pointers.html)) |
| Gradient, Paint Bucket | Gradient preset/type/mode or fill source/tolerance **(inferred context placement)** | No tool menu | P1 |
| Type | In text: cut/copy/paste, spelling suggestions, text formatting; outside text: type-layer action **(inferred context placement)** | While editing text (canvas_tool_menu.rs `TYPE_EDIT_MENU`): Undo, Cut, Copy, Paste, Select All (on the text), Check Spelling…, Find and Replace Text…, Faux Bold/Italic (checked), Rasterize Type, Create Work Path, Convert to Shape, Horizontal/Vertical and the anti-alias modes (checked), Convert to Paragraph/Point Text, Warp Text…; Esc closes only the menu. Spelling suggestions for the word under the pointer and the non-editing type-layer menu remain open | P1; separate editing and nonediting states; [Adobe type editing](https://helpx.adobe.com/photoshop/desktop/text-typography/get-started-with-text/edit-text.html) |
| Type | In text: cut/copy/paste, spelling suggestions, text formatting; outside text: type-layer action **(inferred context placement)** | While editing text (canvas_tool_menu.rs `TYPE_EDIT_MENU`): Undo, Cut, Copy, Paste, Select All (on the text), Check Spelling…, Find and Replace Text…, Faux Bold/Italic (checked), Rasterize Type, Create Work Path, Convert to Shape, Horizontal/Vertical and the anti-alias modes (checked), Convert to Paragraph/Point Text, Warp Text…; Esc closes only the menu. Over a misspelled word the menu starts with up to 3 spelling suggestions (replace the word) and Add to Dictionary (#4370); Ignore / Ignore All and the non-editing type-layer menu remain open | P1; separate editing and nonediting states; [Adobe type editing](https://helpx.adobe.com/photoshop/desktop/text-typography/get-started-with-text/edit-text.html) |
| Hand, Zoom | Fit on Screen, 100%, 200%, Print Size; zoom step choices **(inferred context placement)** | View menu (#3242): Zoom In, Zoom Out, Fit on Screen, Fill Screen, 100%, 200%, Print Size. Zoom In/Out step about the right-clicked point (Preferences › Tools › Zoom Clicked Point to Center centres it). Outside the image the pasteboard menu still wins. | P1; view actions stay out of document history; row order still needs a dated Photoshop capture |
| Pen, Path Selection | Path/point actions, Make Selection, Fill/Stroke Path, Free Transform Path **(inferred context placement)** | Pen canvas right-click follows the user-provided 20-row Photoshop capture below. Paths rows offer Make Selection, Fill and Stroke. Path Selection's own canvas menu remains open. | P1; distinguish node, segment, path, and blank canvas |
| Rectangle, Ellipse Shape, Triangle, Polygon, Line, Custom Shape | Shape/path operations, fill/stroke settings, transform **(inferred context placement)** | No tool menu | P2 |
@@ -83,7 +83,7 @@ Photoshop's Generative Fill and Delete and Fill Selection rows are left out: Pho
| Paths, History, Actions, Swatches, Gradients, Patterns, Styles, Libraries, Properties | Target-specific duplicate/delete/rename and panel operations where a panel exists | Paths row menu added in this PR; other panels need audit | **Inferred** catalog; record a Photoshop capture before claiming parity |
| Document tab | Close, Close Others, Close All, Reveal in file manager, duplicate/move to window where supported | Close, Close Others, Close All, Reveal in Finder, and drag to reorder implemented; duplicate/move to window where supported remains open | **Inferred** exact list; P1 |
| Empty pasteboard / workspace | Gray, Black, Custom canvas color choices; verify any additional entries in a current Photoshop capture | No menu | Adobe [documents right-clicking outside the image for canvas color](https://helpx.adobe.com/ph_en/photoshop/using/filling-stroking-selections-layers-paths.html); exact current layout still needs a capture |
| Text field / selected text | Native edit menu and spelling suggestions without routing to active image tool | Edit menu implemented in text_field_menu.rs for every text field (Undo, Cut, Copy, Paste, Delete, Select All; a right-click inside the selection keeps it; never reaches the canvas or tool menu). Spelling suggestions missing: `edit.checkSpelling` (`suggest`) is not wired into the menu yet (UI-217-8) | P1; **inferred** exact spelling list |
| Text field / selected text | Native edit menu and spelling suggestions without routing to active image tool | Edit menu implemented in text_field_menu.rs for every text field (Undo, Cut, Copy, Paste, Delete, Select All; a right-click inside the selection keeps it; never reaches the canvas or tool menu). Spelling suggestions live in the Type tool's edit menu (canvas_tool_menu.rs), not in generic fields, which hold no type layer text (UI-217-8) | P1; **inferred** exact spelling list |
| Tool icon in Options bar | Reset Tool, Reset All Tools | Audit | Explicitly documented in [Adobe Photoshop reference](https://helpx.adobe.com/archive/en/photoshop/cc/2015/photoshop_reference.pdf); historical source, verify modern label |
| Adaptive Wide Angle constraint line | Auto, Horizontal, Vertical orientation | Current right-click deletes constraint | P1 behavior mismatch: [Adobe documents orientation choice](https://helpx.adobe.com/photoshop/using/adaptive-wide-angle-filter.html) |
+1 -1
View File
@@ -189,7 +189,7 @@ Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_w
| UI-217-5 | Panel groups can float (menu + drag out of the dock) and are restored at launch | missing | [#217](https://github.com/storytold/photocraft/issues/217) | No floating groups in dock.rs/panels.rs |
| UI-217-6 | Document tabs: overflow list, context menu (Close/Others/All/Reveal), drag to reorder | done | [#217](https://github.com/storytold/photocraft/issues/217) | canvas.rs: Close/Others/All menu with unsaved prompt; Reveal reuses file.revealInFinder (engine layer_menu_cmds::reveal); the » chevron (tab_strip) lists overflow; dragging a tab reorders via document.move |
| UI-217-7 | Command palette shows recently run commands on an empty query | done | [#217](https://github.com/storytold/photocraft/issues/217) | palette.rs: note_recent from menus::invoke keeps the last 8 commands (dedup, newest first); an empty query lists them via recent_hits; the_palette_records_and_replays_recent_commands_on_an_empty_query covers record -> empty query -> replay |
| UI-217-8 | Tool-specific canvas right-click menu; right-click spelling suggestions in text fields | partial | [#217](https://github.com/storytold/photocraft/issues/217) | paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; spelling suggestions (edit.checkSpelling suggest) not in the text-field menu yet |
| UI-217-8 | Tool-specific canvas right-click menu; right-click spelling suggestions in text fields | partial | [#217](https://github.com/storytold/photocraft/issues/217) | paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; the Type tool's edit menu starts with spelling suggestions and Add to Dictionary over a misspelled word (#4370); Ignore / Ignore All, suggestions in the non-editing type menu and in generic text fields are not there yet |
| UI-217-9 | UI scale presets 100/115/125/150/200 % through zoom_factor, remembered | done | [#217](https://github.com/storytold/photocraft/issues/217) | prefs::UiScale offers Auto plus 75 to 300 percent (13 presets, more than the five this row lists); prefs_ui.rs display_scale resolves it (Auto follows the system scale, fractional and unscaled 4K included, #1072/#1278) and sync_display_scale applies it with ctx.set_zoom_factor. It is a preference, so it is remembered. The zoom_factor assert in input_tests.rs is about canvas zoom not scaling the interface, not about this setting |
| UI-217-10 | Appearance: animation toggle, canvas pattern toggle per theme, start-screen recents | partial | [#217](https://github.com/storytold/photocraft/issues/217) | interface.enableAnimations (prefs_ui.rs sync_animations, #3273) zeroes egui animation_time and scroll animation in both styles; the Home screen lists recent files (canvas.rs home_recent, #2691); no canvas pattern toggle yet (#2264) and no option to hide the Home screen recents |
| UI-217-11 | Soft dark and soft light themes with picker previews; outlined knobs in light themes | missing | [#217](https://github.com/storytold/photocraft/issues/217) | theme.rs ThemeKind has twelve themes (the four Pro brightness levels, Studio and Studio Light, Classic, Adwaita and Adwaita Dark, Solarized Dark, Breeze Light and Breeze Dark); no Soft dark or Soft light, and no outlined knobs in the light themes |
+1 -1
View File
@@ -59,7 +59,7 @@ id = "UI-217-8"
target = "Tool-specific canvas right-click menu; right-click spelling suggestions in text fields"
status = "partial"
issue = 217
note = "paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; spelling suggestions (edit.checkSpelling suggest) not in the text-field menu yet"
note = "paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; the Type tool's edit menu starts with spelling suggestions and Add to Dictionary over a misspelled word (#4370); Ignore / Ignore All, suggestions in the non-editing type menu and in generic text fields are not there yet"
[[item]]
id = "UI-217-9"