diff --git a/crates/engine/data/thesaurus.txt b/crates/engine/data/thesaurus.txt new file mode 100644 index 00000000..1916eb46 --- /dev/null +++ b/crates/engine/data/thesaurus.txt @@ -0,0 +1,264 @@ +# DeckCraft's built-in thesaurus: headword|synonym, synonym, ... +# Written for DeckCraft (no third-party word list). Lowercase; one meaning group per line; +# a headword may appear on several lines (one per sense). +ability|capability, capacity, skill, competence, talent +able|capable, competent, qualified, skilled +accept|agree to, approve, adopt, embrace +accurate|precise, exact, correct, right +achieve|accomplish, attain, reach, realize, deliver +achievement|accomplishment, success, attainment, milestone +add|include, append, attach, insert +advantage|benefit, edge, strength, asset, upside +affect|influence, impact, change, alter +aim|goal, objective, target, purpose, intent +allow|permit, enable, let, authorize +analyze|examine, study, assess, evaluate, review +answer|reply, response, solution +approach|method, strategy, way, technique, plan +area|region, field, domain, zone, sector +arrange|organize, order, sort, plan +ask|request, inquire, query, question +assess|evaluate, gauge, judge, review, appraise +assist|help, aid, support, back +attempt|try, effort, endeavor, bid +bad|poor, weak, inferior, unfavorable, negative +basic|fundamental, essential, core, elementary, simple +begin|start, commence, launch, initiate, open +benefit|advantage, gain, value, payoff, upside +best|finest, top, leading, optimal, premier +big|large, major, substantial, significant, sizable +boost|increase, raise, lift, improve, strengthen +brief|short, concise, succinct, quick +bright|vivid, brilliant, luminous, clever +build|construct, create, develop, assemble, establish +business|company, firm, enterprise, organization, venture +buy|purchase, acquire, obtain +careful|cautious, thorough, meticulous, attentive +cause|reason, source, origin, driver +change|alter, modify, adjust, revise, transform +change|shift, transition, adjustment, revision +cheap|inexpensive, affordable, low-cost, economical +check|verify, confirm, examine, inspect, review +choice|option, alternative, selection, pick +choose|select, pick, opt for, decide on +clear|plain, obvious, evident, transparent, lucid +client|customer, buyer, account, patron +close|near, nearby, adjacent +close|end, finish, conclude, shut +common|frequent, usual, typical, widespread, shared +company|business, firm, organization, enterprise, corporation +complete|finish, conclude, finalize, accomplish +complete|whole, full, entire, total +complex|complicated, intricate, involved, elaborate +concern|worry, issue, problem, risk +consider|weigh, contemplate, evaluate, think about +continue|proceed, carry on, keep going, persist, maintain +cost|price, expense, charge, outlay +create|make, build, produce, design, generate +critical|crucial, vital, essential, key, decisive +current|present, existing, ongoing, latest, up-to-date +customer|client, buyer, consumer, user +data|information, figures, facts, statistics +decide|determine, choose, resolve, settle +decrease|reduce, lower, cut, decline, drop +define|specify, describe, explain, outline +deliver|provide, supply, bring, hand over, ship +demonstrate|show, prove, illustrate, display +describe|explain, outline, portray, depict +design|plan, layout, blueprint, pattern +design|create, plan, devise, draft +develop|build, create, grow, expand, evolve +difference|distinction, contrast, gap, variation +different|distinct, various, diverse, unlike +difficult|hard, challenging, tough, demanding +discuss|talk about, debate, examine, review +easy|simple, straightforward, effortless, painless +effect|result, impact, outcome, consequence +effective|successful, efficient, productive, powerful +effort|work, attempt, endeavor, exertion +end|finish, conclusion, close, completion +enough|sufficient, adequate, ample +ensure|guarantee, make sure, secure, confirm +error|mistake, fault, flaw, bug, slip +essential|vital, crucial, key, necessary, fundamental +establish|set up, found, create, institute +estimate|forecast, projection, approximation, guess +evaluate|assess, appraise, judge, rate, review +event|occasion, function, happening, incident +example|instance, case, illustration, sample +excellent|outstanding, superb, exceptional, first-rate +expand|grow, extend, enlarge, broaden, scale +expect|anticipate, foresee, predict, await +explain|clarify, describe, illustrate, account for +fail|fall short, miss, flop, break down +fast|quick, rapid, swift, speedy +feature|characteristic, attribute, capability, function +final|last, closing, concluding, ultimate +find|discover, locate, identify, uncover +finish|complete, end, conclude, wrap up +focus|concentrate, center, target, emphasize +focus|emphasis, priority, attention, core +future|upcoming, coming, forthcoming, prospective +gain|increase, rise, improvement, profit +get|obtain, acquire, receive, gain +give|provide, offer, supply, grant +goal|aim, objective, target, purpose, ambition +good|fine, great, strong, solid, positive +great|excellent, outstanding, terrific, remarkable +grow|increase, expand, rise, develop, scale +growth|expansion, increase, development, rise +help|assist, aid, support, enable +help|assistance, aid, support, guidance +high|elevated, tall, top, strong +idea|concept, notion, thought, proposal +identify|recognize, pinpoint, detect, determine +impact|effect, influence, consequence, result +important|significant, key, major, crucial, vital +improve|enhance, upgrade, refine, strengthen, boost +include|contain, cover, incorporate, comprise +increase|raise, boost, grow, expand, lift +increase|rise, growth, gain, jump +information|data, details, facts, knowledge +innovative|inventive, creative, original, novel, pioneering +issue|problem, concern, matter, question +job|task, assignment, role, position +keep|retain, maintain, preserve, hold +key|main, central, critical, essential, principal +large|big, sizable, substantial, major, extensive +last|final, latest, previous, closing +launch|start, introduce, roll out, release, unveil +lead|guide, direct, head, steer, run +learn|discover, find out, master, study +less|fewer, smaller, reduced, lower +little|small, minor, slight, modest +look|appear, seem +look|view, glance, examine, inspect +low|minimal, modest, small, reduced +main|primary, chief, principal, central, key +maintain|keep, preserve, sustain, uphold +major|significant, important, key, principal, big +make|create, build, produce, generate +manage|run, handle, oversee, direct, lead +market|industry, sector, segment, marketplace +measure|gauge, assess, quantify, track +meet|satisfy, fulfil, reach, achieve +meeting|session, conference, discussion, review +method|approach, technique, process, procedure, way +model|framework, template, pattern, design +more|additional, extra, further, added +move|shift, transfer, relocate, transition +need|requirement, demand, necessity +need|require, call for, demand, want +new|fresh, recent, novel, modern, latest +next|following, upcoming, subsequent +number|figure, count, total, quantity, amount +objective|goal, aim, target, purpose +offer|provide, propose, extend, present +old|previous, former, legacy, outdated, existing +open|start, launch, begin, unveil +opportunity|opening, chance, prospect, possibility +option|choice, alternative, possibility, route +organize|arrange, structure, coordinate, order +outcome|result, consequence, effect, end result +overview|summary, outline, synopsis, recap +part|portion, piece, segment, section, component +partner|ally, collaborator, associate, affiliate +people|staff, team, individuals, personnel +plan|strategy, scheme, roadmap, proposal, program +plan|organize, arrange, schedule, prepare +point|aspect, detail, issue, argument +popular|well-liked, widespread, in demand, favored +possible|feasible, achievable, viable, potential +present|show, display, introduce, demonstrate +present|current, existing, ongoing +price|cost, rate, charge, fee +priority|focus, concern, precedence, goal +problem|issue, challenge, difficulty, obstacle +process|procedure, method, workflow, system +produce|make, create, generate, manufacture, yield +product|offering, item, solution, good +profit|earnings, return, gain, margin +progress|advance, headway, development, improvement +project|initiative, program, undertaking, venture +propose|suggest, recommend, put forward, offer +provide|give, supply, deliver, offer +purpose|aim, goal, objective, intent, reason +quality|standard, caliber, excellence, grade +question|query, issue, inquiry +quick|fast, rapid, speedy, swift, prompt +rapid|fast, quick, swift, speedy +reach|achieve, attain, hit, arrive at +reason|cause, rationale, motive, basis +recent|latest, new, current, fresh +recommend|suggest, advise, propose, advocate +reduce|cut, lower, decrease, lessen, trim +region|area, territory, zone, market +relevant|pertinent, applicable, related, appropriate +remove|delete, eliminate, take out, drop +replace|substitute, swap, supersede, change +report|account, summary, review, statement +require|need, demand, call for, necessitate +research|study, investigation, analysis, inquiry +resource|asset, supply, means, capacity +result|outcome, effect, consequence, finding +review|assessment, evaluation, examination, analysis +review|examine, assess, evaluate, check, revisit +revenue|income, sales, earnings, turnover +risk|danger, threat, exposure, hazard +role|part, function, position, responsibility +run|operate, manage, conduct, execute +safe|secure, protected, reliable +same|identical, equal, matching, equivalent +save|keep, store, preserve, conserve +see|view, observe, notice, spot +sell|market, offer, trade, retail +service|support, assistance, offering +share|portion, part, stake, percentage +share|distribute, spread, communicate, pass on +show|demonstrate, display, present, reveal, illustrate +significant|important, notable, considerable, major, meaningful +similar|alike, comparable, related, equivalent +simple|easy, straightforward, plain, basic +size|scale, extent, magnitude, dimensions +small|little, minor, compact, modest +solution|answer, fix, remedy, resolution +solve|resolve, fix, settle, work out +specific|particular, precise, exact, distinct +start|begin, launch, commence, kick off, initiate +step|stage, phase, measure, action +strategy|plan, approach, policy, roadmap, tactic +strong|robust, solid, powerful, sturdy +structure|framework, organization, layout, arrangement +success|achievement, accomplishment, win, triumph +successful|effective, thriving, prosperous, winning +suggest|propose, recommend, advise, imply +summary|overview, recap, synopsis, digest +support|help, back, assist, sustain +support|help, assistance, backing, aid +sure|certain, confident, positive, definite +target|goal, objective, aim +task|job, assignment, duty, activity +team|group, crew, staff, squad, unit +test|trial, check, experiment, evaluation +test|try, check, evaluate, verify +think|believe, consider, reckon, suppose +time|period, duration, while, moment +tool|instrument, utility, device, aid +total|sum, whole, aggregate, overall +trend|pattern, tendency, movement, direction +understand|grasp, comprehend, follow, realize +unique|distinctive, singular, one-of-a-kind, unmatched +update|revise, refresh, upgrade, modernize +update|revision, change, upgrade, news +use|employ, apply, utilize, leverage +useful|helpful, practical, handy, valuable +value|worth, benefit, merit, importance +various|diverse, assorted, different, several +view|opinion, perspective, outlook, position +vision|outlook, aspiration, goal, dream +way|method, approach, manner, means +weak|feeble, fragile, poor, limited +well|effectively, successfully, nicely, properly +work|labor, effort, activity, job +work|function, operate, run, succeed +year|annum, twelve months diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 8e3a532c..bf3b3920 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -16,6 +16,16 @@ pub fn specs() -> Vec { cmd!("comment.resolve", "Resolve Thread", ["Comments"], None, "{index, resolved?: bool}", has_slide, resolve), cmd!(query "comment.list", "Comments", [], None, "{slide?} → comments", has_slide, list), cmd!(query "review.accessibility", "Check Accessibility", ["Review", "Accessibility"], None, "{} → [{slide, shape, issue}]", has_doc, accessibility), + cmd!(query "review.thesaurus", "Thesaurus", ["Review", "Proofing"], None, "{word?: default the selected word, or the word at the caret} → {word, senses: [{word, synonyms}]}", has_doc, thesaurus), + cmd!( + "review.replaceWord", + "Insert Synonym", + [], + None, + "{with}: replace the selected word (or the word at the caret) while editing text", + has_text, + replace_word + ), cmd!(query "review.spelling", "Spelling", ["Review", "Proofing"], Some("F7"), "{} → [{slide, shape, word}] (words not in the built-in list)", has_doc, spelling), cmd!(noundo "show.fromStart", "Play from Start", ["Slide Show", "Start Slide Show"], Some("Cmd+Shift+Enter"), "{}", has_slide, |s, _| start(s, 0)), cmd!(noundo "show.fromCurrent", "Play from Current Slide", ["Slide Show", "Start Slide Show"], Some("Cmd+Enter"), "{}", has_slide, |s, _| { @@ -264,3 +274,36 @@ fn custom_show(s: &mut Session, p: &Value) -> Result { })?; ok() } + +/// The selected text when it lies in one paragraph, else the word at the caret. +fn word_under_caret(s: &Session) -> Option { + let st = s.doc().ok()?; + let t = st.selection.text.clone()?; + let body = super::text::body_of(st, &t)?; + let (a, b) = if t.anchor != t.caret { t.ordered() } else { deckcraft_model::edit::word_at(body, t.caret) }; + if a.0 != b.0 { + return None; + } + let text = body.paragraphs.get(a.0)?.text(); + let w: String = text.chars().skip(a.1).take(b.1.saturating_sub(a.1)).collect(); + let w = w.trim().to_string(); + (!w.is_empty()).then_some(w) +} + +fn thesaurus(s: &mut Session, p: &Value) -> Result { + let word = str_param(p, "word").map(str::to_string).or_else(|| word_under_caret(s)).unwrap_or_default(); + let senses = crate::thesaurus::lookup(&word); + Ok(json!({"word": word, "senses": senses})) +} + +fn replace_word(s: &mut Session, p: &Value) -> Result { + let with = str_param(p, "with").filter(|w| !w.trim().is_empty()).ok_or_else(|| bad("review.replaceWord", "missing `with`"))?.to_string(); + if with.chars().count() > 200 { + return Err(bad("review.replaceWord", "too long")); + } + let collapsed = s.doc()?.selection.text.as_ref().is_some_and(|t| t.anchor == t.caret); + if collapsed { + s.execute("text.selectWord", &json!({}))?; + } + s.execute("text.insert", &json!({"text": with})) +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c2b56c7f..398801d4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -15,6 +15,7 @@ pub mod keys; pub mod links; pub mod recovery; pub mod sample; +pub mod thesaurus; pub mod tools; use std::sync::Arc; diff --git a/crates/engine/src/thesaurus.rs b/crates/engine/src/thesaurus.rs new file mode 100644 index 00000000..10cc207d --- /dev/null +++ b/crates/engine/src/thesaurus.rs @@ -0,0 +1,119 @@ +//! Review › Thesaurus: synonyms from a small built-in English word list (`data/thesaurus.txt`), +//! looked up offline so it works in the browser build too. + +const DATA: &str = include_str!("../data/thesaurus.txt"); + +/// One sense of a word: its headword and synonyms. +#[derive(Clone, Debug, PartialEq, serde::Serialize)] +pub struct Sense { + pub word: String, + pub synonyms: Vec, +} + +fn senses_of(head: &str) -> Vec { + DATA.lines() + .filter(|l| !l.starts_with('#')) + .filter_map(|l| l.split_once('|')) + .filter(|(h, _)| h.trim() == head) + .map(|(h, s)| Sense { word: h.trim().to_string(), synonyms: s.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect() }) + .collect() +} + +/// The senses for `word`, trying simple English endings when the word itself isn't listed +/// ("goals" → "goal", "improved" → "improve", "growing" → "grow"). Synonyms take the word's +/// capitalisation ("Goal" → "Aim", "GOAL" → "AIM"). +pub fn lookup(word: &str) -> Vec { + let w: String = word.trim().trim_matches(|c: char| !c.is_alphanumeric() && c != '-').chars().take(64).collect(); + let lower = w.to_lowercase(); + if lower.is_empty() { + return vec![]; + } + let mut tries = vec![lower.clone()]; + for (suffix, add) in [("ies", "y"), ("es", ""), ("s", ""), ("ied", "y"), ("ed", "e"), ("ed", ""), ("ing", "e"), ("ing", "")] { + if let Some(stem) = lower.strip_suffix(suffix) + && stem.chars().count() >= 2 + { + tries.push(format!("{stem}{add}")); + } + } + let Some(found) = tries.iter().map(|t| senses_of(t)).find(|v| !v.is_empty()) else { return vec![] }; + let upper = w.chars().count() > 1 && w.chars().all(|c| !c.is_lowercase()); + let title = !upper && w.chars().next().is_some_and(char::is_uppercase); + found + .into_iter() + .map(|s| Sense { + word: s.word, + synonyms: s + .synonyms + .into_iter() + .map(|x| { + if upper { + x.to_uppercase() + } else if title { + let mut c = x.chars(); + c.next().map(|f| f.to_uppercase().chain(c).collect()).unwrap_or_default() + } else { + x + } + }) + .collect(), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn senses_endings_and_case() { + let g = lookup("goal"); + assert_eq!(g.len(), 1); + assert!(g[0].synonyms.contains(&"objective".to_string())); + assert_eq!(lookup("change").len(), 2, "verb and noun senses"); + assert_eq!(lookup("Goals")[0].synonyms[0], "Aim"); + assert_eq!(lookup("GOAL")[0].synonyms[0], "AIM"); + assert_eq!(lookup("improved")[0].word, "improve"); + assert_eq!(lookup("growing")[0].word, "grow"); + assert_eq!(lookup("strategies")[0].word, "strategy"); + assert_eq!(lookup("“goal,”")[0].word, "goal", "punctuation around the word"); + assert!(lookup("").is_empty() && lookup("zzxq").is_empty() && lookup(" ").is_empty()); + assert!(lookup(&"a".repeat(10_000)).is_empty()); + } + + #[test] + fn data_is_well_formed() { + for l in DATA.lines().filter(|l| !l.starts_with('#') && !l.trim().is_empty()) { + let (h, s) = l.split_once('|').unwrap_or_else(|| panic!("no `|`: {l}")); + assert_eq!(h, h.trim().to_lowercase(), "{l}"); + let syn: Vec<&str> = s.split(',').map(str::trim).collect(); + assert!(syn.len() >= 2 && syn.iter().all(|x| !x.is_empty() && *x != h), "{l}"); + let mut d = syn.clone(); + d.sort(); + d.dedup(); + assert_eq!(d.len(), syn.len(), "duplicate synonym: {l}"); + } + } + + #[test] + fn commands_look_up_and_replace_the_word_at_the_caret() { + use serde_json::json; + let mut s = crate::Session::with_new(); + let id = s.execute("insert.textBox", &json!({"rect": [50, 50, 400, 40]})).unwrap()["id"].as_u64().unwrap(); + s.execute("text.edit", &json!({"id": id})).unwrap(); + s.execute("text.insert", &json!({"text": "Our Goal is growth."})).unwrap(); + s.execute("text.edit", &json!({"id": id, "at": [0, 6]})).unwrap(); + let r = s.execute("review.thesaurus", &json!({})).unwrap(); + assert_eq!(r["word"], "Goal"); + assert_eq!(r["senses"][0]["synonyms"][0], "Aim"); + s.execute("review.replaceWord", &json!({"with": "Aim"})).unwrap(); + let text = |s: &crate::Session| { + s.doc().unwrap().shapes().iter().find(|x| x.id.0 as u64 == id).and_then(|x| x.text.as_ref()).map(|t| t.paragraphs[0].text()).unwrap() + }; + assert_eq!(text(&s), "Our Aim is growth."); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(text(&s), "Our Goal is growth."); + assert_eq!(s.execute("review.thesaurus", &json!({"word": "growth."})).unwrap()["senses"][0]["word"], "growth"); + assert!(s.execute("review.replaceWord", &json!({"with": ""})).is_err()); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ec97e6e6..d1a82cb7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -146,6 +146,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "{}"), ("view.zoom", "Zoom…", None, "{percent}"), ("view.fit", "Fit to Window", None, "{}"), + ("review.thesaurusPane", "Thesaurus", Some("Shift+F7"), "{word?}: open the Thesaurus pane on the selected word"), ("view.pane", "Task Pane", None, "{pane: format|animation|selection|comments|designer|background|null}"), ("view.tab", "Ribbon Tab", None, "{tab}"), ("view.collapseRibbon", "Collapse the Ribbon", Some("Cmd+F1"), "{}"), @@ -184,6 +185,8 @@ pub struct SlideApp { pub textures: textures::Textures, pub control_rx: Option>, pub dialog: Option, + /// The word looked up in the Thesaurus pane. + pub thesaurus_query: String, pub show: Option, /// Audio/video playback (editor control bar, slide show, `media.*` commands). pub media: media::MediaHost, @@ -239,6 +242,7 @@ impl SlideApp { textures: textures::Textures::default(), control_rx: None, dialog: None, + thesaurus_query: String::new(), show: None, media: media::MediaHost::new(audio_out), status: None, @@ -473,6 +477,19 @@ impl SlideApp { self.ui.zoom = Some((pct as f32 / 100.0).clamp(0.1, 4.0)); } "view.fit" => self.ui.zoom = None, + "review.thesaurusPane" => { + let word = match p.get("word").and_then(Value::as_str) { + Some(w) => w.to_string(), + None => self + .session + .execute("review.thesaurus", &json!({})) + .ok() + .and_then(|r| r["word"].as_str().map(String::from)) + .unwrap_or_default(), + }; + self.thesaurus_query = word; + self.ui.pane = Some("thesaurus".into()); + } "view.pane" => { let pane = p.get("pane").and_then(Value::as_str).map(String::from); self.ui.pane = diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index 2b3ada3a..0cf608e6 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -27,6 +27,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { "selection" => "Selection", "comments" => "Comments", "designer" => "Design Ideas", + "thesaurus" => "Thesaurus", _ => "Pane", }; ui.horizontal(|ui| { @@ -50,6 +51,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { "selection" => crate::selpane::show(app, ui), "comments" => comments(app, ui), "designer" => designer(app, ui), + "thesaurus" => thesaurus(app, ui), _ => {} }); }); @@ -635,3 +637,42 @@ fn designer(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(8.0); } } + +/// Review › Thesaurus: look a word up; clicking a synonym puts it in place of the selected word +/// when editing text, otherwise looks that synonym up in turn. +fn thesaurus(app: &mut SlideApp, ui: &mut Ui) { + let t = Tokens::get(ui.ctx()); + let mut q = app.thesaurus_query.clone(); + let edit = ui.add(egui::TextEdit::singleline(&mut q).hint_text("Search for a word").desired_width(f32::INFINITY)); + if edit.changed() { + app.thesaurus_query = q.clone(); + } + ui.add_space(6.0); + let senses = deckcraft_engine::thesaurus::lookup(&q); + if q.trim().is_empty() { + ui.label(egui::RichText::new("Select a word on the slide, or type one above.").color(t.text_dim)); + return; + } + if senses.is_empty() { + ui.label(egui::RichText::new(format!("No synonyms for \"{}\".", q.trim())).color(t.text_dim)); + return; + } + let editing = app.session.active().is_some_and(|d| d.selection.text.is_some()); + for sense in senses { + ui.label(egui::RichText::new(&sense.word).font(theme::bold(13.0))); + for syn in sense.synonyms { + let r = ui.add(egui::Button::new(&syn).frame(false)).on_hover_text(if editing { "Insert" } else { "Look up" }); + if r.clicked() { + if editing { + run(app, "review.replaceWord", json!({"with": syn})); + } + app.thesaurus_query = syn; + } + } + ui.add_space(6.0); + } +} + +#[cfg(test)] +#[path = "thesaurus_tests.rs"] +mod thesaurus_tests; diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 428eff0b..d9c9f39c 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1780,8 +1780,7 @@ fn record(app: &mut SlideApp, ui: &mut Ui) { fn review(app: &mut SlideApp, ui: &mut Ui) { group(ui, |ui| { big(app, ui, Icon::Spelling, "Spelling", "app.dialog", json!({"id": "spelling"})); - let r = big_button(ui, Icon::Thesaurus, "Thesaurus", false); - let _ = r.on_hover_text("Thesaurus (coming soon)"); + big(app, ui, Icon::Thesaurus, "Thesaurus", "review.thesaurusPane", json!({})); }); group(ui, |ui| { big(app, ui, Icon::Accessibility, "Check\nAccessibility", "app.dialog", json!({"id": "accessibility"})); diff --git a/crates/ui-egui/src/thesaurus_tests.rs b/crates/ui-egui/src/thesaurus_tests.rs new file mode 100644 index 00000000..985e6401 --- /dev/null +++ b/crates/ui-egui/src/thesaurus_tests.rs @@ -0,0 +1,54 @@ +//! Review › Thesaurus driven like a person: caret in a word, open the pane, click a synonym. + +use deckcraft_engine::Session; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use serde_json::json; + +use crate::{Services, SlideApp}; + +#[test] +fn thesaurus_pane_swaps_the_word_at_the_caret() { + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + let id = s.execute("insert.textBox", &json!({"rect": [100, 100, 500, 50]})).unwrap()["id"].as_u64().unwrap(); + s.execute("text.edit", &json!({"id": id})).unwrap(); + s.execute("text.insert", &json!({"text": "Our Goal is growth."})).unwrap(); + s.execute("text.edit", &json!({"id": id, "at": [0, 6]})).unwrap(); + let mut app = SlideApp::new(s, Services::default()); + app.ui.tab = "Review".into(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + h.get_by_label("Thesaurus").click(); + h.run_steps(3); + assert_eq!(h.state().ui.pane.as_deref(), Some("thesaurus")); + assert_eq!(h.state().thesaurus_query, "Goal"); + if let Ok(img) = h.render() { + let _ = img.save("/tmp/thesaurus.png"); + } + h.get_by_label("Objective").click(); + h.run_steps(3); + let text = h + .state() + .session + .active() + .unwrap() + .shapes() + .iter() + .find(|x| x.id.0 as u64 == id) + .and_then(|x| x.text.as_ref()) + .map(|t| t.paragraphs[0].text()); + assert_eq!(text.as_deref(), Some("Our Objective is growth.")); + assert_eq!(h.state().thesaurus_query, "Objective", "the pane follows the new word"); + + // Typing a word that isn't listed says so instead of showing nothing. + h.state_mut().thesaurus_query = "zzxq".into(); + h.run_steps(2); + assert!(h.query_by_label_contains("No synonyms").is_some()); +} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 5827b396..97480599 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -39,6 +39,9 @@ pub fn big_button_ex(ui: &mut Ui, icon: Icon, label: &str, enabled: bool, split: let text_w = lines.iter().map(|l| ui.fonts_mut(|f| f.layout_no_wrap(l.to_string(), font.clone(), t.text).size().x)).fold(0.0f32, f32::max); let w = (text_w + 10.0 + if split && lines.len() == 1 { 10.0 } else { 0.0 }).max(40.0); let (rect, resp) = ui.allocate_exact_size(vec2(w, RIBBON_H - 2.0), if enabled { Sense::click() } else { Sense::hover() }); + // Named for screen readers and UI tests ("Ink to\nShape" reads "Ink to Shape"). + let spoken = label.replace('\n', " "); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, &spoken)); let painter = ui.painter_at(rect.expand(1.0)); let (main_rect, arrow_rect) = if split { (Rect::from_min_max(rect.min, pos2(rect.max.x, rect.min.y + 38.0)), Some(Rect::from_min_max(pos2(rect.min.x, rect.min.y + 38.0), rect.max))) diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 2a3d450c..fb8e3698 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -9,7 +9,7 @@ The summary between ` |---|---|---|---|---|---| | P0 | 72 | 61 | 11 | 0 | 92% | | P1 | 65 | 38 | 21 | 6 | 75% | -| P2 | 54 | 13 | 15 | 26 | 38% | +| P2 | 54 | 13 | 16 | 25 | 39% | | Area | Parity | |---|---| @@ -21,7 +21,7 @@ The summary between ` | Shapes | 88% | | Format | 83% | | Arrange | 100% | -| Text | 80% | +| Text | 81% | | Tables | 89% | | Charts | 65% | | SmartArt | 33% | @@ -155,7 +155,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou | Text | Text direction (rotated, stacked) | P1 | D | | Text | Find and replace, replace fonts | P1 | P | | Text | Spelling check | P1 | P | -| Text | Thesaurus | P2 | M | +| Text | Thesaurus | P2 | P | | Text | AutoCorrect and smart quotes | P2 | M | | Text | Symbols and emoji | P1 | D | | Text | Equations | P2 | M |