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}