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