1use super::*;
2
3const SPLIT: fn(char) -> bool = |c: char| c.is_whitespace() || c.is_ascii_punctuation() || c == 's';
4
5#[derive(Default, Debug)]
6struct Popup {
7 size: Vec2,
8 text: Astr,
9}
10#[derive(Default, Debug)]
11struct HyperKey {
12 val: Option<Popup>,
13 trie: HashMap<Str, Self>,
14}
15#[derive(Default, Debug)]
16pub struct HyperDB {
17 keys: HyperKey,
18 max_keys: usize,
19}
20impl HyperDB {
21 pub fn new(f: &Font, pairs: impl iter::IntoIterator<Item = (impl ToString, impl ToString)>) -> Self {
22 let MAX_RATIO = 10.;
23
24 let mut max_breaks = 0;
25 let mut keys: HyperKey = Def();
26 pairs.into_iter().for_each(|(k, v)| {
27 let (k, v) = (k.to_string(), v.to_string());
28 max_breaks = max_breaks.max(k.chars().filter(|&c| SPLIT(c)).count());
29 let (w, h) = v.lines().fold(Vec2(0), |(x, y), l| {
30 let (w, h) = Text::size(l, f, 1.);
31 (w.max(x), h + y)
32 });
33
34 let size = (w, h).or_val(w < MAX_RATIO * h, || (w * h).sqrt().pipe(|s| (2., 0.5).mul(s)));
35
36 let val = &mut k.split(SPLIT).fold(&mut keys, |t, k| t.trie.entry(k.to_lowercase().into()).or_default()).val;
37 ASSERT!(val.is_none(), "Hyperdb key collision {k:?}");
38 *val = Popup { size, text: v.into() }.pipe(Some);
39 });
40 Self { keys, max_keys: max_breaks * 2 + 1 }
41 }
42 fn get<'a>(&self, scale: f32, keys: impl iter::Iterator<Item = &'a str>) -> Option<(usize, Vec2, Astr)> {
43 let mut trie = &self.keys.trie;
44 for (n, k) in keys
45 .map(|l| l.trim_matches(SPLIT))
46 .enumerate()
47 .take_while(|&(n, l)| n > 0 || !l.is_empty())
48 .filter(|(_, l)| !l.is_empty())
49 {
50 let k = trie.get(&*k.to_lowercase())?;
51
52 if let Some(Popup { size, text }) = k.val.as_ref() {
53 return (n + 1, size.mul(scale), text.clone()).pipe(Some);
54 }
55
56 trie = &k.trie;
57 }
58 None
59 }
60}
61
62#[derive(Default, Debug)]
63pub struct HyperText {
64 lsb: f32,
65 size: Vec2,
66 scale: f32,
67 lines: Box<[STR]>,
68 scrollbar: Slider,
69 hovered: bool,
70 last_pip: f32,
71 batched: Box<[BatchedWords]>,
72 popup: Option<(usize, Vec2, Box<Self>)>,
73 pub text: CachedStr,
74}
75impl HyperText {
76 pub fn draw<'s: 'l, 'l>(&'s mut self, r: &mut RenderLock<'l>, t: &'l Theme, layout @ Surf { pos, size }: Surf, scale: f32, db: &HyperDB) {
77 let (SCR_PAD, POP_PAD) = (0.01, Vec2(0.2 * scale));
78 let (id, s, font) = (ref_UUID(self), self, &t.font);
79
80 if s.text.changed() || scale != s.scale || size != s.size {
81 let (lsb, lines, _) = u::parse_text(&s.text, font, scale, size.x(), char::is_whitespace).pipe(task::block_on);
82 (s.lsb, s.size, s.scale, s.last_pip) = (lsb, size, scale, f32::NAN);
83 s.lines = unsafe { mem::transmute(lines.into_boxed_slice()) };
84 }
85 let Self {
86 lsb,
87 ref lines,
88 ref mut scrollbar,
89 ref mut hovered,
90 ref mut last_pip,
91 ref mut batched,
92 ref mut popup,
93 ..
94 } = *s;
95
96 let (scrollable, p, (start, len)) = {
97 let (start, len) = (1. - scrollbar.pip_pos, lines.len());
98 let (_, l) = u::visible_range(layout, scale, 0., len);
99 let skip = f32(len - l) * start;
100 let p = move |x, n| u::line_pos(lines, font, scale, layout, skip, n, x + lsb);
101 (len > l, p, u::visible_range(layout, scale, skip, len))
102 };
103 let (pip_size, adv) = u::visible_norm(layout, len, lines.len());
104
105 if &scrollbar.pip_pos != last_pip {
106 (*last_pip, *popup) = (scrollbar.pip_pos, None);
107 let window = db.max_keys;
108 let Continue = || Some(None);
109
110 let words = lines
111 .iter()
112 .enumerate()
113 .skip(start)
114 .take(len + 1)
115 .filter(|(_, l)| !l.is_empty())
116 .flat_map(|(n, line)| {
117 line.split_inclusive(SPLIT)
118 .flat_map(|l| {
119 l.rfind(SPLIT)
120 .and_then(|i| vec![&l[..i], &l[i..]].pipe(Some).or_def(i > 0))
121 .unwrap_or_else(|| vec![l])
122 })
123 .map(move |l| (n, l))
124 })
125 .chain(vec![(usize::MAX, ""); window].or_def(!lines.is_empty()))
126 .collect_vec();
127
128 *batched = words
129 .windows(window)
130 .scan((0, None, 0, 0, 0), move |(skip, tip, beg, end, prev_l), window| {
131 let &(lnum, word) = window.at(0);
132 let word_end = *end + word.len();
133 let next_line = (0, word.len(), lnum);
134 let next_word = (*end, word_end, lnum);
135 let next_batch = || next_word.or_val(*prev_l == lnum, || next_line);
136
137 let line = {
138 let (beg, end, lnum) = (*beg, *end, *prev_l);
139 move |tip| {
140 let line = lines.at(lnum);
141 let adv = line[..beg].utf8_count();
142 let adv = Text::adv_at(line, font, scale, adv);
143 let pos = p(adv, lnum);
144 let line = unsafe { mem::transmute(&line[beg..end]) };
145 Some((tip, pos, line))
146 }
147 };
148 let normal = || line(None);
149
150 if *skip > 0 {
151 *skip -= 1;
152 if *skip > 0 && *prev_l == lnum {
153 *end = word_end;
154 return Continue();
155 }
156
157 (*beg, *end, *prev_l) = next_batch();
158 return line(tip.clone()).pipe(Some);
159 }
160
161 let keys = window.iter().map(|&(_, l)| l);
162
163 let Some((n, s, t)) = db.get(scale, keys) else {
164 if *prev_l == lnum {
165 *end = word_end;
166 return Continue();
167 }
168
169 (*beg, *end, *prev_l) = next_batch();
170 return Some(normal());
171 };
172
173 (*skip, *tip) = (n, Some((s, t)));
174
175 let normal = None.or_val(beg == end, normal);
176 (*beg, *end, *prev_l) = next_batch();
177 Some(normal)
178 })
179 .flatten()
180 .collect();
181 }
182
183 r.draw(Rect {
184 pos,
185 size: layout.w_sub(f32(scrollable) * SCR_PAD).size,
186 color: t.bg,
187 });
188 *hovered = r.hovered();
189
190 let _c = r.clip(layout);
191
192 let mut hover = false;
193 batched.iter().enumerate().for_each(|(id, (tip, p, text))| {
194 let pos = pos.sum(p);
195 let mut draw_text = |color| r.draw(Text { pos, scale, color, text, font });
196
197 let &Some((size, ref tip)) = tip else { return draw_text(t.text) };
198
199 draw_text(t.highlight);
200 let h = r.hovered();
201 hover |= h;
202
203 if !h || popup.as_ref().map(|(i, ..)| *i == id).unwrap_or(false) || child_hovered(popup) {
204 return;
205 }
206
207 let at = r.mouse_pos();
208 let side = at.sum(size).ls(at.sub(size).abs());
209 let scale = scale * 1.05;
210 let at = at.sum((0., scale - (at.y() - pos.y()).abs()));
211 let nat = at.sub(size).sub((0., scale));
212 let at = at.mul(side).sum(nat.mul(side.map(|s| !s)));
213 *popup = (id, at, Self { size, text: (**tip).into(), ..Def() }.into()).pipe(Some);
214 });
215
216 let mut draw_scrollbar = |sc: &'s mut Slider| {
217 if !scrollable {
218 return sc.pip_pos = 1.;
219 }
220
221 let s = layout.xr(SCR_PAD).w(SCR_PAD);
222
223 let sc = Cell::from_mut(sc);
224 r.logic(
225 layout,
226 move |e, _, _| {
227 let move_pip = |o: f32| sc.mutate(|s| s.pip_pos = (s.pip_pos + o * adv * f32(len)).clamp(0., 1.));
228 match *e {
229 Scroll { at, .. } => move_pip(at.y()),
230 Keyboard { key, m } if m.pressed() => match key {
231 Key::Up | Key::PageUp => move_pip(1.),
232 Key::Down | Key::PageDown => move_pip(-1.),
233 _ => return Pass,
234 },
235 _ => return Pass,
236 }
237 Accept
238 },
239 id,
240 );
241
242 sc.mutate(|sc| sc.draw(r, t, s, pip_size));
243 };
244
245 draw_scrollbar(scrollbar);
246
247 if !hover && !child_hovered(popup) && timeout(true) {
248 timeout(false);
249 *popup = None;
250 }
251
252 if let Some((_, pos, p)) = popup {
253 let s = Surf::new(*pos, p.size);
254 r.unclipped(|r| {
255 if p.popup.is_none() {
256 let Surf { pos, size } = s.size_sub(POP_PAD.mul(-2));
257 r.draw(Rect { pos, size, color: (0., 0., 0., 1.) })
258 }
259 p.draw(r, t, s.xy(POP_PAD), scale, db)
260 });
261 }
262 }
263}
264
265impl<'s: 'l, 'l> Lock::HyperText<'s, 'l, '_> {
266 pub fn draw(self, g: impl Into<Surf>, sc: f32, d: &HyperDB) {
267 let Self { s, r, t } = self;
268 s.draw(r, t, g.into(), sc, d)
269 }
270}
271
272fn child_hovered(p: &Option<(usize, Vec2, Box<HyperText>)>) -> bool {
273 p.as_ref().map(|(_, _, p)| p.hovered || child_hovered(&p.popup)).unwrap_or(false)
274}
275
276fn timeout(active: bool) -> bool {
277 unsafe {
278 static mut TIME: usize = 0;
279 TIME += 1;
280 if !active {
281 TIME = 0
282 }
283 TIME > 60
284 }
285}
286
287type BatchedWords = (Option<(Vec2, Astr)>, Vec2, &'static str);