grafix-toolbox 0.8.33

Personal collection of opengl and rust tools, also serving as an functional gui crate. See ./gui/elements for premade gui elements
Documentation
use super::*;

const SPLIT: fn(char) -> bool = |c: char| c.is_whitespace() || c.is_ascii_punctuation() || c == 's';

#[derive(Default, Debug)]
struct Popup {
	size: Vec2,
	text: Astr,
}
#[derive(Default, Debug)]
struct HyperKey {
	val: Option<Popup>,
	trie: HashMap<Str, Self>,
}
#[derive(Default, Debug)]
pub struct HyperDB {
	keys: HyperKey,
	max_keys: usize,
}
impl HyperDB {
	pub fn new(f: &Font, pairs: impl iter::IntoIterator<Item = (impl ToString, impl ToString)>) -> Self {
		let MAX_RATIO = 10.;

		let mut max_breaks = 0;
		let mut keys: HyperKey = Def();
		pairs.into_iter().for_each(|(k, v)| {
			let (k, v) = (k.to_string(), v.to_string());
			max_breaks = max_breaks.max(k.chars().filter(|&c| SPLIT(c)).count());
			let (w, h) = v.lines().fold(Vec2(0), |(x, y), l| {
				let (w, h) = Text::size(l, f, 1.);
				(w.max(x), h + y)
			});

			let size = (w, h).or_val(w < MAX_RATIO * h, || (w * h).sqrt().pipe(|s| (2., 0.5).mul(s)));

			let val = &mut k.split(SPLIT).fold(&mut keys, |t, k| t.trie.entry(k.to_lowercase().into()).or_default()).val;
			ASSERT!(val.is_none(), "Hyperdb key collision {k:?}");
			*val = Popup { size, text: v.into() }.pipe(Some);
		});
		Self { keys, max_keys: max_breaks * 2 + 1 }
	}
	fn get<'a>(&self, scale: f32, keys: impl iter::Iterator<Item = &'a str>) -> Option<(usize, Vec2, Astr)> {
		let mut trie = &self.keys.trie;
		for (n, k) in keys
			.map(|l| l.trim_matches(SPLIT))
			.enumerate()
			.take_while(|&(n, l)| n > 0 || !l.is_empty())
			.filter(|(_, l)| !l.is_empty())
		{
			let k = trie.get(&*k.to_lowercase())?;

			if let Some(Popup { size, text }) = k.val.as_ref() {
				return (n + 1, size.mul(scale), text.clone()).pipe(Some);
			}

			trie = &k.trie;
		}
		None
	}
}

