Skip to main content

ui/
hyph.rs

1// hyph -- hyphenation of wrapped texts and text boxes (spell::Hyphenator, text::segment)
2//
3//   TX_HYPHENATE (text style, with TX_WRAP), TB_HYPHENATE (textbox, with TB_MULTI | TB_WRAP)
4//        a word that does not fit at the end of a line is broken at its last hyphenation
5//        point that fits, and a hyphen is drawn there.  Soft hyphens (U+00AD) in the text
6//        work in every wrapped text, with or without the flag: invisible inside a line, a
7//        hyphen where the line breaks after one
8//   hyph_language(tag)          the language: a BCP 47 tag; "" (default) = font_language(),
9//                               the user's tag of the UI language
10//   next_hyph_language(tag)     the language of the next wrapped text (TX_WRAP) or multi-line
11//                               box only (it counts when that one hyphenates)
12//   hyph_sources(SRC_*)         where dictionaries come from (spell crate; 0 = the default:
13//                               everything in window mode, only app:// in headless runs, so
14//                               scripts never depend on the machine)
15//   hyph_set(tag, hyphenator)   the app's own patterns for a language (before any source)
16//   hyphenator(tag) -> Option<Arc<Hyphenator>>   the patterns for a tag ("": the current
17//                               language), found (spell::find_hyphen: hyph_xx_YY.dic of
18//                               LibreOffice, /usr/share/hyphen, config://hyphen,
19//                               app://hyphen) and read on first use, then kept
20//
21// A dictionary is read on the UI thread the first time a language is needed (de_DE, 1 MB:
22// some 40 ms); a language without one keeps texts unhyphenated (soft hyphens still work).
23
24use alloc::collections::BTreeMap;
25use alloc::string::String;
26use alloc::sync::Arc;
27use alloc::vec::Vec;
28
29use spell::{find_hyphen, Hyphenator, SRC_ALL, SRC_APP};
30
31use crate::Ui;
32
33/// the UI's hyphenation state
34#[derive(Default)]
35pub(crate) struct S {
36    lang: String,
37    next: Option<String>,
38    sources: u32,
39    /// the app's own patterns (tag in lower case with '_')
40    own: Vec<(String, Arc<Hyphenator>)>,
41    /// tag (lower case with '_') -> what was found for it
42    cache: BTreeMap<String, Option<Arc<Hyphenator>>>,
43}
44
45// "de-DE" -> "de_de"
46fn norm(tag: &str) -> String {
47    tag.trim().to_ascii_lowercase().replace('-', "_")
48}
49
50impl Ui {
51    /// the hyphenation language: a BCP 47 tag ("de-DE"); "" = the UI language's
52    pub fn hyph_language(&mut self, tag: &str) {
53        if self.m.hyph.lang != tag {
54            self.m.hyph.lang = String::from(tag);
55            self.repass();
56        }
57    }
58
59    /// the language of the next wrapped text or multi-line box only (it counts when that one
60    /// hyphenates: TX_HYPHENATE, TB_HYPHENATE)
61    pub fn next_hyph_language(&mut self, tag: &str) {
62        self.m.hyph.next = Some(String::from(tag));
63    }
64
65    /// where hyphenation dictionaries come from (spell::SRC_*; 0 = the default)
66    pub fn hyph_sources(&mut self, sources: u32) {
67        if self.m.hyph.sources != sources {
68            self.m.hyph.sources = sources;
69            self.m.hyph.cache.clear();
70            self.repass();
71        }
72    }
73
74    /// the app's own hyphenation patterns for a language tag ("de", "en-US")
75    pub fn hyph_set(&mut self, tag: &str, h: Hyphenator) {
76        let t = norm(tag);
77        self.m.hyph.own.retain(|o| o.0 != t);
78        self.m.hyph.own.push((t, Arc::new(h)));
79        self.m.hyph.cache.clear();
80        self.repass();
81    }
82
83    /// the hyphenation patterns for a tag ("": the current language), None when there are none
84    pub fn hyphenator(&mut self, tag: &str) -> Option<Arc<Hyphenator>> {
85        let tag = if tag.is_empty() {
86            if self.m.hyph.lang.is_empty() { crate::font_language() } else { self.m.hyph.lang.clone() }
87        } else {
88            String::from(tag)
89        };
90        let t = norm(&tag);
91        if let Some(h) = self.m.hyph.cache.get(&t) {
92            return h.clone();
93        }
94        // the app's own: the tag, else the same language
95        let primary = String::from(t.split('_').next().unwrap_or(""));
96        let own = self.m.hyph.own.iter().find(|o| o.0 == t).or_else(|| self.m.hyph.own.iter().find(|o| o.0.split('_').next() == Some(primary.as_str())));
97        let found = match own {
98            Some(o) => Some(o.1.clone()),
99            None => {
100                let sources = match self.m.hyph.sources {
101                    0 if self.headless => SRC_APP,
102                    0 => SRC_ALL,
103                    s => s,
104                };
105                find_hyphen(&tag, sources).and_then(|(path, _)| Hyphenator::load(&path)).map(Arc::new)
106            }
107        };
108        self.m.hyph.cache.insert(t, found.clone());
109        found
110    }
111
112    // (crate) the patterns for the widget being made: next_hyph_language, else the language
113    // (taken when `on`: the widget hyphenates; the pending language is used up either way)
114    pub(crate) fn hyph_take(&mut self, on: bool) -> Option<Arc<Hyphenator>> {
115        let next = self.m.hyph.next.take();
116        if !on {
117            return None;
118        }
119        self.hyphenator(next.as_deref().unwrap_or(""))
120    }
121}