diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3094ce6fa1..0277fbdf43 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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 diff --git a/crates/ui-egui/src/canvas_tool_menu.rs b/crates/ui-egui/src/canvas_tool_menu.rs index b6b86111e1..7fe4fce9e3 100644 --- a/crates/ui-egui/src/canvas_tool_menu.rs +++ b/crates/ui-egui/src/canvas_tool_menu.rs @@ -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, } +/// 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, +} + +/// Most suggestions the type edit menu lists. +const MAX_SUGGESTIONS: usize = 3; +/// The spelling rows' ids: `spelling.replace.` 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 { + 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::().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(); diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index ceacb71928..e797b27cc0 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -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 { + use crate::canvas_tool_menu::{entry_enabled, rows, spelling_rows}; + let spelling = spelling_rows(menu); + let mut entries: Vec = 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::>() + "entries": context_entries(app, menu) }) }), "panels": app.ui.panels, diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 6b2c38cc89..2a9c4a9d5d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e86037b7f4..d209af709e 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ffa683b6e0..e27766fefa 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -115,6 +115,8 @@ Search… Αναζήτηση… Check Spelling… Ορθογραφικός έλεγχος… Find and Replace Text… Εύρεση και αντικατάσταση κειμένου… + Add to Dictionary Προσθήκη στο λεξικό + No Spelling Suggestions Δεν υπάρχουν ορθογραφικές προτάσεις Fill… Γέμισμα… Stroke… Περίγραμμα… Preferences… Προτιμήσεις… diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 49b56d3520..62886211e8 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1e30b6a39e..7789b2c02b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c3a74e745f..1c9f15f0ba 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 30fecce4c2..cb77490c4b 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 12a8c283ea..288353a61b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index fafb869f8e..26a31ce4de 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -132,6 +132,8 @@ Search… 検索… Check Spelling… スペルチェック… Find and Replace Text… テキストを検索して置換… + Add to Dictionary 辞書に追加 + No Spelling Suggestions スペルの候補はありません Fill… 塗りつぶし… Stroke… 境界線を描く… Content-Aware Fill… コンテンツに応じた塗りつぶし… diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 3609ad270d..b6a95907d3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -121,6 +121,8 @@ Search… 검색… Check Spelling… 맞춤법 검사… Find and Replace Text… 텍스트 찾기 및 바꾸기… + Add to Dictionary 사전에 추가 + No Spelling Suggestions 맞춤법 제안 없음 Fill… 채우기… Stroke… 획… Content-Aware Fill… 내용 인식 채우기… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index abc3f3f5c6..1ec6d64b0f 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ddda4d88d4..fab352b28a 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 05408b834e..b7c0def324 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 6e5ee708ae..2f174fd7b9 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 Стили текста diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 4305ff7faa..e9dc17d8d4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index de4e42fc58..0facb9e9cc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 Текстові стилі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 5ed7ec483a..03ad654f76 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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ạ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 29d2b57ea6..080a5ac03d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -130,6 +130,8 @@ Search… 搜索… Check Spelling… 检查拼写… Find and Replace Text… 查找和替换文本… + Add to Dictionary 添加到词典 + No Spelling Suggestions 没有拼写建议 Fill… 填充… Stroke… 描边… Content-Aware Fill… 内容识别填充… diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 36f523934d..3ed91649c2 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -138,6 +138,8 @@ Search… 搜尋… Check Spelling… 拼字檢查… Find and Replace Text… 尋找並取代文字… + Add to Dictionary 加入字典 + No Spelling Suggestions 沒有拼字建議 Fill… 填滿… Stroke… 筆畫… Content-Aware Fill… 內容感知填滿… diff --git a/crates/ui-egui/src/text_field_menu.rs b/crates/ui-egui/src/text_field_menu.rs index 420263e7ef..6556c68cfc 100644 --- a/crates/ui-egui/src/text_field_menu.rs +++ b/crates/ui-egui/src/text_field_menu.rs @@ -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}; diff --git a/crates/ui-egui/src/transform_type_tests.rs b/crates/ui-egui/src/transform_type_tests.rs index ea3c128a18..dcbbb70b3c 100644 --- a/crates/ui-egui/src/transform_type_tests.rs +++ b/crates/ui-egui/src/transform_type_tests.rs @@ -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")); diff --git a/crates/ui-egui/src/type_tool.rs b/crates/ui-egui/src/type_tool.rs index dc99138a32..6a7c816084 100644 --- a/crates/ui-egui/src/type_tool.rs +++ b/crates/ui-egui/src/type_tool.rs @@ -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 diff --git a/docs/context-menu-parity.md b/docs/context-menu-parity.md index 425c91f42f..739e445cdc 100644 --- a/docs/context-menu-parity.md +++ b/docs/context-menu-parity.md @@ -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) | diff --git a/docs/scorecard.md b/docs/scorecard.md index 651d68309f..d25198c082 100644 --- a/docs/scorecard.md +++ b/docs/scorecard.md @@ -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 | diff --git a/scorecard/ui.toml b/scorecard/ui.toml index 3fa79b59fa..54cc13876c 100644 --- a/scorecard/ui.toml +++ b/scorecard/ui.toml @@ -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"