Skip to main content

ui/
ed_core.rs

1// ed_core -- the editing core of the editors with a text model of their own: richtext
2// (rich_edit.rs) and code_editor (code_edit.rs)
3//
4//   TextPos { line, byte }               a place in a document of lines (code) or paragraphs
5//                                        (rich text): the line and a UTF-8 byte offset in it
6//   TextSel { anchor, caret }            a selection (anchor == caret: the caret alone)
7//   (crate) EdView                       what the caret moves need of a widget: the lines'
8//                                        texts and their visual rows (byte ranges, caret x, y)
9//   (crate) ed_key(view, sel, prefx, key, page, tab) -> EdAct   what an editing key does:
10//                                        the caret moves (grapheme clusters, words, rows with
11//                                        a preferred x, pages, the document; Shift selects),
12//                                        the range Backspace / Delete remove (+Ctrl: words), Enter,
13//                                        Tab, clipboard and undo chords; None: not an editing key
14//   (crate) word_range(text, byte)       the word a double click selects (a run of blanks or of
15//                                        punctuation as one)
16//   (crate) Ui::ed_key_first / ed_key_take / ed_caret_on   the key queue past the reservation
17//                                        of the focused editor, the caret blink
18//
19// Both widgets take the edit state of the text boxes while they have the focus (edit.rs
20// ed_hold / ed_drop): the key filter of app-level key() then hides their editing keys,
21// textbox_selection / textbox_select and a screen reader's selection (Act::Select) work on the
22// text of their K_TEXTBOX record, the caret reaches magnifiers and the IME through ime_pos.
23// Keys follow the text boxes: Left / Right (+Ctrl words, +Shift select), Up / Down at the
24// preferred x, PgUp / PgDn by the view height, Home / End on the visual row (Ctrl: the
25// document), Backspace / Delete (+Ctrl words), Ctrl+A / C / X / V, Ctrl+Z undo, Ctrl+Y /
26// Ctrl+Shift+Z redo, Shift+Delete cut, Ctrl+Insert copy, Shift+Insert paste.  Right-to-left the
27// arrows follow the text (rtl_text_key, applied by the caller).
28
29use text::segment;
30
31use crate::consts::*;
32use crate::keys::*;
33use crate::Ui;
34
35/// a place in a document: the line (code) or paragraph (rich text) and a UTF-8 byte offset
36/// into its text
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct TextPos {
39    pub line: usize,
40    pub byte: usize,
41}
42
43impl TextPos {
44    /// the place `byte` of line `line`
45    pub const fn new(line: usize, byte: usize) -> TextPos {
46        TextPos { line, byte }
47    }
48}
49
50/// a selection: the anchor stays where it began, the caret moves (equal: no selection)
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct TextSel {
53    pub anchor: TextPos,
54    pub caret: TextPos,
55}
56
57impl TextSel {
58    /// the caret alone at p
59    pub const fn at(p: TextPos) -> TextSel {
60        TextSel { anchor: p, caret: p }
61    }
62    /// from anchor to caret
63    pub const fn new(anchor: TextPos, caret: TextPos) -> TextSel {
64        TextSel { anchor, caret }
65    }
66    /// anchor == caret: no text selected
67    pub fn is_empty(&self) -> bool {
68        self.anchor == self.caret
69    }
70    /// the earlier end
71    pub fn start(&self) -> TextPos {
72        self.anchor.min(self.caret)
73    }
74    /// the later end
75    pub fn end(&self) -> TextPos {
76        self.anchor.max(self.caret)
77    }
78    /// (start, end)
79    pub fn range(&self) -> (TextPos, TextPos) {
80        (self.start(), self.end())
81    }
82}
83
84/// what the caret moves need of an editor: its lines and their visual rows.  Coordinates are
85/// content pixels (x from the text's left edge, y from the document's top).
86pub(crate) trait EdView {
87    fn lines(&self) -> usize;
88    fn text(&self, line: usize) -> &str;
89    /// the visual rows of a line (at least 1)
90    fn rows(&self, line: usize) -> usize;
91    /// the byte range of a row; a soft break's index is the next row's start
92    fn row_range(&self, line: usize, row: usize) -> (usize, usize);
93    /// the caret x of `byte` inside that row
94    fn x_at(&self, line: usize, row: usize, byte: usize) -> i32;
95    /// the caret stop of that row nearest to x (a grapheme boundary)
96    fn hit_x(&self, line: usize, row: usize, x: i32) -> usize;
97    /// top and height of a row
98    fn row_y(&self, line: usize, row: usize) -> (i32, i32);
99    /// the row at y (clamped to the document)
100    fn row_at(&self, y: i32) -> (usize, usize);
101}
102
103/// the row of a line that shows `byte` (a soft break belongs to the next row)
104pub(crate) fn row_of(v: &dyn EdView, p: TextPos) -> usize {
105    let n = v.rows(p.line);
106    let mut r = n.saturating_sub(1);
107    while r > 0 && v.row_range(p.line, r).0 > p.byte {
108        r -= 1;
109    }
110    r
111}
112
113/// the caret x of p
114pub(crate) fn x_of(v: &dyn EdView, p: TextPos) -> i32 {
115    v.x_at(p.line, row_of(v, p), p.byte)
116}
117
118/// the end of the document
119pub(crate) fn doc_end(v: &dyn EdView) -> TextPos {
120    let l = v.lines().saturating_sub(1);
121    TextPos::new(l, v.text(l).len())
122}
123
124/// p kept inside the document and on a character boundary
125pub(crate) fn clamp_pos(v: &dyn EdView, p: TextPos) -> TextPos {
126    let n = v.lines();
127    if n == 0 {
128        return TextPos::default();
129    }
130    let line = p.line.min(n - 1);
131    let t = v.text(line);
132    let mut b = p.byte.min(t.len());
133    while !t.is_char_boundary(b) {
134        b -= 1;
135    }
136    TextPos::new(line, b)
137}
138
139/// the grapheme cluster boundary before p (the end of the previous line at a line start)
140pub(crate) fn prev_pos(v: &dyn EdView, p: TextPos) -> TextPos {
141    if p.byte == 0 {
142        if p.line == 0 {
143            return p;
144        }
145        return TextPos::new(p.line - 1, v.text(p.line - 1).len());
146    }
147    TextPos::new(p.line, segment::grapheme_prev(v.text(p.line), p.byte))
148}
149
150/// the grapheme cluster boundary after p (the start of the next line at a line end)
151pub(crate) fn next_pos(v: &dyn EdView, p: TextPos) -> TextPos {
152    let t = v.text(p.line);
153    if p.byte >= t.len() {
154        if p.line + 1 >= v.lines() {
155            return p;
156        }
157        return TextPos::new(p.line + 1, 0);
158    }
159    TextPos::new(p.line, segment::grapheme_next(t, p.byte))
160}
161
162/// Ctrl+Left: the start of the word before p (the end of the previous line at a line start)
163pub(crate) fn word_left(v: &dyn EdView, p: TextPos) -> TextPos {
164    if p.byte == 0 {
165        return prev_pos(v, p);
166    }
167    TextPos::new(p.line, segment::word_prev(v.text(p.line), p.byte))
168}
169
170/// Ctrl+Right: the start of the next word after p (the next line's start at a line end)
171pub(crate) fn word_right(v: &dyn EdView, p: TextPos) -> TextPos {
172    let t = v.text(p.line);
173    if p.byte >= t.len() {
174        return next_pos(v, p);
175    }
176    TextPos::new(p.line, segment::word_next(t, p.byte))
177}
178
179// the row after (delta 1) / before (-1) a row, None at the document's ends
180fn row_step(v: &dyn EdView, line: usize, row: usize, delta: i32) -> Option<(usize, usize)> {
181    if delta > 0 {
182        if row + 1 < v.rows(line) {
183            Some((line, row + 1))
184        } else if line + 1 < v.lines() {
185            Some((line + 1, 0))
186        } else {
187            None
188        }
189    } else if row > 0 {
190        Some((line, row - 1))
191    } else if line > 0 {
192        Some((line - 1, v.rows(line - 1) - 1))
193    } else {
194        None
195    }
196}
197
198/// Up / Down: `delta` rows away at the preferred x (kept in prefx, -1: none yet); past the
199/// first / last row: the document's start / end
200pub(crate) fn vmove(v: &dyn EdView, p: TextPos, prefx: &mut i32, delta: i32) -> TextPos {
201    let x = if *prefx < 0 { x_of(v, p) } else { *prefx };
202    *prefx = x.max(0);
203    let (mut line, mut row) = (p.line, row_of(v, p));
204    for _ in 0..delta.unsigned_abs() {
205        match row_step(v, line, row, delta.signum()) {
206            Some(r) => (line, row) = r,
207            None => return if delta < 0 { TextPos::default() } else { doc_end(v) },
208        }
209    }
210    TextPos::new(line, v.hit_x(line, row, x))
211}
212
213/// PgUp / PgDn: the row `dy` pixels above / below the caret's row at the preferred x
214pub(crate) fn page_move(v: &dyn EdView, p: TextPos, prefx: &mut i32, dy: i32) -> TextPos {
215    let x = if *prefx < 0 { x_of(v, p) } else { *prefx };
216    *prefx = x.max(0);
217    let row = row_of(v, p);
218    let (y, h) = v.row_y(p.line, row);
219    let target = y + h / 2 + dy;
220    if target < 0 {
221        return TextPos::default();
222    }
223    let (line, r) = v.row_at(target);
224    if (line, r) == (p.line, row) {
225        // the view holds less than a row: one row at least
226        return vmove(v, p, prefx, dy.signum());
227    }
228    TextPos::new(line, v.hit_x(line, r, x))
229}
230
231/// Home: the start of the caret's visual row
232pub(crate) fn row_home(v: &dyn EdView, p: TextPos) -> TextPos {
233    TextPos::new(p.line, v.row_range(p.line, row_of(v, p)).0)
234}
235
236/// End: the end of the caret's visual row (a soft-wrapped row: before its last cluster, the
237/// blank the row broke after, so the caret stays on that row)
238pub(crate) fn row_end(v: &dyn EdView, p: TextPos) -> TextPos {
239    let row = row_of(v, p);
240    let (a, b) = v.row_range(p.line, row);
241    if row + 1 < v.rows(p.line) && b > a {
242        return TextPos::new(p.line, segment::grapheme_prev(v.text(p.line), b).max(a));
243    }
244    TextPos::new(p.line, b)
245}
246
247/// what Backspace removes before p: the word (ctrl), an emoji sequence whole, of another
248/// cluster its last code point (an accent can be typed again); at a line start the break
249pub(crate) fn back_range(v: &dyn EdView, p: TextPos, ctrl: bool) -> (TextPos, TextPos) {
250    if p.byte == 0 {
251        return (prev_pos(v, p), p);
252    }
253    let t = v.text(p.line);
254    let a = if ctrl {
255        segment::word_prev(t, p.byte)
256    } else {
257        let g = segment::grapheme_prev(t, p.byte);
258        if segment::pictographic_cluster(t, g, p.byte) {
259            g
260        } else {
261            let mut c = p.byte - 1;
262            while !t.is_char_boundary(c) {
263                c -= 1;
264            }
265            c.max(g)
266        }
267    };
268    (TextPos::new(p.line, a), p)
269}
270
271/// what Delete removes after p: the cluster or the word (ctrl); at a line end the break
272pub(crate) fn del_range(v: &dyn EdView, p: TextPos, ctrl: bool) -> (TextPos, TextPos) {
273    let t = v.text(p.line);
274    if p.byte >= t.len() {
275        return (p, next_pos(v, p));
276    }
277    let b = if ctrl { segment::word_next(t, p.byte) } else { segment::grapheme_next(t, p.byte) };
278    (p, TextPos::new(p.line, b))
279}
280
281/// the word a double click at `byte` selects: a word, or a run of blanks / of punctuation
282pub(crate) fn word_range(t: &str, byte: usize) -> (usize, usize) {
283    if t.is_empty() {
284        return (0, 0);
285    }
286    let (mut a, mut b) = segment::word_at(t, byte.min(t.len()));
287    let kind = segment::word_kind(t, a, b);
288    if kind != segment::WK_WORD {
289        while a > 0 {
290            let (pa, pb) = segment::word_at(t, a - 1);
291            if segment::word_kind(t, pa, pb) != kind {
292                break;
293            }
294            a = pa;
295        }
296        while b < t.len() {
297            let (na, nb) = segment::word_at(t, b);
298            if segment::word_kind(t, na, nb) != kind {
299                break;
300            }
301            b = nb;
302        }
303    }
304    (a, b)
305}
306
307/// what an editing key does (ed_key)
308#[derive(Clone, Copy, Debug, PartialEq)]
309pub(crate) enum EdAct {
310    /// the selection moved (navigation, Ctrl+A)
311    Sel(TextSel),
312    /// remove a..b (Backspace / Delete; with a selection: the selection); word: Ctrl held
313    Del(TextPos, TextPos, bool),
314    /// Enter (no Ctrl / Alt)
315    Enter,
316    /// Tab / Shift+Tab (only when the editor takes Tab)
317    Tab(bool),
318    Copy,
319    Cut,
320    Paste,
321    Undo,
322    Redo,
323    /// a key whose text arrives as typed text (letters, digits, AltGr chords): consumed
324    Char,
325}
326
327/// ed_key(view, sel, prefx, key, page_h, tab) -> what the key (VK | MOD_* << 16, already mapped
328/// with rtl_text_key) does to an editor; None: not an editing key (it stays for the app).
329/// prefx is the preferred x of Up / Down (-1 after other keys); page_h the view height (px)
330pub(crate) fn ed_key(v: &dyn EdView, sel: TextSel, prefx: &mut i32, key: u32, page_h: i32, tab: bool) -> Option<EdAct> {
331    let vk = key & 0xFFFF;
332    let m = key >> 16;
333    let shift = m & MOD_SHIFT != 0;
334    let altgr = m & MOD_ALTGR != 0 || m & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT;
335    let ctrl = !altgr && m & MOD_CTRL != 0;
336    let alt = !altgr && m & MOD_ALT != 0;
337    if alt {
338        return None;
339    }
340    let keep = *prefx;
341    *prefx = -1;
342    let to = |p: TextPos| {
343        let p = clamp_pos(v, p);
344        Some(EdAct::Sel(if shift { TextSel::new(sel.anchor, p) } else { TextSel::at(p) }))
345    };
346    let c = sel.caret;
347    match vk {
348        VK_LEFT => {
349            if ctrl {
350                to(word_left(v, c))
351            } else if !shift && !sel.is_empty() {
352                to(sel.start())
353            } else {
354                to(prev_pos(v, c))
355            }
356        }
357        VK_RIGHT => {
358            if ctrl {
359                to(word_right(v, c))
360            } else if !shift && !sel.is_empty() {
361                to(sel.end())
362            } else {
363                to(next_pos(v, c))
364            }
365        }
366        VK_UP | VK_DOWN if !ctrl => {
367            *prefx = keep;
368            let p = vmove(v, c, prefx, if vk == VK_UP { -1 } else { 1 });
369            let px = *prefx;
370            let r = to(p);
371            *prefx = px;
372            r
373        }
374        VK_PRIOR | VK_NEXT if !ctrl => {
375            *prefx = keep;
376            let h = page_h.max(1);
377            let p = page_move(v, c, prefx, if vk == VK_PRIOR { -h } else { h });
378            let px = *prefx;
379            let r = to(p);
380            *prefx = px;
381            r
382        }
383        VK_HOME => to(if ctrl { TextPos::default() } else { row_home(v, c) }),
384        VK_END => to(if ctrl { doc_end(v) } else { row_end(v, c) }),
385        VK_BACK if !shift || !ctrl => {
386            if !sel.is_empty() {
387                Some(EdAct::Del(sel.start(), sel.end(), false))
388            } else {
389                let (a, b) = back_range(v, c, ctrl);
390                Some(EdAct::Del(a, b, ctrl))
391            }
392        }
393        VK_DELETE => {
394            if shift && !ctrl {
395                Some(EdAct::Cut)
396            } else if !sel.is_empty() {
397                Some(EdAct::Del(sel.start(), sel.end(), false))
398            } else {
399                let (a, b) = del_range(v, c, ctrl);
400                Some(EdAct::Del(a, b, ctrl))
401            }
402        }
403        VK_INSERT => {
404            if ctrl {
405                Some(EdAct::Copy)
406            } else if shift {
407                Some(EdAct::Paste)
408            } else {
409                Some(EdAct::Char)
410            }
411        }
412        VK_RETURN if !ctrl => Some(EdAct::Enter),
413        VK_TAB if tab && !ctrl => Some(EdAct::Tab(shift)),
414        _ if ctrl => match vk {
415            0x41 if !shift => Some(EdAct::Sel(TextSel::new(TextPos::default(), doc_end(v)))),
416            0x43 if !shift => Some(EdAct::Copy),
417            0x58 if !shift => Some(EdAct::Cut),
418            0x56 if !shift => Some(EdAct::Paste),
419            0x5A => Some(if shift { EdAct::Redo } else { EdAct::Undo }),
420            0x59 if !shift => Some(EdAct::Redo),
421            _ => None,
422        },
423        _ => {
424            // keys without a text of their own stay for the app
425            let own = vk.wrapping_sub(VK_SHIFT) <= VK_CAPITAL - VK_SHIFT
426                || vk.wrapping_sub(VK_LWIN) <= VK_APPS - VK_LWIN
427                || vk.wrapping_sub(VK_F1) <= 23
428                || vk.wrapping_sub(VK_NUMLOCK) <= 1
429                || vk.wrapping_sub(VK_LSHIFT) <= VK_RMENU - VK_LSHIFT
430                || matches!(vk, VK_ESCAPE | VK_TAB | VK_UP | VK_DOWN | VK_PRIOR | VK_NEXT | VK_RETURN);
431            if own {
432                *prefx = keep;
433                None
434            } else {
435                *prefx = keep;
436                Some(EdAct::Char)
437            }
438        }
439    }
440}
441
442/// the code points of typed text an editor inserts (control characters dropped; Tab kept when
443/// `tab`)
444pub(crate) fn typed_text(cps: &[u32], tab: bool) -> alloc::string::String {
445    let mut s = alloc::string::String::new();
446    for &c in cps {
447        if (c < 32 && !(tab && c == 9)) || c == 127 {
448            continue;
449        }
450        if let Some(ch) = char::from_u32(c) {
451            s.push(ch);
452        }
453    }
454    s
455}
456
457impl Ui {
458    /// the first unconsumed key of this frame (VK | MOD_* << 16), past the reservation of the
459    /// focused editor; 0: none
460    pub(crate) fn ed_key_first(&self) -> u32 {
461        self.wp.ctx.keyq.iter().copied().find(|&k| (k as i32) >= 0).unwrap_or(0)
462    }
463
464    /// consume the key ed_key_first returned
465    pub(crate) fn ed_key_take(&mut self) {
466        let q = &mut self.wp.ctx.keyq;
467        if let Some(i) = q.iter().position(|&k| (k as i32) >= 0) {
468            q[i] |= 0x8000_0000;
469        }
470    }
471
472    /// consume an unconsumed key with exactly these modifiers (Shift, Ctrl, Alt) anywhere in
473    /// the queue
474    pub(crate) fn ed_key_chord(&mut self, vk: u32, mods: u32) -> bool {
475        let m = MOD_SHIFT | MOD_CTRL | MOD_ALT;
476        let q = &mut self.wp.ctx.keyq;
477        match q.iter().position(|&k| (k as i32) >= 0 && k & 0xFFFF == vk && (k >> 16) & m == mods && k >> 16 & MOD_ALTGR == 0) {
478            Some(i) => {
479                q[i] |= 0x8000_0000;
480                true
481            }
482            None => false,
483        }
484    }
485
486    /// the caret is drawn now (blinks with the system's time from t0; arms the timer;
487    /// headless: always)
488    pub(crate) fn ed_caret_on(&mut self, t0: u32) -> bool {
489        if self.headless || self.blink_ms == 0 {
490            return true;
491        }
492        let blink = self.blink_ms;
493        let t = self.now_ms.wrapping_sub(t0);
494        self.timer(UIT_CARET, blink - t % blink);
495        (t / blink) & 1 == 0
496    }
497
498    /// the widget is the window's focused widget and the window has the keyboard
499    pub(crate) fn ed_focused(&self, id: u32) -> bool {
500        self.wp.ctx.focus == id && self.uiw_cur == self.m.win.active
501    }
502
503    /// the IME's composition (preedit: IBus, Wayland text input) of the live window; empty:
504    /// none.  The editors draw it at the caret (the text boxes leave it to the IME's window).
505    pub(crate) fn ed_preedit(&self) -> plat::Preedit {
506        let pw = if self.headless { self.uiw_cur as i32 } else { self.uiw_pw(self.uiw_cur) };
507        if pw < 0 {
508            return plat::Preedit::default();
509        }
510        plat::ime_preedit(pw as u8)
511    }
512
513    /// draw a composition at x, the baseline y (row top / height for its box) in a font: a box
514    /// in the background colour over the text, the text underlined, a caret at its cursor
515    #[allow(clippy::too_many_arguments)]
516    pub(crate) fn ed_preedit_draw(&mut self, pe: &plat::Preedit, x: i32, top: i32, h: i32, base: i32, font: (u32, i32), fg: u32, bg: u32) {
517        let prev = self.text.get_font();
518        self.text.set_font(font.0, font.1);
519        let w = self.text.width(&pe.text).max(1);
520        let cx = self.text.caret_x(pe.text.as_bytes(), pe.cursor.min(pe.text.len()));
521        self.text.set_font(prev.0, prev.1);
522        let hair = self.hair().max(1);
523        self.draw_color(bg | 0xFF00_0000);
524        self.draw_rect(x, top, w + hair, h);
525        self.cmd_text(x, base, pe.text.as_bytes(), font.0, font.1, fg, 0);
526        self.draw_color(fg);
527        self.draw_rect(x, base + hair.max((font.1 >> 8) / 10), w, hair);
528        self.draw_rect(x + cx, top, hair, h);
529    }
530
531    /// give the focus to an editor without the focus ring (a click, a tool button)
532    pub(crate) fn ed_focus_quiet(&mut self, id: u32) {
533        if self.wp.ctx.focus != id {
534            self.wp.ctx.focus = id;
535            self.wp.ctx.focus_visible = false;
536            self.repass();
537        }
538    }
539}