1//! Spelling through UIKit's UITextChecker, which the spelling thread reaches on the main
2//! thread, as UIKit's objects expect.
3
4use canvas::spelling::{Dictionary, pick};
5use objc2::{class, msg_send, msg_send_id, rc::Retained, runtime::AnyObject};
6use objc2_foundation::{MainThreadMarker, NSArray, NSRange, NSString};
7use std::ffi::c_void;
8
9unsafe extern "C" {
10 /// The main queue; only its address matters.
11 static _dispatch_main_q: u8;
12 fn dispatch_sync_f(queue: *mut c_void, context: *mut c_void, work: extern "C" fn(*mut c_void));
13}
14
15/// Runs `work` on the main thread, waiting for it.
16fn on_main<F: FnOnce() -> R + Send, R: Send>(work: F) -> R {
17 if MainThreadMarker::new().is_some() {
18 return work();
19 }
20 extern "C" fn run<F: FnOnce() -> R, R>(context: *mut c_void) {
21 let (work, result) = unsafe { &mut *context.cast::<(Option<F>, Option<R>)>() };
22 *result = work.take().map(|work| work());
23 }
24 let mut slot = (Some(work), None);
25 unsafe {
26 dispatch_sync_f(
27 (&raw const _dispatch_main_q).cast_mut().cast(),
28 (&raw mut slot).cast(),
29 run::<F, R>,
30 );
31 }
32 slot.1.expect("the main queue ran the work")
33}
34
35fn checker() -> Retained<AnyObject> {
36 unsafe { msg_send_id![class!(UITextChecker), new] }
37}
38
39struct Checker {
40 /// The spelling languages the system has, as `en_US`.
41 languages: Vec<String>,
42}
43
44impl Dictionary for Checker {
45 fn misspelled(&self, words: &[(&str, u32)]) -> Vec<bool> {
46 on_main(|| {
47 let checker = checker();
48 words
49 .iter()
50 .map(|(word, lcid)| {
51 let Some(language) = pick(*lcid, &self.languages) else {
52 return false;
53 };
54 let word = NSString::from_str(word);
55 let found: NSRange = unsafe {
56 msg_send![&checker,
57 rangeOfMisspelledWordInString: &*word,
58 range: NSRange::new(0, word.length()),
59 startingAt: 0isize,
60 wrap: false,
61 language: &*NSString::from_str(language)]
62 };
63 found.length > 0
64 })
65 .collect()
66 })
67 }
68
69 fn suggest(&self, word: &str, lcid: u32) -> Vec<String> {
70 on_main(|| {
71 let Some(language) = pick(lcid, &self.languages) else {
72 return Vec::new();
73 };
74 let word = NSString::from_str(word);
75 let guesses: Option<Retained<NSArray<NSString>>> = unsafe {
76 msg_send_id![&checker(),
77 guessesForWordRange: NSRange::new(0, word.length()),
78 inString: &*word,
79 language: &*NSString::from_str(language)]
80 };
81 guesses.map_or_else(Vec::new, |guesses| {
82 guesses.iter().map(|guess| guess.to_string()).collect()
83 })
84 })
85 }
86
87 fn learn(&self, word: &str) {
88 on_main(|| unsafe {
89 let _: () = msg_send![class!(UITextChecker), learnWord: &*NSString::from_str(word)];
90 });
91 }
92}
93
94/// UIKit's spell checker with the languages the system has.
95pub fn dictionary() -> Box<dyn Dictionary> {
96 let languages = on_main(|| {
97 let languages: Retained<NSArray<NSString>> =
98 unsafe { msg_send_id![class!(UITextChecker), availableLanguages] };
99 languages
100 .iter()
101 .map(|language| language.to_string())
102 .collect()
103 });
104 Box::new(Checker { languages })
105}