Skip to main content

grafix_toolbox/gui/elements/
hypertext.rs

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);