Skip to main content

grafix_toolbox/gui/elements/
textedit.rs

1use super::{lazy::*, *};
2use u::{Caret, DynamicStr, caret as ca, if_ctrl};
3
4#[derive(Default, Debug)]
5pub struct TextEdit {
6	size: Vec2,
7	scale: f32,
8	a: mAffected,
9	parser: LazyCell<ParseResult<mAffected>>,
10	scrollbar: Slider,
11	pub text: CachedStr,
12}
13#[derive(Default, Debug)]
14struct mAffected {
15	caret: Caret,
16	select: Caret,
17	history: History,
18}
19impl TextEdit {
20	pub fn draw<'s: 'l, 'l>(&'s mut self, r: &mut RenderLock<'l>, t: &'l Theme, layout @ Surf { size, .. }: Surf, scale: f32, readonly: bool) {
21		let SCR_PAD = 0.02;
22		let CUR_PAD = 0.01;
23		let (id, s, font) = (ref_UUID(self), self, &t.font);
24
25		let lazy_parse = async move |text: Astr, font: Arc<Font>| {
26			let lnum_w = {
27				let lnum = f32(1 + stream::iter(text.lines()).count().await);
28				let max_lnum = lnum.log10().max(1.).ceil();
29				let w = font.glyph('0').adv * scale * max_lnum;
30				w.or_def(w * 20. < size.x())
31			};
32			let (lsb, lines, lnums) = u::parse_text(&text, &font, scale, size.x() - lnum_w - SCR_PAD, char::is_whitespace).await;
33			let lines: Vec<_> = stream::iter(lines.into_iter().zip(lnums.into_iter()).map(DynamicStr::new)).collect().await;
34			((lsb, lnum_w), lines.into())
35		};
36
37		if s.text.changed() || scale != s.scale || size != s.size {
38			let (text, font) = (s.text.clone(), font.clone());
39
40			(s.size, s.scale, (s.a.caret, s.a.select), s.parser) = (
41				size,
42				scale,
43				Def(),
44				LazyCell::with(((Def(), vec![P(text.clone())].into()), Def()), async move |_| (lazy_parse(text, font).await, Def())),
45			);
46		}
47
48		let Self { a, parser, scrollbar, .. } = s;
49		let mut parser = parser.lock();
50		let (((lsb, lnum_w), ref lines), ref reconcile) = *parser;
51		reconcile.apply(a);
52		let mAffected { caret, select, history } = a;
53
54		let (scrollable, p, (start, len)) = {
55			let (start, len) = (1. - scrollbar.pip_pos, lines.len());
56			let (_, l) = u::visible_range(layout, scale, 0., len);
57			let skip = f32(len - l) * start;
58			let p = move |lines, n| u::line_pos(lines, font, scale, layout, skip, n, lsb);
59			(len > l, p, u::visible_range(layout, scale, skip, len))
60		};
61		let ((beg_y, end_y, len_y), (pip_size, adv)) = (vec3((start, start + len, len)), u::visible_norm(layout, len, lines.len()));
62
63		let Surf { pos, size } = layout.w(lnum_w);
64		r.draw(Rect { pos, size, color: t.fg });
65
66		let _c = r.clip(layout);
67
68		let layout @ Surf { pos, size } = layout.x(lnum_w).w_sub(f32(scrollable) * SCR_PAD + lnum_w);
69		r.draw(Rect { pos, size, color: t.bg });
70
71		if !r.focused(id) {
72		} else if caret != select {
73			let (caret @ (_, b), select @ (_, e)) = ca::sort(*caret, *select);
74
75			for i in b.max(beg_y)..=e.min(end_y) {
76				let x = 0.0.or_val(i != b, || ca::adv(lines, font, scale, caret, 0.));
77				let w = size.x().or_val(i != e, || ca::adv(lines, font, scale, select, 0.));
78				let pos = pos.sum(p(lines, usize(i))).sum((x, 0));
79				r.draw(Rect { pos, size: (w - x, scale), color: t.highlight });
80			}
81		} else {
82			let x = ca::adv(lines, font, scale, *caret, CUR_PAD);
83			let Surf { pos, size } = layout.xy(p(lines, usize(caret.y()))).x(x).size((CUR_PAD, scale));
84			r.draw(Rect { pos, size, color: t.highlight });
85		}
86
87		let words = lines
88			.iter()
89			.enumerate()
90			.skip(start)
91			.take(len + 1)
92			.map(|(n, line)| {
93				let p = p(lines, n);
94				if let R(_) = line {
95					let Surf { pos, size } = layout.x_self(1).x(-CUR_PAD).size((CUR_PAD, CUR_PAD));
96					r.draw(Rect { pos: pos.sum(p), size, color: t.highlight });
97				}
98
99				(p, line)
100			})
101			.collect_vec();
102
103		words.into_iter().for_each(|(p, line)| {
104			let (p, text) = (pos.sum(p), line.as_clipped_str(font, scale, size.x()));
105			if !text.is_empty() {
106				r.draw(Text { pos: p, scale, color: t.text, text, font });
107			}
108
109			if 0. < lnum_w
110				&& let Some(text) = line.lnum()
111			{
112				let (pos, text) = ((pos.x() - lnum_w, p.y()), &text);
113				r.draw(Text { pos, scale, color: t.highlight, text, font });
114			}
115		});
116
117		let (pos, sc) = (pos.sum(p(lines, start)), Cell::from_mut(scrollbar));
118		r.logic(
119			layout,
120			move |e, focused, mouse_pos| {
121				let parser = Cell::from_mut(&mut parser);
122				let (cs @ (c, s), rw, lines) = ((*caret, *select), !readonly, || (unsafe { &*parser.as_ptr() }).pipe(|((_, l), _)| l));
123
124				let click = |c: Vec2| ca::at_pos(lines(), font, scale, start, c.sub(pos));
125				let max_caret = || ca::set(lines(), vec2(isize::MAX), (0, 0));
126
127				let move_pip = |o: f32| sc.mutate(|s| s.pip_pos = (s.pip_pos + o * adv).clamp(0., 1.).or_val(scrollable, || 1.));
128
129				let center_pip = |sel @ (c, _): (Caret, _)| {
130					f32(beg_y + len_y / 2 - c.y()).pipe(move_pip);
131					sel
132				};
133
134				let clamp_pip = |sel @ (c, _): (Caret, _)| {
135					if c.y() >= end_y {
136						f32(beg_y + len_y - c.y()).pipe(move_pip)
137					} else if c.y() <= beg_y {
138						f32(beg_y + 1 - c.y()).pipe(move_pip)
139					}
140					sel
141				};
142
143				let move_caret = |c, o| ca::set(lines(), c, o);
144
145				let set_caret = |m: Mod, c| (c, s.or_val(m.shift(), || c));
146
147				let collect = |(c, s)| u::collect_range(lines().iter(), c, s).pipe(task::block_on);
148
149				let edit = |pre, post, ins: &str, copy_out: fn(&str)| {
150					let (c, s) = ca::sort(c, s);
151					let s = s.or_val(c != s, || move_caret(c, (pre, 0)));
152					let (c, s) = ca::sort(c, s);
153
154					if c == s && ins.is_empty() {
155						return (c, c);
156					}
157
158					if c != s {
159						let del = &collect(cs);
160						copy_out(del);
161					}
162
163					parser.mutate(|p| p.set(|((_, l), _)| u::replace_range(l, ins, c, s)));
164
165					let c = move_caret(c, (post, 0));
166
167					parser.mutate(|p| {
168						let (caret_was, font) = (c, font.clone());
169						p.update(async move |((_, lines), _)| {
170							let caret = ca::serialise(lines.iter(), caret_was).await;
171
172							let text = String::new()
173								.tap_async(async |t| stream::iter(lines.iter()).for_each(|l| l.write_self(t)).await)
174								.await;
175
176							let (lines, parsed) = lazy_parse(text.into(), font).await.pipe(|(h, l)| (l.clone(), (h, l))); // TODO use vervec and compact() for more efficient reparses
177
178							let caret = ca::set_async(&lines, (0, 0), (caret, 0)).await;
179
180							let effect = move |mAffected { caret: c, select: s, history }: &mut _| {
181								if *c == caret_was && c == s {
182									(*c, *s) = (caret, caret);
183								}
184								history.add((caret, lines))
185							};
186
187							(parsed, effect.into())
188						})
189					});
190
191					(c, c)
192				};
193
194				let caret = |cs| (*caret, *select) = cs;
195
196				match *e {
197					OfferFocus => return Accept,
198					Defocus => move_pip(0.),
199					Scroll { at, m } => return move_pip(at.y() * if_ctrl(m, 10., 1.)).pipe(|_| Accept),
200					MouseButton { m, .. } if m.pressed() => set_caret(m, click(mouse_pos)).pipe(caret),
201					MouseMove { at, m } if focused && m.lmb() => set_caret(m, click(at)).pipe(clamp_pip).pipe(caret),
202					Keyboard { key, m } if focused && m.pressed() => {
203						let x = |o| set_caret(m, move_caret(c, (o, 0)));
204						let y = |o| set_caret(m, move_caret(c, (0, o)));
205
206						match key {
207							Key::Escape => return DropFocus,
208							Key::Right => x(if_ctrl(m, 10, 1)).pipe(clamp_pip).pipe(caret),
209							Key::Left => x(-if_ctrl(m, 10, 1)).pipe(clamp_pip).pipe(caret),
210							Key::Up => y(-1).pipe(clamp_pip).pipe(caret),
211							Key::Down => y(1).pipe(clamp_pip).pipe(caret),
212							Key::PageUp => y(-len_y).pipe(center_pip).pipe(caret),
213							Key::PageDown => y(len_y).pipe(center_pip).pipe(caret),
214							Key::A if m.ctrl() => (max_caret(), (0, 0)).pipe(center_pip).pipe(caret),
215							Key::C if m.ctrl() && c != s => collect(cs).pipe(RenderLock::set_clipboard),
216							Key::X if rw && m.ctrl() => edit(0, 0, "", |s| RenderLock::set_clipboard(s)).pipe(clamp_pip).pipe(caret),
217							Key::V if rw && m.ctrl() => edit(0, 0, &RenderLock::clipboard(), noop).pipe(clamp_pip).pipe(caret),
218							Key::Delete if rw => edit(1, 0, "", noop).pipe(clamp_pip).pipe(caret),
219							Key::Backspace if rw => edit(-1, 0, "", noop).pipe(clamp_pip).pipe(caret),
220							Key::Return if rw => edit(0, 1, "\n", noop).pipe(clamp_pip).pipe(caret),
221							Key::Z if rw && m.ctrl() => {
222								let h = 's: {
223									if !m.shift()
224										&& let h @ Some(_) = history.undo()
225									{
226										break 's h;
227									}
228									if m.shift()
229										&& let h @ Some(_) = history.redo()
230									{
231										break 's h;
232									}
233									None
234								};
235
236								if let Some((c, t)) = h {
237									parser.mutate(|p| p.set(move |((_, l), _)| *l = t));
238									(c, c).pipe(center_pip).pipe(caret)
239								}
240							}
241							_ => (),
242						}
243					}
244					Char { ch } if rw && focused => edit(0, 1, ch.as_str(), noop).pipe(clamp_pip).pipe(caret),
245					_ => (),
246				}
247				Accept.or_def(focused)
248			},
249			id,
250		);
251
252		if scrollable {
253			sc.mutate(|s| s.draw(r, t, layout.x_self(1).w(SCR_PAD), pip_size));
254		}
255	}
256}
257
258impl<'s: 'l, 'l> Lock::TextEdit<'s, 'l, '_> {
259	pub fn draw(self, g: impl Into<Surf>, sc: f32) {
260		let Self { s, r, t } = self;
261		s.draw(r, t, g.into(), sc, false)
262	}
263}
264
265#[derive(Default, Debug)]
266struct History {
267	states: Vec<TextState>,
268	at: usize,
269}
270impl History {
271	fn add(&mut self, (c, v): TextState) {
272		let HIST_SIZE = 100;
273
274		let Self { states, at } = self;
275
276		if *at + 1 < states.len() {
277			states.truncate(*at + 1);
278		}
279		states.push((c, v));
280
281		let len = states.len();
282		if len < HIST_SIZE {
283			*at += 1.or_def(len > 1);
284		} else {
285			states.remove(0);
286		}
287	}
288	fn undo(&mut self) -> Option<TextState> {
289		let Self { states, at } = self;
290
291		if *at < 1 {
292			None?
293		}
294		*at -= 1;
295
296		states.at(*at).clone().pipe(Some)
297	}
298	fn redo(&mut self) -> Option<TextState> {
299		let Self { states, at } = self;
300
301		if *at + 1 >= states.len() {
302			None?
303		}
304		*at += 1;
305
306		states.at(*at).clone().pipe(Some)
307	}
308}
309
310fn noop(_: &str) {}
311type Lines = VerVec<DynamicStr>;
312type ParseResult<T> = ((Vec2, Lines), Effect<T>);
313type TextState = (Caret, VerVec<DynamicStr>);
314use DynamicStr::*;