#[derive(Default, Debug)]
pub struct HyperText {
	lsb: f32,
	size: Vec2,
	scale: f32,
	lines: Box<[STR]>,
	scrollbar: Slider,
	hovered: bool,
	last_pip: f32,
	batched: Box<[BatchedWords]>,
	popup: Option<(usize, Vec2, Box<Self>)>,
	pub text: CachedStr,
}
impl HyperText {
	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) {
		let (SCR_PAD, POP_PAD) = (0.01, Vec2(0.2 * scale));
		let (id, s, font) = (ref_UUID(self), self, &t.font);

		if s.text.changed() || scale != s.scale || size != s.size {
			let (lsb, lines, _) = u::parse_text(&s.text, font, scale, size.x(), char::is_whitespace).pipe(task::block_on);
			(s.lsb, s.size, s.scale, s.last_pip) = (lsb, size, scale, f32::NAN);
			s.lines = unsafe { mem::transmute(lines.into_boxed_slice()) };
		}
		let Self {
			lsb,
			ref lines,
			ref mut scrollbar,
			ref mut hovered,
			ref mut last_pip,
			ref mut batched,
			ref mut popup,
			..
		} = *s;

		let (scrollable, p, (start, len)) = {
			let (start, len) = (1. - scrollbar.pip_pos, lines.len());
			let (_, l) = u::visible_range(layout, scale, 0., len);
			let skip = f32(len - l) * start;
			let p = move |x, n| u::line_pos(lines, font, scale, layout, skip, n, x + lsb);
			(len > l, p, u::visible_range(layout, scale, skip, len))
		};
		let (pip_size, adv) = u::visible_norm(layout, len, lines.len());

		if &scrollbar.pip_pos != last_pip {
			(*last_pip, *popup) = (scrollbar.pip_pos, None);
			let window = db.max_keys;
			let Continue = || Some(None);

			let words = lines
				.iter()
				.enumerate()
				.skip(start)
				.take(len + 1)
				.filter(|(_, l)| !l.is_empty())
				.flat_map(|(n, line)| {
					line.split_inclusive(SPLIT)
						.flat_map(|l| {
							l.rfind(SPLIT)
								.and_then(|i| vec![&l[..i], &l[i..]].pipe(Some).or_def(i > 0))
								.unwrap_or_else(|| vec![l])
						})
						.map(move |l| (n, l))
				})
				.chain(vec![(usize::MAX, ""); window].or_def(!lines.is_empty()))
				.collect_vec();

			*batched = words
				.windows(window)
				.scan((0, None, 0, 0, 0), move |(skip, tip, beg, end, prev_l), window| {
					let &(lnum, word) = window.at(0);
					let word_end = *end + word.len();
					let next_line = (0, word.len(), lnum);
					let next_word = (*end, word_end, lnum);
					let next_batch = || next_word.or_val(*prev_l == lnum, || next_line);

					let line = {
						let (beg, end, lnum) = (*beg, *end, *prev_l);
						move |tip| {
							let line = lines.at(lnum);
							let adv = line[..beg].utf8_count();
							let adv = Text::adv_at(line, font, scale, adv);
							let pos = p(adv, lnum);
							let line = unsafe { mem::transmute(&line[beg..end]) };
							Some((tip, pos, line))
						}
					};
					let normal = || line(None);

					if *skip > 0 {
						*skip -= 1;
						if *skip > 0 && *prev_l == lnum {
							*end = word_end;
							return Continue();
						}

						(*beg, *end, *prev_l) = next_batch();
						return line(tip.clone()).pipe(Some);
					}

					let keys = window.iter().map(|&(_, l)| l);

					let Some((n, s, t)) = db.get(scale, keys) else {
						if *prev_l == lnum {
							*end = word_end;
							return Continue();
						}

						(*beg, *end, *prev_l) = next_batch();
						return Some(normal());
					};

					(*skip, *tip) = (n, Some((s, t)));

					let normal = None.or_val(beg == end, normal);
					(*beg, *end, *prev_l) = next_batch();
					Some(normal)
				})
				.flatten()
				.collect();
		}

		r.draw(Rect {
			pos,
			size: layout.w_sub(f32(scrollable) * SCR_PAD).size,
			color: t.bg,
		});
		*hovered = r.hovered();

		let _c = r.clip(layout);

		let mut hover = false;
		batched.iter().enumerate().for_each(|(id, (tip, p, text))| {
			let pos = pos.sum(p);
			let mut draw_text = |color| r.draw(Text { pos, scale, color, text, font });

			let &Some((size, ref tip)) = tip else { return draw_text(t.text) };

			draw_text(t.highlight);
			let h = r.hovered();
			hover |= h;

			if !h || popup.as_ref().map(|(i, ..)| *i == id).unwrap_or(false) || child_hovered(popup) {
				return;
			}

			let at = r.mouse_pos();
			let side = at.sum(size).ls(at.sub(size).abs());
			let scale = scale * 1.05;
			let at = at.sum((0., scale - (at.y() - pos.y()).abs()));
			let nat = at.sub(size).sub((0., scale));
			let at = at.mul(side).sum(nat.mul(side.map(|s| !s)));
			*popup = (id, at, Self { size, text: (**tip).into(), ..Def() }.into()).pipe(Some);
		});

		let mut draw_scrollbar = |sc: &'s mut Slider| {
			if !scrollable {
				return sc.pip_pos = 1.;
			}

			let s = layout.xr(SCR_PAD).w(SCR_PAD);

			let sc = Cell::from_mut(sc);
			r.logic(
				layout,
				move |e, _, _| {
					let move_pip = |o: f32| sc.mutate(|s| s.pip_pos = (s.pip_pos + o * adv * f32(len)).clamp(0., 1.));
					match *e {
						Scroll { at, .. } => move_pip(at.y()),
						Keyboard { key, m } if m.pressed() => match key {
							Key::Up | Key::PageUp => move_pip(1.),
							Key::Down | Key::PageDown => move_pip(-1.),
							_ => return Pass,
						},
						_ => return Pass,
					}
					Accept
				},
				id,
			);

			sc.mutate(|sc| sc.draw(r, t, s, pip_size));
		};

		draw_scrollbar(scrollbar);

		if !hover && !child_hovered(popup) && timeout(true) {
			timeout(false);
			*popup = None;
		}

		if let Some((_, pos, p)) = popup {
			let s = Surf::new(*pos, p.size);
			r.unclipped(|r| {
				if p.popup.is_none() {
					let Surf { pos, size } = s.size_sub(POP_PAD.mul(-2));
					r.draw(Rect { pos, size, color: (0., 0., 0., 1.) })
				}
				p.draw(r, t, s.xy(POP_PAD), scale, db)
			});
		}
	}
}

impl<'s: 'l, 'l> Lock::HyperText<'s, 'l, '_> {
	pub fn draw(self, g: impl Into<Surf>, sc: f32, d: &HyperDB) {
		let Self { s, r, t } = self;
		s.draw(r, t, g.into(), sc, d)
	}
}

fn child_hovered(p: &Option<(usize, Vec2, Box<HyperText>)>) -> bool {
	p.as_ref().map(|(_, _, p)| p.hovered || child_hovered(&p.popup)).unwrap_or(false)
}

fn timeout(active: bool) -> bool {
	unsafe {
		static mut TIME: usize = 0;
		TIME += 1;
		if !active {
			TIME = 0
		}
		TIME > 60
	}
}

type BatchedWords = (Option<(Vec2, Astr)>, Vec2, &'static str);