1use alloc::collections::BTreeMap;
39use alloc::string::String;
40use alloc::sync::Arc;
41use alloc::vec::Vec;
42
43use rt::sync::SpinLock;
44use spell::{Dictionary, Source, Speller, Suggester, Token, SRC_ALL, SRC_APP};
45
46use crate::consts::*;
47use crate::i18n;
48use crate::keys::*;
49use crate::Ui;
50
51const BUDGET: u32 = 200; const BUDGET_US: u64 = 6000; const SUG_SHOWN: usize = 5;
54const BOXES_MAX: usize = 64; const MOD3: u32 = MOD_SHIFT | MOD_CTRL | MOD_ALT;
56
57#[derive(Default)]
59struct BoxSpell {
60 hash: u64,
61 generation: u32,
62 rev: u32,
63 toks: Vec<Token>,
64 next: usize, bad: Vec<(usize, usize)>,
66 seen: u32,
67}
68
69struct Menu {
71 id: u32,
72 pid: u32, start: usize,
74 end: usize,
75 word: String,
76 sugs: Vec<String>,
77 wait: u32, add_row: u32, kbd: bool, rect: [i32; 4], caret: Option<(usize, usize)>, }
83
84enum Act {
85 Use(usize),
86 Add,
87 Ignore,
88}
89
90type Loaded = (u32, Option<Dictionary>, String);
92
93type Sugs = (u32, Option<Vec<String>>);
95
96pub(crate) struct S {
97 off: bool,
98 lang: String,
99 sources: u32,
100 state: u32,
101 speller: Option<Speller>,
102 generation: u32,
103 load: u32, job: Option<plat::Job>,
105 slot: Arc<SpinLock<Option<Loaded>>>,
106 boxes: BTreeMap<u32, BoxSpell>,
107 budget: u32,
108 budget_frame: u32,
109 budget_until: u64, menu: Option<Menu>,
111 sug_async: Option<bool>, sug_n: u32, sug_jobs: Vec<plat::Job>,
114 sug_slot: Arc<SpinLock<Sugs>>,
115}
116
117impl Default for S {
118 fn default() -> S {
119 S {
120 off: false,
121 lang: String::new(),
122 sources: 0,
123 state: SPELL_OFF,
124 speller: None,
125 generation: 0,
126 load: 0,
127 job: None,
128 slot: Arc::new(SpinLock::new(None)),
129 boxes: BTreeMap::new(),
130 budget: 0,
131 budget_frame: 0,
132 budget_until: 0,
133 menu: None,
134 sug_async: None,
135 sug_n: 0,
136 sug_jobs: Vec::new(),
137 sug_slot: Arc::new(SpinLock::new((0, None))),
138 }
139 }
140}
141
142impl S {
143 fn reset(&mut self) {
145 self.speller = None;
146 self.state = SPELL_OFF;
147 self.load = self.load.wrapping_add(1);
148 self.generation = self.generation.wrapping_add(1);
149 self.menu = None;
150 }
151
152 fn sug_start(&mut self, sg: Suggester, word: &str) -> u32 {
155 self.sug_n = self.sug_n.wrapping_add(1).max(1);
156 let n = self.sug_n;
157 *self.sug_slot.lock() = (n, None);
158 let i = match self.sug_jobs.iter().position(|j| j.done()) {
159 Some(i) => i,
160 None => {
161 self.sug_jobs.push(plat::Job::default());
162 self.sug_jobs.len() - 1
163 }
164 };
165 let slot = self.sug_slot.clone();
166 let w = String::from(word);
167 let job = &mut self.sug_jobs[i];
168 job.set(1, move |_, _| {
169 if slot.lock().0 != n {
170 return; }
172 let v = sg.suggest(&w, SUG_SHOWN);
173 let mut g = slot.lock();
174 if g.0 == n {
175 g.1 = Some(v);
176 }
177 });
178 job.start();
179 n
180 }
181}
182
183fn fnv(b: &[u8]) -> u64 {
185 let mut h: u64 = 0xCBF2_9CE4_8422_2325;
186 for &c in b {
187 h = (h ^ c as u64).wrapping_mul(0x0000_0100_0000_01B3);
188 }
189 h
190}
191
192impl Ui {
193 pub fn spell_enable(&mut self, on: bool) {
195 let s = &mut self.m.edit.spell;
196 if s.off == on {
197 s.off = !on;
198 s.menu = None;
199 self.repass();
200 }
201 }
202
203 pub fn spell_language(&mut self, tag: &str) {
205 let s = &mut self.m.edit.spell;
206 if s.lang != tag {
207 s.lang = String::from(tag);
208 s.reset();
209 self.repass();
210 }
211 }
212
213 pub fn spell_sources(&mut self, sources: u32) {
215 let s = &mut self.m.edit.spell;
216 if s.sources != sources {
217 s.sources = sources;
218 s.reset();
219 self.repass();
220 }
221 }
222
223 pub fn spell_set(&mut self, sp: Speller) {
225 let s = &mut self.m.edit.spell;
226 s.reset();
227 s.speller = Some(sp);
228 s.state = SPELL_READY;
229 self.repass();
230 }
231
232 pub fn spell_suggest_async(&mut self, on: bool) {
234 self.m.edit.spell.sug_async = Some(on);
235 }
236
237 pub fn spell_state(&self) -> u32 {
239 let s = &self.m.edit.spell;
240 if s.off { SPELL_OFF } else { s.state }
241 }
242
243 pub fn speller(&mut self) -> Option<&mut Speller> {
245 self.spell_ensure();
246 self.m.edit.spell.speller.as_mut()
247 }
248
249 pub fn spell_errors(&self, id: u32) -> Vec<(usize, usize)> {
251 let s = &self.m.edit.spell;
252 if s.off || s.speller.is_none() {
253 return Vec::new();
254 }
255 s.boxes.get(&id).filter(|b| b.generation == s.generation).map_or_else(Vec::new, |b| b.bad.clone())
256 }
257
258 fn spell_personal(&self, sp: &mut Speller) {
260 if !self.headless {
261 let p = spell::personal_path(sp.language());
262 sp.set_personal_file(Some(&p));
263 }
264 }
265
266 fn spell_ensure(&mut self) {
268 if self.m.edit.spell.off {
269 return;
270 }
271 let r = self.m.edit.spell.slot.lock().take();
273 if let Some((n, dict, lang)) = r {
274 if n == self.m.edit.spell.load && self.m.edit.spell.state == SPELL_LOADING {
275 match dict {
276 Some(d) => {
277 let mut sp = Speller::with_dictionary(d, &lang);
278 self.spell_personal(&mut sp);
279 self.spell_ready(sp);
280 }
281 None => self.m.edit.spell.state = SPELL_NONE,
282 }
283 self.repass();
284 return;
285 }
286 }
287 if self.m.edit.spell.state != SPELL_OFF {
288 return; }
290 let headless = self.headless;
291 let s = &mut self.m.edit.spell;
292 let sources = if s.sources != 0 {
293 s.sources
294 } else if headless {
295 SRC_APP
296 } else {
297 SRC_ALL
298 };
299 let langs: Vec<String> = if s.lang.is_empty() { i18n::user_languages() } else { alloc::vec![s.lang.clone()] };
300 let found = langs.iter().find_map(|l| spell::find(l, sources));
301 match found {
302 None => s.state = SPELL_NONE,
303 Some(Source::System(t)) => match Speller::from_source(&Source::System(t)) {
304 Some(mut sp) => {
305 self.spell_personal(&mut sp);
306 self.spell_ready(sp);
307 }
308 None => self.m.edit.spell.state = SPELL_NONE,
309 },
310 Some(Source::Hunspell { aff, dic, lang, .. }) => {
311 if headless || s.job.as_ref().is_some_and(|j| !j.done()) {
312 if !headless {
313 self.timer(UIT_SPELL, 50); return;
315 }
316 match Dictionary::load(&aff, &dic) {
317 Some(d) => {
318 let mut sp = Speller::with_dictionary(d, &lang);
319 self.spell_personal(&mut sp);
320 self.spell_ready(sp);
321 }
322 None => self.m.edit.spell.state = SPELL_NONE,
323 }
324 return;
325 }
326 let n = s.load;
328 let slot = s.slot.clone();
329 let mut job = s.job.take().unwrap_or_default();
330 job.set(1, move |_, _| {
331 let d = Dictionary::load(&aff, &dic);
332 *slot.lock() = Some((n, d, lang.clone()));
333 });
334 job.start();
335 s.job = Some(job);
336 s.state = SPELL_LOADING;
337 }
338 }
339 }
340
341 fn spell_ready(&mut self, sp: Speller) {
342 let s = &mut self.m.edit.spell;
343 s.speller = Some(sp);
344 s.state = SPELL_READY;
345 s.generation = s.generation.wrapping_add(1);
346 }
347
348 pub(super) fn spell_scan(&mut self, id: u32) -> bool {
350 let flags = self.m.edit.tb_flags;
351 if flags & TB_SPELL == 0 || flags & (TB_PASSWORD | TB_READONLY | TB_DIGITS) != 0 || self.m.edit.spell.off {
352 return false;
353 }
354 self.spell_ensure();
355 let (headless, frame) = (self.headless, self.frame);
356 let e = &mut self.m.edit;
357 let s = &mut e.spell;
358 let Some(sp) = s.speller.as_mut() else { return false };
359 let Ok(text) = core::str::from_utf8(&e.buf) else { return false };
360 if s.budget_frame != frame {
361 s.budget_frame = frame;
362 s.budget = if headless { u32::MAX } else { BUDGET };
363 s.budget_until = if headless { u64::MAX } else { plat::time_us() + BUDGET_US };
364 }
365 let h = fnv(text.as_bytes());
366 let b = s.boxes.entry(id).or_default();
367 if b.hash != h || b.generation != s.generation || b.rev != sp.revision() || b.seen == 0 {
368 b.hash = h;
369 b.generation = s.generation;
370 b.rev = sp.revision();
371 b.toks = spell::tokens(text, sp.wordchars());
372 b.next = 0;
373 b.bad.clear();
374 }
375 while b.next < b.toks.len() {
376 let t = b.toks[b.next];
377 if sp.cached(&text[t.start..t.end]).is_none() {
378 if s.budget == 0 || (s.budget < BUDGET && plat::time_us() >= s.budget_until) {
379 break;
380 }
381 s.budget -= 1;
382 }
383 b.bad.extend(sp.token_errors(text, &t));
384 b.next += 1;
385 }
386 b.seen = frame.max(1);
387 let pending = b.next < b.toks.len();
388 if s.boxes.len() > BOXES_MAX {
389 s.boxes.retain(|k, b| *k == id || frame.wrapping_sub(b.seen) < 600);
390 }
391 if pending {
392 self.timer(UIT_SPELL, 16);
393 }
394 true
395 }
396
397 pub(super) fn spell_draw(&mut self, id: u32, x: i32, ls: usize, le: usize, editing: bool) {
400 let Some(b) = self.m.edit.spell.boxes.get(&id) else { return };
401 let spans: Vec<(usize, usize)> = b.bad.iter().filter(|r| r.1 > ls && r.0 < le).copied().collect();
402 if spans.is_empty() {
403 return;
404 }
405 let e = &self.m.edit;
407 let typing = if editing && e.ed_merge && e.ed_caret == e.ed_anchor { Some(e.ed_caret) } else { None };
408 let base = e.tb_base;
409 let c = self.color(T_CRIT);
410 self.draw_color(c);
411 for (a, z) in spans {
412 if typing == Some(z) {
413 continue;
414 }
415 let (a0, z0) = (a.max(ls), z.min(le));
416 let x0 = x.wrapping_add(self.text.caret_x(&self.m.edit.buf[ls..], a0 - ls));
417 let x1 = x.wrapping_add(self.text.caret_x(&self.m.edit.buf[ls..], z0 - ls));
418 self.spell_wave(x0, x1, base);
419 let lh = self.dp(20);
420 if let Some(m) = self.m.edit.spell.menu.as_mut() {
421 if m.id == id && m.start == a {
422 m.rect = [x0, base - lh * 3 / 4, (x1 - x0).max(1), lh];
423 }
424 }
425 }
426 }
427
428 fn spell_wave(&mut self, x0: i32, x1: i32, y: i32) {
430 let hp = self.dp(2).max(2); let amp = self.dp(2).max(2);
432 let y0 = y + self.dp(2);
433 let w = self.dp(1).max(1) << 8;
434 let mut x = x0;
435 let mut up = false;
436 while x < x1 {
437 let nx = (x + hp).min(x1);
438 let (ya, yb) = if up { (y0 + amp, y0) } else { (y0, y0 + amp) };
439 self.draw_line(x << 8, (ya << 8) + 128, nx << 8, (yb << 8) + 128, w);
440 x = nx;
441 up = !up;
442 }
443 }
444
445 fn spell_pid(&mut self, id: u32) -> u32 {
447 self.id_push(id);
448 let p = self.id("spell-menu");
449 self.id_pop();
450 p
451 }
452
453 fn spell_at(&self, id: u32, i: usize) -> Option<(usize, usize)> {
455 self.m.edit.spell.boxes.get(&id)?.bad.iter().find(|r| r.0 <= i && i <= r.1).copied()
456 }
457
458 fn spell_open(&mut self, id: u32, a: usize, z: usize, kbd: bool) {
460 let word = String::from_utf8_lossy(&self.m.edit.buf[a..z]).into_owned();
461 let headless = self.headless;
462 let s = &mut self.m.edit.spell;
463 let Some(sp) = s.speller.as_mut() else { return };
464 let sg = if s.sug_async.unwrap_or(!headless) { sp.suggester() } else { None };
465 let (sugs, wait) = match sg {
466 Some(sg) => (Vec::new(), s.sug_start(sg, &word)),
467 None => (sp.suggest(&word, SUG_SHOWN), 0),
468 };
469 let pid = self.spell_pid(id);
470 let e = &self.m.edit;
471 let caret = (e.ed_id == id && self.wp.ctx.focus == id).then_some((e.ed_caret, e.ed_anchor));
472 let rect = self.wp.ctx.last_rect;
473 self.m.edit.spell.menu = Some(Menu { id, pid, start: a, end: z, word, sugs, wait, add_row: 0, kbd, rect, caret });
474 self.popup_open(pid);
475 if kbd {
476 self.wp.ctx.focus_visible = true;
477 self.pp_flag(pid, crate::popup::PPF_FOCUS);
478 }
479 self.repass();
480 }
481
482 pub(super) fn spell_input(&mut self, id: u32, ev: u32, hit: Option<usize>, editing: bool) {
485 if ev & EV_RCLICK != 0 {
486 if let Some((a, z)) = hit.and_then(|i| self.spell_at(id, i)) {
487 self.wp.ctx.released &= !(1 << BTN_RIGHT); self.spell_open(id, a, z, false);
489 }
490 } else if editing && self.m.edit.spell.menu.is_none() {
491 let caret = self.m.edit.ed_caret;
492 if let Some((a, z)) = self.spell_at(id, caret) {
493 let k = self.key_first();
494 let open = if k & 0xFFFF == VK_APPS {
495 self.key(VK_APPS)
496 } else if k & 0xFFFF == VK_F10 && (k >> 16) & MOD3 == MOD_SHIFT {
497 self.key(VK_F10)
498 } else {
499 false
500 };
501 if open {
502 self.spell_open(id, a, z, true);
503 }
504 }
505 }
506 self.spell_menu(id);
507 }
508
509 pub(crate) fn spell_poll(&mut self) {
512 let s = &mut self.m.edit.spell;
513 let Some(m) = s.menu.as_mut().filter(|m| m.wait != 0) else { return };
514 let v = {
515 let mut g = s.sug_slot.lock();
516 if g.0 != m.wait {
517 return;
518 }
519 match g.1.take() {
520 Some(v) => v,
521 None => return,
522 }
523 };
524 m.sugs = v;
525 m.wait = 0;
526 let (kbd, pid, row) = (m.kbd, m.pid, m.add_row);
527 if kbd && row != 0 && self.wp.ctx.focus == row {
528 self.pp_flag(pid, crate::popup::PPF_FOCUS);
529 }
530 self.repass();
531 }
532
533 pub(crate) fn spell_busy(&self) -> bool {
535 let s = &self.m.edit.spell;
536 s.sug_jobs.iter().any(|j| !j.done()) || s.job.as_ref().is_some_and(|j| !j.done())
537 }
538
539 pub(super) fn spell_menu_open(&self, id: u32) -> bool {
541 self.m.edit.spell.menu.as_ref().is_some_and(|m| m.id == id && self.popup_is_open(m.pid))
542 }
543
544 fn spell_closed(&mut self, id: u32, caret: Option<(usize, usize)>) {
547 if self.wp.ctx.focus != id || self.m.edit.ed_id == id {
548 return;
549 }
550 let Some((c, a)) = caret else { return };
551 let frame = self.frame;
552 let e = &mut self.m.edit;
553 e.ed_id = id;
554 e.ed_caret = Self::tb_fix_index(&e.buf, c);
555 e.ed_anchor = Self::tb_fix_index(&e.buf, a);
556 e.ed_seen = frame;
557 e.ed_merge = false;
558 e.ed_prefx = -1;
559 self.tb_blink_reset();
560 }
561
562 fn spell_menu(&mut self, id: u32) {
564 let Some(m) = self.m.edit.spell.menu.as_ref() else { return };
565 if m.id != id {
566 return;
567 }
568 let (kbd, rect, sugs, pid, wait) = (m.kbd, m.rect, m.sugs.clone(), m.pid, m.wait != 0);
569 if !self.popup_is_open(pid) {
570 let caret = m.caret;
571 self.m.edit.spell.menu = None;
572 self.spell_closed(id, caret);
573 return;
574 }
575 let (lr, ls) = (self.wp.ctx.last_rect, self.wp.ctx.last_state);
577 if kbd {
578 self.wp.ctx.last_rect = rect;
579 }
580 let mut act = None;
581 if self.flyout_begin(pid, if kbd { FL_BELOW } else { FL_POINT }) {
582 self.id_push(id);
583 self.id_push_str("spell");
584 if wait {
585 self.next_flags(ST_DISABLED);
586 let r = self.id("wait");
587 self.menu_item(r, i18n::tr("Vorschläge werden gesucht …"));
588 } else if sugs.is_empty() {
589 self.next_flags(ST_DISABLED);
590 let r = self.id("none");
591 self.menu_item(r, i18n::tr("Keine Vorschläge"));
592 }
593 for (k, s) in sugs.iter().enumerate() {
594 let r = self.id_n(k as u32);
595 if self.menu_item(r, s) & EV_ACT != 0 {
596 act = Some(Act::Use(k));
597 }
598 }
599 self.menu_sep();
600 let r = self.id("add");
601 if let Some(m) = self.m.edit.spell.menu.as_mut() {
602 m.add_row = r;
603 }
604 if self.menu_item(r, i18n::tr("Zum Wörterbuch hinzufügen")) & EV_ACT != 0 {
605 act = Some(Act::Add);
606 }
607 let r = self.id("ignore");
608 if self.menu_item(r, i18n::tr("Ignorieren")) & EV_ACT != 0 {
609 act = Some(Act::Ignore);
610 }
611 self.id_pop();
612 self.id_pop();
613 }
614 self.flyout_end();
615 self.wp.ctx.last_rect = lr;
616 self.wp.ctx.last_state = ls;
617 let Some(act) = act else { return };
618 let Some(m) = self.m.edit.spell.menu.take() else { return };
619 self.spell_closed(id, m.caret);
620 match act {
621 Act::Use(k) => self.spell_replace(id, &m, &sugs[k]),
622 Act::Add => {
623 if let Some(sp) = self.m.edit.spell.speller.as_mut() {
624 if !sp.add(&m.word) {
625 self.warn("spell: the personal word list could not be written");
626 }
627 }
628 }
629 Act::Ignore => {
630 if let Some(sp) = self.m.edit.spell.speller.as_mut() {
631 sp.ignore(&m.word);
632 }
633 }
634 }
635 self.repass();
636 }
637
638 fn spell_replace(&mut self, id: u32, m: &Menu, with: &str) {
640 let e = &self.m.edit;
641 if m.end > e.buf.len() || &e.buf[m.start..m.end] != m.word.as_bytes() {
642 return; }
644 if e.tb_max > 0 && e.buf.len() - (m.end - m.start) + with.len() > e.tb_max {
645 return; }
647 let focused = e.ed_id == id && self.wp.ctx.focus == id;
648 if focused {
649 self.ud_push();
650 }
651 self.tb_delete(m.start, m.end);
652 let n = self.tb_insert(m.start, with.as_bytes());
653 if focused {
654 let e = &mut self.m.edit;
655 e.ed_caret = m.start + n;
656 e.ed_anchor = m.start + n;
657 e.ed_merge = false;
658 }
659 self.m.edit.tb_ev |= EV_ACT;
660 }
661}