Skip to main content

ui/
rich_edit.rs

1// rich_edit -- ui.richtext: a rich-text editor over a RichDoc (rich_doc.rs), and its toolbar
2//
3//   ui.richtext(id, &mut doc, RT_*) -> EV_*   the editor, filling the place it gets (natural
4//        480 x 240 DIP; next_size(-1, -1) fills): the paragraphs wrapped at the width
5//        (text::segment line breaks, grapheme clusters inside a word too long), runs in their
6//        fonts (bold / italic: the family's faces, else synthetic), colours, highlight,
7//        underline / strike, links in the accent colour, alignment (justify spreads the
8//        blanks), bulleted / numbered lists by level, headings, quotations; a scroll bar.
9//        EV_ACT: the content changed this frame; EV_FOCUS while focused; EV_COMMIT: the focus
10//        left.  Ids id .. id + 5 (id + 1 the scroll bar, id + 2 .. 5 the link flyout).
11//        RT_READONLY: select and copy only (a click opens a link); RT_NO_FRAME: no box;
12//        RT_OPEN_LINKS: the editor opens an activated link with open_url itself; RT_TAB: Tab
13//        outside a list inserts a tab (else Tab moves the focus on); RT_PLAIN_PASTE: paste
14//        only text.
15//   ui.richtext_toolbar(id, editor, &mut doc, RTB_*) -> EV_ACT when it changed doc
16//        undo / redo, paragraph style, font family (registered families: font_register),
17//        size, bold / italic / underline / strike, text and highlight colour, link, alignment,
18//        lists, indent, clear format; the buttons show the state at the caret and give the
19//        focus back to the editor (ids id .. id + 149)
20//   ui.richtext_link(id) -> Option<String>   a link the user activated (Ctrl+click, a click
21//        when read-only, Ctrl+Enter on it), taken once
22//   ui.richtext_link_dialog(id)              open the editor's link flyout (as Ctrl+K)
23//
24// Keys (besides those of ed_core.rs): Ctrl+B / I / U bold, italic, underline, Ctrl+Shift+X
25// strike, Ctrl+Space clear format, Ctrl+K link, Ctrl+L / E / R / J alignment, Ctrl+Shift+8 /
26// Ctrl+Shift+L bullets, Ctrl+Shift+7 numbers, Ctrl+1 / 2 / 3 headings, Ctrl+0 body text,
27// Ctrl+] / Ctrl+[ size up / down, Tab / Shift+Tab a list level in / out, Enter a new
28// paragraph (in an empty list item: the list ends), Backspace at the start of a list item:
29// the item becomes body text, Ctrl+Shift+V paste text only, Esc drops the focus.  With no
30// selection a format applies to what is typed next.
31// Clipboard: CLIP_TEXT (list markers when several paragraphs are copied) and CLIP_HTML
32// (rich_html.rs: HTML with the exact fragment as JSON in a comment); a paste takes HTML when
33// there is some (any program's), else the text.  Undo: the document's History (rich_doc.rs).
34// Accessibility: a K_TEXTBOX record (label: the text, value / max: caret / anchor as byte
35// offsets into it) as text boxes have, a K_LINK record per visible link; the caret goes to
36// ime_pos (IME, magnifiers); the editor takes the text boxes' edit state while focused
37// (ed_hold), so a screen reader can select.  Right-to-left the text keeps its order (as in
38// text boxes); bidirectional reordering inside a paragraph is not done.
39//
40// Layout: a paragraph is laid out once per version (Para::version, its attributes, the
41// width, dpi, text scale and document font); the rows are cached per editor.  Only the
42// visible paragraphs are drawn.
43
44use alloc::collections::BTreeMap;
45use alloc::rc::Rc;
46use alloc::string::String;
47use alloc::vec::Vec;
48
49use text::segment;
50use text::{FS_SYN_BOLD, FS_SYN_ITALIC};
51
52use crate::cmd::{Draw, Gfx};
53use crate::consts::*;
54use crate::ed_core::*;
55use crate::i18n::tr;
56use crate::keys::*;
57use crate::rich_doc::*;
58use crate::rich_html::clip_html;
59use crate::vicon::paint_svg_icon2;
60use crate::{FontSel, Sp, Ui};
61
62/// richtext flags: select and copy only
63pub const RT_READONLY: u32 = 1;
64/// no text box frame
65pub const RT_NO_FRAME: u32 = 2;
66/// the editor opens activated links itself (open_url)
67pub const RT_OPEN_LINKS: u32 = 4;
68/// Tab outside a list inserts a tab
69pub const RT_TAB: u32 = 8;
70/// a paste takes only the text
71pub const RT_PLAIN_PASTE: u32 = 16;
72
73/// richtext_toolbar groups: undo / redo
74pub const RTB_HISTORY: u32 = 1;
75/// paragraph style, font family, size
76pub const RTB_STYLE: u32 = 2;
77/// bold, italic, underline, strike
78pub const RTB_CHARS: u32 = 4;
79/// text and highlight colour
80pub const RTB_COLOR: u32 = 8;
81/// link
82pub const RTB_LINK: u32 = 16;
83/// alignment
84pub const RTB_ALIGN: u32 = 32;
85/// lists and indent
86pub const RTB_LISTS: u32 = 64;
87/// clear format
88pub const RTB_CLEAR: u32 = 128;
89/// every group
90pub const RTB_ALL: u32 = 255;
91
92const NAT_W: i32 = 480;
93const NAT_H: i32 = 240;
94const LEVEL_W: i32 = 24; // DIP per indent level
95const LIST_W: i32 = 26; // DIP for the list marker
96const QUOTE_W: i32 = 16; // DIP of a quotation's indent
97const TAB_W: i32 = 40; // DIP between tab stops
98
99// a piece of a row: bytes a..b of one run, at x (px from the text column's left)
100#[derive(Clone)]
101struct Piece {
102    a: usize,
103    b: usize,
104    x: i32,
105    w: i32,
106    font: u32,
107    size: i32,
108    asc: i32,
109    flags: u8,
110    color: u32,
111    bg: u32,
112    link: bool,
113    tab: bool,
114}
115
116// a visual row of a paragraph
117struct Row {
118    a: usize,
119    b: usize,
120    // top (from the paragraph's first row), height, baseline offset
121    y: i32,
122    h: i32,
123    base: i32,
124    // the caret stops: grapheme boundaries a..=b and their x
125    stops: Vec<(usize, i32)>,
126    pieces: Vec<Piece>,
127}
128
129impl Row {
130    fn x(&self, byte: usize) -> i32 {
131        match self.stops.binary_search_by(|s| s.0.cmp(&byte)) {
132            Ok(i) => self.stops[i].1,
133            Err(0) => self.stops.first().map_or(0, |s| s.1),
134            Err(i) => self.stops[i - 1].1,
135        }
136    }
137    fn hit(&self, x: i32) -> usize {
138        self.stops.iter().min_by_key(|s| (s.1 - x).abs()).map_or(self.a, |s| s.0)
139    }
140    // the character under x (its start), None outside the row's text
141    fn char_at(&self, x: i32) -> Option<usize> {
142        let w = self.stops.windows(2).find(|w| x >= w[0].1 && x < w[1].1)?;
143        Some(w[0].0)
144    }
145}
146
147// the layout of a paragraph
148struct PLay {
149    // space above the first row, the rows' height, the space below
150    top: i32,
151    h: i32,
152    rows: Vec<Row>,
153    // where the text column starts (indent, list marker, quotation)
154    indent: i32,
155    // the marker's font and size (the first run's)
156    mfont: (u32, i32, i32),
157}
158
159#[derive(Clone, PartialEq, Default)]
160struct LKey {
161    w: i32,
162    dpi: i32,
163    scale: u32,
164    font: String,
165    size: u16,
166    ui: u32,
167}
168
169// the state of one editor between frames
170#[derive(Default)]
171struct View {
172    seen: u32,
173    scroll: i32,
174    key: LKey,
175    lays: BTreeMap<(u32, u8, u8, u8, u8), Rc<PLay>>,
176    t0: u32,
177    prefx: i32,
178    // pointer drag: 0 none, 1 characters, 2 words (the word it began on)
179    drag: u8,
180    word: (TextPos, TextPos),
181    held: Option<(usize, usize)>,
182    last_sel: TextSel,
183    last_rev: u64,
184    // the text for the accessibility record, by document and revision
185    label: (u32, u64, String),
186    // the link flyout: open, its text, the caret's rectangle (anchor)
187    link_open: bool,
188    link_text: String,
189    caret_rect: [i32; 4],
190}
191
192/// shared state (Ui::m.rich)
193#[derive(Default)]
194pub(crate) struct S {
195    views: BTreeMap<u32, View>,
196    link: Option<(u32, String)>,
197    icons: BTreeMap<(usize, u32, u32), Rc<Art>>,
198}
199
200// a toolbar icon (vicon path data) in a colour and an accent colour
201struct Art {
202    art: &'static str,
203    color: u32,
204    accent: u32,
205}
206
207impl Draw for Art {
208    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
209        let s = r[2].min(r[3]);
210        paint_svg_icon2(g, self.art, r[0] + (r[2] - s) / 2, r[1] + (r[3] - s) / 2, s << 8, self.color, self.accent, 1.75);
211    }
212}
213
214// toolbar icons on a 24 x 24 grid
215const ART_BOLD: &str = "S2.4M7.5 5H13A3.5 3.5 0 0 1 13 12H7.5ZM7.5 12H14A3.5 3.5 0 0 1 14 19H7.5Z";
216const ART_ITALIC: &str = "M10 5H18M6 19H14M14 5L10 19";
217const ART_UNDER: &str = "M7 4V11A5 5 0 0 0 17 11V4M6 20H18";
218const ART_STRIKE: &str = "M16.5 7.5C15.8 5.9 14.2 5 12 5C9.3 5 7.5 6.3 7.5 8.4C7.5 10 8.6 11 10.4 11.5M4.5 12.5H19.5M8 16.2C8.7 18 10.3 19 12.3 19C15 19 16.8 17.6 16.8 15.6C16.8 14.8 16.6 14.2 16.1 13.6";
219const ART_LEFT: &str = "M4 6H20M4 10H14M4 14H20M4 18H14";
220const ART_CENTER: &str = "M4 6H20M7 10H17M4 14H20M7 18H17";
221const ART_RIGHT: &str = "M4 6H20M10 10H20M4 14H20M10 18H20";
222const ART_JUSTIFY: &str = "M4 6H20M4 10H20M4 14H20M4 18H20";
223const ART_BULLETS: &str = "M9.5 6.5H20M9.5 12H20M9.5 17.5H20|FM6.4 6.5A1.2 1.2 0 1 1 4 6.5A1.2 1.2 0 1 1 6.4 6.5Z|FM6.4 12A1.2 1.2 0 1 1 4 12A1.2 1.2 0 1 1 6.4 12Z|FM6.4 17.5A1.2 1.2 0 1 1 4 17.5A1.2 1.2 0 1 1 6.4 17.5Z";
224const ART_NUMBERS: &str = "M10 6.5H20M10 12H20M10 17.5H20|S1.3M4.4 5.2L5.6 4.4V8.6M4.4 8.6H6.8|S1.3M4.3 11C4.7 10.2 6.7 10.2 6.7 11.4C6.7 12.4 4.3 12.9 4.3 14.2H6.9|S1.3M4.3 16.1C4.8 15.5 6.7 15.5 6.7 16.7C6.7 17.4 6 17.7 5.4 17.7C6.1 17.7 6.9 18 6.9 18.9C6.9 20.1 4.9 20.3 4.2 19.5";
225const ART_INDENT: &str = "M11 6H20M11 10H20M11 14H20M11 18H20|M4 9L7.5 12L4 15";
226const ART_OUTDENT: &str = "M11 6H20M11 10H20M11 14H20M11 18H20|M7.5 9L4 12L7.5 15";
227const ART_LINK: &str = "M10 14L14 10M11 7L13 5A3.5 3.5 0 0 1 19 11L17 13M13 17L11 19A3.5 3.5 0 0 1 5 13L7 11";
228const ART_COLOR: &str = "M7 16L12 4.5L17 16M8.8 12H15.2|AFM4 18.5H20V21.5H4Z";
229const ART_HILITE: &str = "M14.5 4.5L19.5 9.5L11 18H6V13Z|M12.7 6.3L17.7 11.3|AFM3.5 19.5H20.5V22H3.5Z";
230const ART_CLEAR: &str = "M6 5H16M11 5L8.5 17|M14 14.5L19 19.5M19 14.5L14 19.5";
231const ART_UNDO: &str = "M9 14L4 9L9 4|M4 9H14.5A5.5 5.5 0 0 1 14.5 20H8";
232const ART_REDO: &str = "M15 14L20 9L15 4|M20 9H9.5A5.5 5.5 0 0 0 9.5 20H16";
233
234// the colours of the colour menus (0: automatic / none)
235const PALETTE: [u32; 20] = [
236    0xFF000000, 0xFF595959, 0xFF8C8C8C, 0xFFC0C0C0, 0xFFFFFFFF, 0xFFC00000, 0xFFE8590C, 0xFFF2C200, 0xFF2E9E44, 0xFF0F7B8C, 0xFF1F5FBF,
237    0xFF6A3FB5, 0xFFB5338A, 0xFF7F3F00, 0xFF3D6B00, 0xFFFFF27A, 0xFFB7F0A6, 0xFFA6E0F5, 0xFFF5C2E0, 0xFFFFD0A6,
238];
239const SIZES: [u16; 14] = [80, 90, 100, 110, 120, 140, 160, 180, 200, 240, 280, 360, 480, 720];
240
241// the scale of a paragraph style (percent) and whether it is bold
242fn style_scale(s: u8) -> (u32, bool) {
243    match s {
244        PS_H1 => (180, true),
245        PS_H2 => (145, true),
246        PS_H3 => (120, true),
247        _ => (100, false),
248    }
249}
250
251// the colour of c over an opaque background (alpha)
252fn opaque(c: u32) -> u32 {
253    if c >> 24 == 0 { c | 0xFF00_0000 } else { c }
254}
255
256// the view of the caret moves over the laid out paragraphs
257struct RView<'a> {
258    doc: &'a RichDoc,
259    lays: &'a [Rc<PLay>],
260    tops: &'a [i32],
261}
262
263impl EdView for RView<'_> {
264    fn lines(&self) -> usize {
265        self.doc.para_count()
266    }
267    fn text(&self, line: usize) -> &str {
268        self.doc.para(line).text()
269    }
270    fn rows(&self, line: usize) -> usize {
271        self.lays[line].rows.len().max(1)
272    }
273    fn row_range(&self, line: usize, row: usize) -> (usize, usize) {
274        let r = &self.lays[line].rows[row];
275        (r.a, r.b)
276    }
277    fn x_at(&self, line: usize, row: usize, byte: usize) -> i32 {
278        self.lays[line].rows[row].x(byte)
279    }
280    fn hit_x(&self, line: usize, row: usize, x: i32) -> usize {
281        self.lays[line].rows[row].hit(x)
282    }
283    fn row_y(&self, line: usize, row: usize) -> (i32, i32) {
284        let l = &self.lays[line];
285        let r = &l.rows[row];
286        (self.tops[line] + l.top + r.y, r.h)
287    }
288    fn row_at(&self, y: i32) -> (usize, usize) {
289        let n = self.lays.len();
290        let mut line = self.tops.partition_point(|&t| t <= y).saturating_sub(1).min(n - 1);
291        let l = &self.lays[line];
292        let ly = y - self.tops[line] - l.top;
293        if ly >= l.rows.last().map_or(0, |r| r.y + r.h) && line + 1 < n && y >= self.tops[line] + l.h {
294            line += 1;
295        }
296        let l = &self.lays[line];
297        let ly = y - self.tops[line] - l.top;
298        let row = l.rows.iter().rposition(|r| r.y <= ly).unwrap_or(0);
299        (line, row)
300    }
301}
302
303impl RView<'_> {
304    // the caret stop nearest to (x, y) (content px)
305    fn hit(&self, x: i32, y: i32) -> TextPos {
306        let (line, row) = self.row_at(y);
307        TextPos::new(line, self.hit_x(line, row, x))
308    }
309    // the character under (x, y), None beside the text
310    fn char_at(&self, x: i32, y: i32) -> Option<TextPos> {
311        let (line, row) = self.row_at(y);
312        let (ry, rh) = self.row_y(line, row);
313        if y < ry || y >= ry + rh {
314            return None;
315        }
316        self.lays[line].rows[row].char_at(x).map(|b| TextPos::new(line, b))
317    }
318}
319
320impl Ui {
321    // the font of a run: the family's face of that weight / slant, else one of its faces with
322    // synthetic styles, else the UI font
323    pub(crate) fn rt_face(&mut self, family: &str, bold: bool, italic: bool) -> u32 {
324        let syn = |ui: &mut Ui, f: u32, flags: u32| -> u32 {
325            if flags == 0 {
326                return f;
327            }
328            let r = ui.text.style(f, flags);
329            if r >= 0 { r as u32 } else { f }
330        };
331        let want = (if bold { FS_SYN_BOLD } else { 0 }) | (if italic { FS_SYN_ITALIC } else { 0 });
332        if !family.is_empty() {
333            if self.font_is_registered(family) {
334                let w = if bold { 700 } else { 400 };
335                if let Some(f) = self.font_registered_face(family, w, italic) {
336                    return f;
337                }
338                if let Some(f) = self.font_registered_face(family, w, false) {
339                    return syn(self, f, want & FS_SYN_ITALIC);
340                }
341                if let Some(f) = self.font_registered_face(family, 400, italic) {
342                    return syn(self, f, want & FS_SYN_BOLD);
343                }
344                if let Some(f) = self.font_registered_face(family, 400, false) {
345                    return syn(self, f, want);
346                }
347            } else if family.eq_ignore_ascii_case("monospace") {
348                let (f, _) = self.mono_font(11.0);
349                return syn(self, f, want);
350            } else if let Some(f) = self.font_load(&FontSel { family: String::from(family), weight: if bold { 700 } else { 400 }, italic, size: 11.0, axes: Vec::new() }) {
351                return f;
352            }
353        }
354        let base = if bold { text::FONT_BOLD } else { self.style.ts_font()[TS_BODY as usize] as u32 };
355        syn(self, base, want & FS_SYN_ITALIC)
356    }
357
358    // tenths of a point -> 24.8 px at this dpi and text scale
359    fn rt_px(&self, pt10: u32) -> i32 {
360        let px = pt10 as f32 / 10.0 * 4.0 / 3.0 * self.wp.rt.dpi as f32 / 96.0 * self.text_scale as f32 / 100.0;
361        ((px * 256.0) as i32).clamp(256, 512 * 256)
362    }
363
364    // lay out one paragraph at width w (px of the text column)
365    fn rt_lay(&mut self, p: &Para, w: i32, doc_font: &str, base10: u16, faces: &mut Vec<((String, bool, bool), u32)>) -> PLay {
366        let (scale, sbold) = style_scale(p.style);
367        let mut face = |ui: &mut Ui, f: &Fmt| -> (u32, i32) {
368            let fam = if f.font.is_empty() { doc_font } else { f.font.as_str() };
369            let bold = f.flags & RF_BOLD != 0 || sbold;
370            let italic = f.flags & RF_ITALIC != 0;
371            let k = (String::from(fam), bold, italic);
372            let id = match faces.iter().find(|e| e.0 == k) {
373                Some(e) => e.1,
374                None => {
375                    let id = ui.rt_face(fam, bold, italic);
376                    faces.push((k, id));
377                    id
378                }
379            };
380            let pt = if f.size != 0 { f.size } else { base10 } as u32;
381            (id, ui.rt_px(pt * scale / 100))
382        };
383        let indent = self.dp(LEVEL_W) * p.level as i32
384            + if p.list != LS_NONE { self.dp(LIST_W) } else { 0 }
385            + if p.style == PS_QUOTE { self.dp(QUOTE_W) } else { 0 };
386        let avail = (w - indent).max(self.dp(40));
387        let text = p.text();
388        let tb = text.as_bytes();
389        // the runs with their fonts
390        struct Seg {
391            a: usize,
392            b: usize,
393            font: u32,
394            size: i32,
395            asc: i32,
396            desc: i32,
397            lh: i32,
398            fmt: Fmt,
399        }
400        let mut segs: Vec<Seg> = Vec::new();
401        let metrics = |ui: &mut Ui, font: u32, size: i32| {
402            ui.text.set_font(font, size);
403            let (asc, desc) = (ui.text.ascent(), ui.text.descent());
404            (asc, desc, ui.text.line_h().max(asc + desc))
405        };
406        for (a, b, f) in p.spans() {
407            let (font, size) = face(self, f);
408            let (asc, desc, lh) = metrics(self, font, size);
409            segs.push(Seg { a, b, font, size, asc, desc, lh, fmt: f.clone() });
410        }
411        let (mfont, msize) = face(self, segs.first().map_or(&p.mark, |s| &s.fmt));
412        let (masc, mdesc, mlh) = metrics(self, mfont, msize);
413        let tab = self.dp(TAB_W).max(1) << 8;
414        // the width (24.8) of a..b laid out from pen x (tabs to the next stop)
415        let width = |ui: &mut Ui, a: usize, b: usize, x: i32| -> i32 {
416            let mut pen = x;
417            for s in &segs[segs.partition_point(|s| s.b <= a)..] {
418                if s.a >= b {
419                    break;
420                }
421                let (s0, s1) = (s.a.max(a), s.b.min(b));
422                if s0 >= s1 {
423                    continue;
424                }
425                ui.text.set_font(s.font, s.size);
426                let mut i = s0;
427                while i < s1 {
428                    if tb[i] == b'\t' {
429                        pen = (pen / tab + 1) * tab;
430                        i += 1;
431                        continue;
432                    }
433                    let j = tb[i..s1].iter().position(|&c| c == b'\t').map_or(s1, |k| i + k);
434                    pen += ui.text.measure_n(&tb[i..], j - i);
435                    i = j;
436                }
437            }
438            pen - x
439        };
440        // break the text into rows: the last opportunity that fits, else clusters
441        let mut rows_ab: Vec<(usize, usize)> = Vec::new();
442        if !text.is_empty() {
443            let ops: Vec<usize> = segment::line_breaks(text).into_iter().map(|o| o.0).collect();
444            let vis = |e: usize, s: usize| {
445                let mut v = e;
446                while v > s && (tb[v - 1] == b' ' || tb[v - 1] == b'\t') {
447                    v -= 1;
448                }
449                v
450            };
451            let lim = avail << 8;
452            let mut start = 0;
453            let mut x = 0; // pen of the row so far (24.8)
454            let mut prev = 0; // the last opportunity in the row
455            for &o in &ops {
456                if o <= prev {
457                    continue;
458                }
459                let atom = width(self, prev, vis(o, prev), x);
460                if x + atom > lim && prev > start {
461                    rows_ab.push((start, prev));
462                    start = prev;
463                    x = 0;
464                }
465                if x == 0 {
466                    // an atom wider than the row: clusters while they fit
467                    let mut a = start;
468                    loop {
469                        // measured in growing chunks: a long atom is not re-measured whole for every row
470                        let w_all = {
471                            let ve = vis(o, a);
472                            let mut n = 64;
473                            loop {
474                                let mut pe = (a + n).min(ve);
475                                while !text.is_char_boundary(pe) {
476                                    pe -= 1;
477                                }
478                                let w = width(self, a, pe, 0);
479                                if w > lim || pe >= ve {
480                                    break w;
481                                }
482                                n *= 2;
483                            }
484                        };
485                        if w_all <= lim || a >= o {
486                            break;
487                        }
488                        let mut e = segment::grapheme_next(text, a).min(o);
489                        while e < o {
490                            let n = segment::grapheme_next(text, e).min(o);
491                            if width(self, a, n, 0) > lim {
492                                break;
493                            }
494                            e = n;
495                        }
496                        rows_ab.push((a, e));
497                        a = e;
498                        start = e;
499                    }
500                }
501                x += width(self, prev.max(start), o, x);
502                prev = o;
503            }
504            rows_ab.push((start, text.len()));
505        } else {
506            rows_ab.push((0, 0));
507        }
508        // the rows: pieces, caret stops, metrics, alignment
509        let mut rows: Vec<Row> = Vec::with_capacity(rows_ab.len());
510        let mut y = 0;
511        let nrows = rows_ab.len();
512        for (ri, &(ra, rb)) in rows_ab.iter().enumerate() {
513            let mut pieces: Vec<Piece> = Vec::new();
514            let mut stops: Vec<(usize, i32)> = Vec::new();
515            let (mut asc, mut desc, mut lh) = (0, 0, 0);
516            let mut pen = 0; // 24.8
517            let justify = p.align == RA_JUSTIFY && ri + 1 < nrows;
518            for s in &segs[segs.partition_point(|s| s.b <= ra)..] {
519                if s.a >= rb {
520                    break;
521                }
522                let (s0, s1) = (s.a.max(ra), s.b.min(rb));
523                if s0 >= s1 {
524                    continue;
525                }
526                asc = asc.max(s.asc);
527                desc = desc.max(s.desc);
528                lh = lh.max(s.lh);
529                self.text.set_font(s.font, s.size);
530                // pieces split at tabs (and, justified, after blanks)
531                let mut i = s0;
532                while i < s1 {
533                    if tb[i] == b'\t' {
534                        let np = (pen / tab + 1) * tab;
535                        stops.push((i, (pen + 128) >> 8));
536                        pieces.push(Piece { a: i, b: i + 1, x: (pen + 128) >> 8, w: (np - pen + 128) >> 8, font: s.font, size: s.size, asc: s.asc, flags: s.fmt.flags, color: s.fmt.color, bg: s.fmt.bg, link: !s.fmt.link.is_empty(), tab: true });
537                        pen = np;
538                        i += 1;
539                        continue;
540                    }
541                    let mut j = tb[i..s1].iter().position(|&c| c == b'\t').map_or(s1, |k| i + k);
542                    if justify {
543                        if let Some(k) = tb[i..j].iter().position(|&c| c == b' ') {
544                            j = i + k + 1;
545                        }
546                    }
547                    let piece = &tb[i..j];
548                    let x0 = pen;
549                    // a stop at every cluster boundary
550                    let mut g = i;
551                    while g < j {
552                        stops.push((g, (x0 + self.text.measure_n(piece, g - i) + 128) >> 8));
553                        g = segment::grapheme_next(text, g).min(j);
554                    }
555                    let w = self.text.measure_n(piece, j - i);
556                    pieces.push(Piece { a: i, b: j, x: (x0 + 128) >> 8, w: (w + 255) >> 8, font: s.font, size: s.size, asc: s.asc, flags: s.fmt.flags, color: s.fmt.color, bg: s.fmt.bg, link: !s.fmt.link.is_empty(), tab: false });
557                    pen = x0 + w;
558                    i = j;
559                }
560            }
561            stops.push((rb, (pen + 128) >> 8));
562            if pieces.is_empty() {
563                asc = masc;
564                desc = mdesc;
565                lh = mlh;
566            }
567            let h = lh.max(asc + desc).max(1);
568            let base = asc + (h - asc - desc) / 2;
569            // the visible width (without trailing blanks) and the alignment offset
570            let mut vis_end = rb;
571            while vis_end > ra && (tb[vis_end - 1] == b' ' || tb[vis_end - 1] == b'\t') {
572                vis_end -= 1;
573            }
574            let vis_w = stops.iter().find(|s| s.0 >= vis_end).map_or(0, |s| s.1);
575            let off = match p.align {
576                RA_CENTER => ((avail - vis_w) / 2).max(0),
577                RA_RIGHT => (avail - vis_w).max(0),
578                _ => 0,
579            };
580            if justify {
581                let blanks: Vec<usize> = (ra..vis_end).filter(|&i| tb[i] == b' ').collect();
582                if !blanks.is_empty() && vis_w < avail {
583                    let extra = ((avail - vis_w) as i64) << 8;
584                    let n = blanks.len() as i64;
585                    let shift = |b: usize| -> i32 {
586                        let k = blanks.partition_point(|&s| s < b) as i64;
587                        ((extra * k / n) >> 8) as i32
588                    };
589                    for s in stops.iter_mut() {
590                        s.1 += shift(s.0);
591                    }
592                    for pc in pieces.iter_mut() {
593                        pc.x += shift(pc.a);
594                    }
595                }
596            }
597            for s in stops.iter_mut() {
598                s.1 += indent + off;
599            }
600            for pc in pieces.iter_mut() {
601                pc.x += indent + off;
602            }
603            stops.dedup_by_key(|s| s.0);
604            rows.push(Row { a: ra, b: rb, y, h, base, stops, pieces });
605            y += h;
606        }
607        let top = match p.style {
608            PS_H1 => self.dp(12),
609            PS_H2 => self.dp(10),
610            PS_H3 => self.dp(8),
611            _ => 0,
612        };
613        let gap = if p.list != LS_NONE { self.dp(3) } else { self.dp(8) };
614        PLay { top, h: top + y + gap, rows, indent, mfont: (mfont, msize, masc) }
615    }
616
617    // the layout of every paragraph (cached by version) -> (layouts, tops, height)
618    fn rt_layout(&mut self, v: &mut View, doc: &RichDoc, w: i32) -> (Vec<Rc<PLay>>, Vec<i32>, i32) {
619        let key = LKey { w, dpi: self.wp.rt.dpi, scale: self.layout_scale(), font: doc.font.clone(), size: doc.size, ui: self.style.ts_font()[TS_BODY as usize] as u32 };
620        if key != v.key {
621            v.lays.clear();
622            v.key = key;
623        }
624        let base10 = if doc.size == 0 { 110 } else { doc.size };
625        let prev = self.text.get_font();
626        let mut faces = Vec::new();
627        let mut keep = BTreeMap::new();
628        let mut lays = Vec::with_capacity(doc.para_count());
629        let mut tops = Vec::with_capacity(doc.para_count());
630        let mut y = 0;
631        for p in doc.paras() {
632            let k = (p.version(), p.align, p.list, p.level, p.style);
633            let l = match v.lays.get(&k) {
634                Some(l) => l.clone(),
635                None => Rc::new(self.rt_lay(p, w, &doc.font, base10, &mut faces)),
636            };
637            tops.push(y);
638            y += l.h;
639            keep.insert(k, l.clone());
640            lays.push(l);
641        }
642        v.lays = keep;
643        self.text.set_font(prev.0, prev.1);
644        (lays, tops, y)
645    }
646
647    /// ui_richtext(id, doc, RT_*) -> EV_* (see the module header)
648    #[cfg_attr(jfgui_dev, track_caller)]
649    pub fn richtext(&mut self, id: u32, doc: &mut RichDoc, flags: u32) -> u32 {
650        let _src = self.src_scope();
651        let ro = flags & RT_READONLY != 0;
652        let stflags = self.take_flags();
653        let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
654        let hair = self.hair();
655        let st = self.style;
656        let frame = flags & RT_NO_FRAME == 0;
657        let pad = if frame { self.dp(10) } else { self.dp(2) };
658        let sbw = st.scrollbar_thickness(self);
659        let (tx, ty) = (r[0] + pad, r[1] + pad);
660        let tw = (r[2] - pad * 2 - sbw).max(self.dp(40));
661        let th = (r[3] - pad * 2).max(1);
662        let mut v = self.m.rich.views.remove(&id).unwrap_or_default();
663        v.seen = self.frame;
664        let rev0 = doc.revision();
665        let sel0 = doc.selection();
666        let (mut lays, mut tops, mut total) = self.rt_layout(&mut v, doc, tw);
667        // behave: one box for the pointer and the focus
668        let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
669        if stflags & ST_DISABLED != 0 {
670            hb |= HB_DISABLED;
671        }
672        self.wp.ctx.last_rect = r;
673        let hit0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
674        let ev = self.behave(id, hb);
675        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
676        let focused = !disabled && self.ed_focused(id);
677        let mut out = ev & (EV_HOVER | EV_HELD | EV_DOUBLE | EV_RCLICK | EV_DRAG);
678        if focused {
679            // a selection set from outside (a screen reader) since the last frame
680            if let (Some(sel), Some(held)) = (self.textbox_selection(id), v.held) {
681                if sel != held {
682                    doc.set_selection(TextSel::new(doc.unflat(sel.0), doc.unflat(sel.1)));
683                }
684            }
685            if let Some(t) = self.a11y_text(id) {
686                if !ro {
687                    doc.set_paras(RichDoc::from_plain(&t).paras().to_vec());
688                    let e = doc.end();
689                    doc.set_selection(TextSel::at(e));
690                }
691            }
692        } else if v.held.is_some() {
693            v.held = None;
694            v.drag = 0;
695            self.ed_drop(id);
696            out |= EV_COMMIT;
697        }
698        if doc.revision() != rev0 {
699            (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
700        }
701        let mut laid = doc.revision();
702        // ---- pointer
703        let (mx, my) = (self.rtl_map(self.wp.ctx.mx, tx, tw), self.wp.ctx.my);
704        let (cx, cy) = (mx - tx, my - ty + v.scroll);
705        let c = &self.wp.ctx;
706        let press = c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id;
707        if press && !disabled {
708            let view = RView { doc: &*doc, lays: &lays, tops: &tops };
709            let p = view.hit(cx, cy);
710            let link = view.char_at(cx, cy).and_then(|q| doc.link_at(q).map(String::from));
711            v.prefx = -1;
712            v.t0 = self.now_ms;
713            if let Some(url) = link.filter(|_| ro || self.mods & MOD_CTRL != 0) {
714                self.rt_link_activate(id, &url, flags);
715                v.drag = 0;
716            } else if ev & EV_DOUBLE != 0 {
717                let (a, b) = word_range(doc.para(p.line).text(), p.byte);
718                v.word = (TextPos::new(p.line, a), TextPos::new(p.line, b));
719                doc.set_selection(TextSel::new(v.word.0, v.word.1));
720                v.drag = 2;
721            } else if self.mods & MOD_SHIFT != 0 {
722                let a = doc.selection().anchor;
723                doc.set_selection(TextSel::new(a, p));
724                v.drag = 1;
725            } else {
726                doc.set_selection(TextSel::at(p));
727                v.drag = 1;
728            }
729        } else if ev & EV_HELD != 0 && v.drag != 0 {
730            let lh = lays.first().and_then(|l| l.rows.first()).map_or(self.dp(20), |r| r.h);
731            if my < ty {
732                v.scroll -= lh;
733            } else if my >= ty + th {
734                v.scroll += lh;
735            }
736            v.scroll = v.scroll.clamp(0, (total - th).max(0));
737            let cy = my - ty + v.scroll;
738            let view = RView { doc: &*doc, lays: &lays, tops: &tops };
739            let p = view.hit(cx, cy);
740            if v.drag == 2 {
741                let (a, b) = word_range(doc.para(p.line).text(), p.byte);
742                let (wa, wb) = (TextPos::new(p.line, a), TextPos::new(p.line, b));
743                if wa < v.word.0 {
744                    doc.set_selection(TextSel::new(v.word.1, wa));
745                } else {
746                    doc.set_selection(TextSel::new(v.word.0, wb.max(v.word.1)));
747                }
748            } else {
749                let a = doc.selection().anchor;
750                doc.set_selection(TextSel::new(a, p));
751            }
752        } else if ev & EV_HELD == 0 {
753            v.drag = 0;
754        }
755        // ---- keys and typed text
756        if focused && !v.link_open {
757            loop {
758                if doc.revision() != laid {
759                    (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
760                    laid = doc.revision();
761                }
762                let k0 = self.ed_key_first();
763                if k0 == 0 {
764                    break;
765                }
766                let k = self.rtl_text_key(k0);
767                if self.rt_key(id, doc, &mut v, k, flags) {
768                    self.ed_key_take();
769                    v.t0 = self.now_ms;
770                    if v.link_open {
771                        break;
772                    }
773                    continue;
774                }
775                let act = {
776                    let view = RView { doc: &*doc, lays: &lays, tops: &tops };
777                    ed_key(&view, doc.selection(), &mut v.prefx, k, th, false)
778                };
779                let Some(act) = act else { break };
780                self.ed_key_take();
781                v.t0 = self.now_ms;
782                match act {
783                    EdAct::Sel(s) => doc.set_selection(s),
784                    EdAct::Del(a, b, _) if !ro => {
785                        let back = k & 0xFFFF == VK_BACK;
786                        doc.delete_kind(a, b, if !doc.selection().is_empty() { 0 } else if back { RK_BACK } else { RK_DEL });
787                    }
788                    EdAct::Enter if !ro => {
789                        doc.split();
790                    }
791                    EdAct::Copy => self.rt_copy(doc),
792                    EdAct::Cut => {
793                        self.rt_copy(doc);
794                        if !ro {
795                            doc.delete_selection();
796                        }
797                    }
798                    EdAct::Paste if !ro => self.rt_paste(doc, flags & RT_PLAIN_PASTE != 0),
799                    EdAct::Undo if !ro => {
800                        doc.undo();
801                    }
802                    EdAct::Redo if !ro => {
803                        doc.redo();
804                    }
805                    _ => {}
806                }
807            }
808            let cps = self.text_take(super::ctx::TEXTQ_MAX);
809            if !ro {
810                let s = typed_text(&cps, false);
811                if !s.is_empty() {
812                    doc.insert_text_kind(&s, RK_TYPE);
813                    v.t0 = self.now_ms;
814                    v.prefx = -1;
815                }
816            }
817        }
818        if doc.revision() != laid || lays.len() != doc.para_count() {
819            (lays, tops, total) = self.rt_layout(&mut v, doc, tw);
820        }
821        let changed = doc.revision() != rev0;
822        let moved = doc.selection() != sel0;
823        if changed {
824            out |= EV_ACT;
825        }
826        if changed || moved {
827            self.repass();
828        }
829        // ---- wheel, the caret into view
830        let lh = lays.first().and_then(|l| l.rows.first()).map_or(self.dp(20), |r| r.h);
831        let wy = self.wp.ctx.wheel_y;
832        if (ev & EV_HOVER != 0 || self.wp.ctx.hot == id.wrapping_add(1)) && wy != 0 {
833            let s = (v.scroll - wy * lh * 3 / 120).clamp(0, (total - th).max(0));
834            if s != v.scroll {
835                v.scroll = s;
836                self.wp.ctx.wheel_y = 0;
837            }
838        }
839        // the finger's way (touch.rs)
840        let (_, pan) = self.pan_take(id);
841        v.scroll = (v.scroll + pan).clamp(0, (total - th).max(0));
842        let caret = doc.clamp(doc.selection().caret);
843        let (crow_y, crow_h, cxp) = {
844            let view = RView { doc: &*doc, lays: &lays, tops: &tops };
845            let row = row_of(&view, caret);
846            let (y, h) = view.row_y(caret.line, row);
847            (y, h, view.x_at(caret.line, row, caret.byte))
848        };
849        if focused && (doc.selection() != v.last_sel || doc.revision() != v.last_rev) {
850            if crow_y < v.scroll {
851                v.scroll = crow_y;
852            }
853            if crow_y + crow_h > v.scroll + th {
854                v.scroll = crow_y + crow_h - th;
855            }
856        }
857        v.last_sel = doc.selection();
858        v.last_rev = doc.revision();
859        v.scroll = v.scroll.clamp(0, (total - th).max(0));
860
861        // ---- frame
862        if frame {
863            let mut sp = Sp { value: NAT_W, value2: lh, flags: 1, ..Sp::default() };
864            let _ = st.textbox_metrics(self, &mut sp);
865            sp.place(r);
866            sp.ev = ev;
867            sp.value = focused as i32;
868            sp.value2 = disabled as i32;
869            st.textbox_frame(self, &sp);
870        }
871        self.clip_push(r[0] + hair, r[1] + hair, (r[2] - hair * 2).max(0), (r[3] - hair * 2).max(0));
872        self.ltr_begin(tx, tw);
873        let marks = list_marks(doc.paras());
874        let (sa, sb) = doc.selection().range();
875        let sel_col = if focused {
876            (self.color(T_ACCENT) & 0x00FF_FFFF) | 0x60 << 24
877        } else {
878            (self.color(T_TEXT3) & 0x00FF_FFFF) | 0x38 << 24
879        };
880        let tcol = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT });
881        let qcol = self.color(T_TEXT2);
882        let lcol = self.color(T_ACCENT_TEXT);
883        let hc = self.high_contrast;
884        let first = tops.partition_point(|&t| t <= v.scroll).saturating_sub(1);
885        let mut links: Vec<([i32; 4], String)> = Vec::new();
886        for line in first..lays.len() {
887            let top = ty + tops[line] - v.scroll;
888            if top > ty + th {
889                break;
890            }
891            let l = &lays[line];
892            let p = doc.para(line);
893            let ptop = top + l.top;
894            // quotation bar, list marker
895            if p.style == PS_QUOTE {
896                let x = tx + self.dp(LEVEL_W) * p.level as i32 + self.dp(4);
897                let hgt = l.rows.last().map_or(0, |r| r.y + r.h);
898                self.draw_color(self.color(T_CTRL_STROKE2) | 0xFF00_0000);
899                self.draw_rect(x, ptop, self.dp(3), hgt);
900            }
901            if let (Some(m), Some(row)) = (&marks[line], l.rows.first()) {
902                let (mf, ms, masc) = l.mfont;
903                let c = match p.runs().next() {
904                    Some((_, f)) if f.color != 0 && !hc => opaque(f.color),
905                    _ => tcol,
906                };
907                let base = ptop + row.y + row.base;
908                if p.list == LS_BULLET {
909                    // bullets as shapes (fonts may lack ◦ and ▪): disc, ring, square by level
910                    let d = ((masc as f32 * 0.36) as i32).max(self.dp(4)) & !1;
911                    let cx = tx + l.indent - self.dp(9) - d / 2;
912                    let cy = base - (masc as f32 * 0.34) as i32;
913                    self.draw_color(c);
914                    match p.level % 3 {
915                        0 => self.draw_circle(cx << 8, cy << 8, (d << 8) / 2, 0),
916                        1 => self.draw_circle(cx << 8, cy << 8, (d << 8) / 2 - 128, self.hair().max(1) << 8),
917                        _ => self.draw_rect(cx - d / 2, cy - d / 2, d, d),
918                    }
919                } else {
920                    self.text.set_font(mf, ms);
921                    let mw = self.text.width(m);
922                    let x = tx + l.indent - self.dp(6) - mw;
923                    self.cmd_text(x, base.max(ptop + row.y + masc), m.as_bytes(), mf, ms, c, 0);
924                }
925            }
926            for row in &l.rows {
927                let ry = ptop + row.y;
928                if ry > ty + th || ry + row.h < ty {
929                    continue;
930                }
931                // the selection under the text (the paragraph break: a small block after the
932                // last row)
933                let (rs, re) = (TextPos::new(line, row.a), TextPos::new(line, row.b));
934                if sa < sb && sa <= re && sb >= rs {
935                    let (lo, hi) = (sa.max(rs), sb.min(re));
936                    let brk = sb > re && core::ptr::eq(row, l.rows.last().unwrap());
937                    if lo < hi || brk {
938                        let x0 = row.x(lo.byte);
939                        let x1 = row.x(hi.byte) + if brk { self.dp(5) } else { 0 };
940                        self.draw_color(sel_col);
941                        self.draw_rect(tx + x0, ry, (x1 - x0).max(1), row.h);
942                    }
943                }
944                for pc in &row.pieces {
945                    let x = tx + pc.x;
946                    // high contrast: the document's colours give way to the scheme's (a
947                    // highlight becomes a frame)
948                    let (pcolor, pbg) = if hc { (0, 0) } else { (pc.color, pc.bg) };
949                    if pbg != 0 {
950                        self.draw_color(opaque(pbg));
951                        self.draw_rect(x, ry, pc.w, row.h);
952                    } else if hc && pc.bg != 0 {
953                        self.draw_color(tcol);
954                        self.draw_frame(x, ry, pc.w, row.h, 0, hair.max(1));
955                    }
956                    if pc.tab {
957                        continue;
958                    }
959                    let col = if disabled {
960                        tcol
961                    } else if pcolor != 0 {
962                        opaque(pcolor)
963                    } else if pbg != 0 {
964                        // the theme's text colour may not show on the highlight
965                        let c = opaque(pbg);
966                        let luma = ((c >> 16 & 255) * 299 + (c >> 8 & 255) * 587 + (c & 255) * 114) / 1000;
967                        if luma > 140 { 0xFF1A1A1A } else { 0xFFF2F2F2 }
968                    } else if pc.link {
969                        lcol
970                    } else if p.style == PS_QUOTE {
971                        qcol
972                    } else {
973                        tcol
974                    };
975                    let base = ry + row.base;
976                    self.cmd_text(x, base, &p.text().as_bytes()[pc.a..pc.b], pc.font, pc.size, col, 0);
977                    let px = pc.size >> 8;
978                    let th_line = (px / 14).max(1);
979                    if pc.flags & RF_UNDER != 0 || pc.link {
980                        self.draw_color(col);
981                        self.draw_rect(x, base + (px / 9).max(1), pc.w, th_line);
982                    }
983                    if pc.flags & RF_STRIKE != 0 {
984                        self.draw_color(col);
985                        self.draw_rect(x, base - pc.asc * 3 / 10, pc.w, th_line);
986                    }
987                    if pc.link && links.len() < 64 {
988                        let t = String::from(&p.text()[pc.a..pc.b]);
989                        match links.last_mut() {
990                            Some((lr, lt)) if lr[1] == ry && lr[0] + lr[2] == x => {
991                                lr[2] += pc.w;
992                                lt.push_str(&t);
993                            }
994                            _ => links.push(([x, ry, pc.w, row.h], t)),
995                        }
996                    }
997                }
998            }
999        }
1000        // the caret
1001        let (crx, cry) = (tx + cxp, ty + crow_y - v.scroll);
1002        v.caret_rect = [crx, cry, hair.max(1), crow_h];
1003        if focused {
1004            let pe = self.ed_preedit();
1005            if !pe.text.is_empty() {
1006                // the composition in the font at the caret
1007                let l = &lays[caret.line];
1008                let row = &l.rows[row_of(&RView { doc: &*doc, lays: &lays, tops: &tops }, caret)];
1009                let pc = row.pieces.iter().find(|pc| pc.a <= caret.byte && caret.byte <= pc.b && !pc.tab);
1010                let font = pc.map_or((l.mfont.0, l.mfont.1), |pc| (pc.font, pc.size));
1011                let bg = if frame { self.color(if focused { T_INPUT_ACTIVE } else { T_CTRL }) } else { self.color(T_BG) };
1012                let a = (bg >> 24) as i32;
1013                let bg = crate::theme::color_mix(self.color(T_BG) | 0xFF00_0000, bg | 0xFF00_0000, a + (a >> 7));
1014                self.ed_preedit_draw(&pe, crx, cry, crow_h, cry + row.base, font, tcol, bg);
1015            } else if doc.selection().is_empty() && self.ed_caret_on(v.t0) && cry + crow_h > ty && cry < ty + th {
1016                self.draw_color(tcol);
1017                self.draw_rect(crx, cry, hair.max(1), crow_h);
1018            }
1019            self.ime_pos(crx, cry, crow_h);
1020        }
1021        self.ltr_end();
1022        // links: a hand over them, a record each
1023        for (lr, label) in &links {
1024            self.wp.ctx.last_rect = *lr;
1025            let before = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
1026            self.hit(id, hb & ((1 << HB_CURSOR_SHIFT) - 1) | CUR_HAND << HB_CURSOR_SHIFT);
1027            if let Some(h) = self.wp.ctx.hits[self.wp.ctx.hit_cur].get_mut(before) {
1028                h.continuation = true;
1029            }
1030            self.wp.ctx.last_state = 0;
1031            self.sem(K_LINK, 0, Some(label), 0);
1032        }
1033        self.clip_pop();
1034        // ---- the scroll bar
1035        if total > th {
1036            let mut pos = v.scroll;
1037            self.next_rect_px(r[0] + r[2] - sbw - hair, r[1] + hair, sbw, r[3] - hair * 2);
1038            if self.scrollbar(id.wrapping_add(1), &mut pos, total, th, true) & EV_ACT != 0 {
1039                v.scroll = pos.clamp(0, (total - th).max(0));
1040            }
1041        }
1042        // a finger may roll it (touch.rs)
1043        self.pan_view(id, hit0, super::touch::AX_V, [0, v.scroll], [0, (total - th).max(0)]);
1044        // ---- the record: the text, caret and anchor as offsets into it
1045        if v.label.0 != doc.uid() || v.label.1 != doc.revision() {
1046            v.label = (doc.uid(), doc.revision(), doc.text());
1047        }
1048        let (fa, fc) = (doc.flat(doc.selection().anchor), doc.flat(doc.selection().caret));
1049        self.wp.ctx.last_rect = r;
1050        self.wp.ctx.last_state = if focused { SS_FOCUSED as u32 } else { 0 } | if disabled { SS_DISABLED as u32 } else { 0 };
1051        let kind = (TB_MULTI | if ro { TB_READONLY } else { 0 }) >> 24;
1052        let label = core::mem::take(&mut v.label.2);
1053        {
1054            let s = self.sem(K_TEXTBOX, id, Some(&label), if focused { fc as i32 } else { 0 });
1055            s.max = if focused { fa as i32 } else { 0 };
1056            s.min = kind as i32;
1057        }
1058        v.label.2 = label;
1059        if focused {
1060            self.ed_hold(id, fa, fc);
1061            v.held = Some((fa, fc));
1062            out |= EV_FOCUS;
1063        }
1064        // ---- the link flyout
1065        if v.link_open {
1066            self.rt_link_flyout(id, doc, &mut v);
1067        }
1068        self.m.rich.views.insert(id, v);
1069        // views of editors no longer drawn go
1070        let frame_now = self.frame;
1071        if self.m.rich.views.len() > 8 {
1072            self.m.rich.views.retain(|_, v| frame_now.wrapping_sub(v.seen) < 600);
1073        }
1074        self.wp.ctx.last_rect = r;
1075        out
1076    }
1077
1078    // a key of the editor itself -> consumed
1079    fn rt_key(&mut self, id: u32, doc: &mut RichDoc, v: &mut View, k: u32, flags: u32) -> bool {
1080        let ro = flags & RT_READONLY != 0;
1081        let vk = k & 0xFFFF;
1082        let m = k >> 16;
1083        let altgr = m & MOD_ALTGR != 0 || m & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT;
1084        if altgr || m & MOD_ALT != 0 {
1085            return false;
1086        }
1087        let ctrl = m & MOD_CTRL != 0;
1088        let shift = m & MOD_SHIFT != 0;
1089        let sel = doc.selection();
1090        if ctrl {
1091            let edit = |doc: &mut RichDoc, f: &dyn Fn(&mut RichDoc) -> bool| {
1092                if !ro {
1093                    f(doc);
1094                }
1095                true
1096            };
1097            return match (vk, shift) {
1098                (0x42, false) => edit(doc, &|d| d.toggle(RF_BOLD)),
1099                (0x49, false) => edit(doc, &|d| d.toggle(RF_ITALIC)),
1100                (0x55, false) => edit(doc, &|d| d.toggle(RF_UNDER)),
1101                (0x58, true) => edit(doc, &|d| d.toggle(RF_STRIKE)),
1102                (VK_SPACE, false) => edit(doc, &|d| d.clear_format()),
1103                (0x4C, false) => edit(doc, &|d| d.set_align(RA_LEFT)),
1104                (0x45, false) => edit(doc, &|d| d.set_align(RA_CENTER)),
1105                (0x52, false) => edit(doc, &|d| d.set_align(RA_RIGHT)),
1106                (0x4A, false) => edit(doc, &|d| d.set_align(RA_JUSTIFY)),
1107                (0x38, true) | (0x4C, true) => edit(doc, &|d| d.toggle_list(LS_BULLET)),
1108                (0x37, true) => edit(doc, &|d| d.toggle_list(LS_NUMBER)),
1109                (0x31, false) => edit(doc, &|d| d.set_style(PS_H1)),
1110                (0x32, false) => edit(doc, &|d| d.set_style(PS_H2)),
1111                (0x33, false) => edit(doc, &|d| d.set_style(PS_H3)),
1112                (0x30, false) => edit(doc, &|d| d.set_style(PS_NORMAL)),
1113                (VK_OEM_6, false) | (VK_OEM_4, false) => {
1114                    let up = vk == VK_OEM_6;
1115                    let base = if doc.size == 0 { 110 } else { doc.size };
1116                    let cur = doc.current_fmt().size;
1117                    let cur = if cur == 0 { base } else { cur };
1118                    let next = if up { SIZES.iter().copied().find(|&s| s > cur).unwrap_or(cur) } else { SIZES.iter().rev().copied().find(|&s| s < cur).unwrap_or(cur) };
1119                    edit(doc, &|d| d.set_size(next))
1120                }
1121                (0x4B, false) => {
1122                                if !ro {
1123                        self.rt_link_open(id, doc, v);
1124                    }
1125                    true
1126                }
1127                (0x56, true) => {
1128                    if !ro {
1129                        self.rt_paste(doc, true);
1130                    }
1131                    true
1132                }
1133                (VK_RETURN, false) => {
1134                    // Ctrl+Enter on a link opens it
1135                    if let Some(url) = doc.link_at(sel.caret).or_else(|| if sel.caret.byte > 0 { doc.link_at(TextPos::new(sel.caret.line, sel.caret.byte - 1)) } else { None }).map(String::from) {
1136                        self.rt_link_activate(id, &url, flags);
1137                        return true;
1138                    }
1139                    false
1140                }
1141                _ => false,
1142            };
1143        }
1144        match vk {
1145            VK_TAB => {
1146                let para = doc.current_para();
1147                let in_list = para.list != LS_NONE || !sel.is_empty() && (sel.start().line != sel.end().line);
1148                if in_list || (shift && para.level > 0) {
1149                    if !ro {
1150                        doc.indent(if shift { -1 } else { 1 });
1151                    }
1152                    return true;
1153                }
1154                if flags & RT_TAB != 0 && !shift {
1155                    if !ro {
1156                        doc.insert_text_kind("\t", RK_TYPE);
1157                    }
1158                    return true;
1159                }
1160                false
1161            }
1162            VK_BACK if sel.is_empty() && sel.caret.byte == 0 && !ro => {
1163                let para = doc.current_para();
1164                if para.list != LS_NONE {
1165                    doc.set_list(LS_NONE);
1166                    return true;
1167                }
1168                if para.level > 0 {
1169                    doc.indent(-1);
1170                    return true;
1171                }
1172                false
1173            }
1174            VK_ESCAPE => {
1175                self.wp.ctx.focus = 0;
1176                self.repass();
1177                true
1178            }
1179            _ => false,
1180        }
1181    }
1182
1183    // the selection to the clipboard: text and HTML
1184    fn rt_copy(&mut self, doc: &RichDoc) {
1185        let sel = doc.selection();
1186        if sel.is_empty() {
1187            return;
1188        }
1189        let frag = doc.selected_paras();
1190        let text = if frag.len() > 1 { RichDoc::from_paras(frag.clone()).to_plain() } else { doc.selected_text() };
1191        let html = clip_html(&frag, &doc.font, doc.size);
1192        self.clip_set_many(&[(CLIP_TEXT, text.as_bytes()), (CLIP_HTML, html.as_bytes())]);
1193    }
1194
1195    // paste: HTML when there is some (and rich pastes are allowed), else the text
1196    fn rt_paste(&mut self, doc: &mut RichDoc, plain: bool) {
1197        if !plain {
1198            if let Some(h) = self.clip_get(CLIP_HTML) {
1199                let html = String::from_utf8_lossy(&h).into_owned();
1200                let d = RichDoc::from_html(&html);
1201                if d.paras().iter().any(|p| !p.is_empty()) || d.para_count() > 1 {
1202                    doc.insert_paras(d.paras());
1203                    return;
1204                }
1205            }
1206        }
1207        if let Some(t) = self.clip_get(CLIP_TEXT) {
1208            let s = String::from_utf8_lossy(&t).replace('\0', "");
1209            if !s.is_empty() {
1210                doc.insert_text_kind(&s, 0);
1211            }
1212        }
1213    }
1214
1215    fn rt_link_activate(&mut self, id: u32, url: &str, flags: u32) {
1216        self.m.rich.link = Some((id, String::from(url)));
1217        if flags & RT_OPEN_LINKS != 0 {
1218            self.open_url(url);
1219        }
1220        self.repass();
1221    }
1222
1223    /// a link the user activated in richtext `id` (Ctrl+click, a click when read-only,
1224    /// Ctrl+Enter), taken once
1225    pub fn richtext_link(&mut self, id: u32) -> Option<String> {
1226        match &self.m.rich.link {
1227            Some((i, _)) if *i == id => self.m.rich.link.take().map(|l| l.1),
1228            _ => None,
1229        }
1230    }
1231
1232    // open the link flyout of editor id (the link at the caret, else "https://")
1233    fn rt_link_open(&mut self, id: u32, doc: &RichDoc, v: &mut View) {
1234        let sel = doc.selection();
1235        let at = if sel.is_empty() { sel.caret } else { sel.start() };
1236        let cur = doc.link_at(at).or_else(|| if at.byte > 0 { doc.link_at(TextPos::new(at.line, at.byte - 1)) } else { None });
1237        v.link_text = String::from(cur.unwrap_or("https://"));
1238        v.link_open = true;
1239        self.popup_open(id.wrapping_add(2));
1240        self.wp.ctx.focus = id.wrapping_add(3);
1241        self.wp.ctx.focus_visible = false;
1242        self.repass();
1243    }
1244
1245    /// open the link flyout of richtext `id` (as Ctrl+K does): the url of the link at the
1246    /// caret or "https://"; OK sets it on the selection (without one: inserts it as linked
1247    /// text), "Entfernen" removes the link
1248    #[cfg_attr(jfgui_dev, track_caller)]
1249    pub fn richtext_link_dialog(&mut self, id: u32, doc: &RichDoc) {
1250        let _src = self.src_scope();
1251        let mut v = self.m.rich.views.remove(&id).unwrap_or_default();
1252        self.rt_link_open(id, doc, &mut v);
1253        self.m.rich.views.insert(id, v);
1254    }
1255
1256    fn rt_link_flyout(&mut self, id: u32, doc: &mut RichDoc, v: &mut View) {
1257        let pid = id.wrapping_add(2);
1258        // Esc in the url box closes the flyout (the box would only drop its focus)
1259        if self.wp.ctx.focus == id.wrapping_add(3) && self.ed_key_chord(VK_ESCAPE, 0) {
1260            v.link_open = false;
1261            self.popup_close();
1262            self.ed_focus_quiet(id);
1263            return;
1264        }
1265        self.wp.ctx.last_rect = v.caret_rect;
1266        if !self.popup_is_open(pid) {
1267            self.popup_open(pid);
1268        }
1269        let mut done = None;
1270        if self.flyout_begin(pid, 0) {
1271            self.text(TS_BODY, tr("Link-Adresse"));
1272            self.next_size(self.dp(280), 0);
1273            // Enter taken before the box (its EV_COMMIT also comes from a focus change)
1274            if self.wp.ctx.focus == id.wrapping_add(3) && self.ed_key_chord(VK_RETURN, 0) {
1275                done = Some(true);
1276            }
1277            self.textbox(id.wrapping_add(3), Some("https://"), &mut v.link_text, 2048);
1278            self.box_begin(0, BOX_H, 6);
1279            if self.accent_button(id.wrapping_add(4), tr("OK")) & EV_ACT != 0 {
1280                done = Some(true);
1281            }
1282            if self.button(id.wrapping_add(5), tr("Link entfernen")) & EV_ACT != 0 {
1283                done = Some(false);
1284            }
1285            self.box_end();
1286        } else if self.wp.ctx.focus != id.wrapping_add(3) {
1287            // closed (Esc, a click outside)
1288            v.link_open = false;
1289        }
1290        self.flyout_end();
1291        if let Some(set) = done {
1292            let url = if set { String::from(v.link_text.trim()) } else { String::new() };
1293            doc.set_link(&url);
1294            v.link_open = false;
1295            self.popup_close();
1296            self.ed_focus_quiet(id);
1297        }
1298    }
1299
1300    // ------------------------------------------------------------------ the toolbar
1301
1302    // a toolbar icon (cached so that its identity stays)
1303    fn rt_art(&mut self, art: &'static str, color: u32, accent: u32) -> Rc<Art> {
1304        let k = (art.as_ptr() as usize, color, accent);
1305        self.m.rich.icons.entry(k).or_insert_with(|| Rc::new(Art { art, color, accent })).clone()
1306    }
1307
1308    // a square tool button with an icon -> clicked
1309    fn rt_tool(&mut self, id: u32, art: &'static str, label: &str, checked: bool, enabled: bool, accent: u32) -> bool {
1310        let st = self.style;
1311        let mut flags = ST_SUBTLE | ST_ICON_ONLY;
1312        if checked {
1313            flags |= ST_CHECKED;
1314        }
1315        if !enabled {
1316            flags |= ST_DISABLED;
1317        }
1318        let s = self.dp(32);
1319        let r = self.place(s, s);
1320        let ev = self.behave(id, if enabled { HB_FOCUS } else { HB_DISABLED });
1321        let mut sp = Sp { flags, label: Some(label), ..Sp::default() };
1322        self.sp_placed(&mut sp, ev);
1323        st.button(self, &sp);
1324        let c = self.color(if enabled { T_TEXT } else { T_TEXT_DISABLED });
1325        let a = if accent != 0 { opaque(accent) } else { c };
1326        let art = self.rt_art(art, c, a);
1327        let is = self.dp(18);
1328        self.cmd_custom(r[0] + (r[2] - is) / 2, r[1] + (r[3] - is) / 2, is, is, Some(art), 0, 0);
1329        self.focus_ring_draw(ev);
1330        if checked {
1331            self.wp.ctx.last_state |= SS_CHECKED as u32;
1332        }
1333        self.sem(K_BUTTON, id, Some(label), 0);
1334        self.tooltip(label);
1335        ev & EV_ACT != 0
1336    }
1337
1338    // a drop-down button of the toolbar (opens popup pid) -> the popup is open
1339    fn rt_drop(&mut self, id: u32, pid: u32, label: &str, w: i32, tip: &str) -> bool {
1340        self.next_size(w, 0);
1341        self.next_flags(ST_SUBTLE);
1342        let ev = self.button(id, label);
1343        self.tooltip(tip);
1344        if ev & EV_ACT != 0 {
1345            self.popup_open(pid);
1346        }
1347        self.flyout_begin(pid, 0)
1348    }
1349
1350    /// ui_richtext_toolbar(id, editor, doc, RTB_*) -> EV_ACT when it changed doc (see the
1351    /// module header)
1352    #[cfg_attr(jfgui_dev, track_caller)]
1353    pub fn richtext_toolbar(&mut self, id: u32, editor: u32, doc: &mut RichDoc, groups: u32) -> u32 {
1354        let _src = self.src_scope();
1355        let rev = doc.revision();
1356        let fmt = doc.current_fmt();
1357        let para = doc.current_para().clone();
1358        let mut acted = false;
1359        let id = |k: u32| id.wrapping_add(k);
1360        self.box_begin(0, BOX_H, 2);
1361        let sep = |ui: &mut Ui| {
1362            ui.next_size(ui.dp(1), ui.dp(24));
1363            let r = ui.place(ui.dp(1), ui.dp(24));
1364            ui.draw_color(ui.color(T_DIVIDER));
1365            ui.draw_rect(r[0], r[1] + ui.dp(4), r[2].max(1), r[3] - ui.dp(8));
1366        };
1367        if groups & RTB_HISTORY != 0 {
1368            if self.rt_tool(id(0), ART_UNDO, tr("Rückgängig"), false, doc.can_undo(), 0) {
1369                doc.undo();
1370                acted = true;
1371            }
1372            if self.rt_tool(id(1), ART_REDO, tr("Wiederholen"), false, doc.can_redo(), 0) {
1373                doc.redo();
1374                acted = true;
1375            }
1376            sep(self);
1377        }
1378        if groups & RTB_STYLE != 0 {
1379            let names = [tr("Fließtext"), tr("Überschrift 1"), tr("Überschrift 2"), tr("Überschrift 3"), tr("Zitat")];
1380            if self.rt_drop(id(2), id(3), names[para.style.min(4) as usize], self.dp(118), tr("Formatvorlage")) {
1381                for (i, n) in names.iter().enumerate() {
1382                    if para.style == i as u8 {
1383                        self.next_flags(ST_CHECKED);
1384                    }
1385                    if self.menu_item(id(10 + i as u32), n) & EV_ACT != 0 {
1386                        doc.set_style(i as u8);
1387                        acted = true;
1388                    }
1389                }
1390            }
1391            self.flyout_end();
1392            let mut fams: Vec<String> = alloc::vec![String::new()];
1393            fams.extend(self.font_registered());
1394            if !fams.iter().any(|f| f.eq_ignore_ascii_case("monospace")) {
1395                fams.push(String::from("monospace"));
1396            }
1397            let fam_label = if fmt.font.is_empty() { String::from(tr("Standardschrift")) } else { fmt.font.clone() };
1398            if self.rt_drop(id(4), id(5), &fam_label, self.dp(130), tr("Schriftart")) {
1399                for (i, f) in fams.iter().enumerate().take(16) {
1400                    if *f == fmt.font {
1401                        self.next_flags(ST_CHECKED);
1402                    }
1403                    let l = if f.is_empty() { String::from(tr("Standardschrift")) } else { f.clone() };
1404                    if self.menu_item(id(60 + i as u32), &l) & EV_ACT != 0 {
1405                        doc.set_font(f);
1406                        acted = true;
1407                    }
1408                }
1409            }
1410            self.flyout_end();
1411            let size = if fmt.size == 0 { if doc.size == 0 { 110 } else { doc.size } } else { fmt.size };
1412            if self.rt_drop(id(6), id(7), &size_text(size), self.dp(52), tr("Schriftgröße")) {
1413                for (i, s) in SIZES.iter().enumerate() {
1414                    if *s == size {
1415                        self.next_flags(ST_CHECKED);
1416                    }
1417                    let l = size_text(*s);
1418                    if self.menu_item(id(80 + i as u32), &l) & EV_ACT != 0 {
1419                        doc.set_size(*s);
1420                        acted = true;
1421                    }
1422                }
1423            }
1424            self.flyout_end();
1425            sep(self);
1426        }
1427        if groups & RTB_CHARS != 0 {
1428            for (k, flag, art, label) in [
1429                (8, RF_BOLD, ART_BOLD, tr("Fett")),
1430                (9, RF_ITALIC, ART_ITALIC, tr("Kursiv")),
1431                (15, RF_UNDER, ART_UNDER, tr("Unterstrichen")),
1432                (16, RF_STRIKE, ART_STRIKE, tr("Durchgestrichen")),
1433            ] {
1434                if self.rt_tool(id(k), art, label, fmt.flags & flag != 0, true, 0) {
1435                    doc.toggle(flag);
1436                    acted = true;
1437                }
1438            }
1439            sep(self);
1440        }
1441        if groups & RTB_COLOR != 0 {
1442            // swatch ids: text colours id + 100 .., highlights id + 130 .., "none" id + 99 / 129
1443            for (k, pid, base, art, label, cur, is_bg) in [
1444                (17u32, 18u32, 100u32, ART_COLOR, tr("Textfarbe"), fmt.color, false),
1445                (19, 36, 130, ART_HILITE, tr("Hervorhebung"), fmt.bg, true),
1446            ] {
1447                let shown = if cur != 0 { cur } else if is_bg { 0xFFFFE36B } else { self.color(T_TEXT) };
1448                if self.rt_tool(id(k), art, label, false, true, shown) {
1449                    self.popup_open(id(pid));
1450                }
1451                if self.flyout_begin(id(pid), 0) {
1452                    let auto = if is_bg { tr("Keine") } else { tr("Automatisch") };
1453                    if self.menu_item(id(base - 1), auto) & EV_ACT != 0 {
1454                        if is_bg { doc.set_bg(0) } else { doc.set_color(0) };
1455                        acted = true;
1456                    }
1457                    let cols: &[u32] = if is_bg { &PALETTE[4..] } else { &PALETTE[..15] };
1458                    for (r, row) in cols.chunks(5).enumerate() {
1459                        self.box_begin(0, BOX_H, 4);
1460                        for (k, &c) in row.iter().enumerate() {
1461                            if self.swatch(id(base + (r * 5 + k) as u32), c, c == cur) & EV_ACT != 0 {
1462                                if is_bg { doc.set_bg(c) } else { doc.set_color(c) };
1463                                acted = true;
1464                                self.popup_close();
1465                            }
1466                        }
1467                        self.box_end();
1468                    }
1469                }
1470                self.flyout_end();
1471            }
1472            sep(self);
1473        }
1474        if groups & RTB_LINK != 0 {
1475            let on = !fmt.link.is_empty();
1476            if self.rt_tool(id(21), ART_LINK, tr("Link"), on, true, 0) {
1477                let mut v = self.m.rich.views.remove(&editor).unwrap_or_default();
1478                self.rt_link_open(editor, doc, &mut v);
1479                self.m.rich.views.insert(editor, v);
1480            }
1481            sep(self);
1482        }
1483        if groups & RTB_ALIGN != 0 {
1484            for (k, a, art, label) in [
1485                (22, RA_LEFT, ART_LEFT, tr("Linksbündig")),
1486                (23, RA_CENTER, ART_CENTER, tr("Zentriert")),
1487                (24, RA_RIGHT, ART_RIGHT, tr("Rechtsbündig")),
1488                (25, RA_JUSTIFY, ART_JUSTIFY, tr("Blocksatz")),
1489            ] {
1490                if self.rt_tool(id(k), art, label, para.align == a, true, 0) {
1491                    doc.set_align(a);
1492                    acted = true;
1493                }
1494            }
1495            sep(self);
1496        }
1497        if groups & RTB_LISTS != 0 {
1498            if self.rt_tool(id(26), ART_BULLETS, tr("Aufzählung"), para.list == LS_BULLET, true, 0) {
1499                doc.toggle_list(LS_BULLET);
1500                acted = true;
1501            }
1502            if self.rt_tool(id(27), ART_NUMBERS, tr("Nummerierung"), para.list == LS_NUMBER, true, 0) {
1503                doc.toggle_list(LS_NUMBER);
1504                acted = true;
1505            }
1506            if self.rt_tool(id(28), ART_OUTDENT, tr("Einzug verkleinern"), false, para.level > 0, 0) {
1507                doc.indent(-1);
1508                acted = true;
1509            }
1510            if self.rt_tool(id(29), ART_INDENT, tr("Einzug vergrößern"), false, para.level < LEVEL_MAX, 0) {
1511                doc.indent(1);
1512                acted = true;
1513            }
1514            sep(self);
1515        }
1516        if groups & RTB_CLEAR != 0 && self.rt_tool(id(30), ART_CLEAR, tr("Format löschen"), false, true, 0) {
1517            doc.clear_format();
1518            acted = true;
1519        }
1520        self.box_end();
1521        if acted {
1522            self.ed_focus_quiet(editor);
1523            self.repass();
1524        }
1525        if doc.revision() != rev { EV_ACT } else { 0 }
1526    }
1527}