Skip to main content

ui/
md_view.rs

1// md_view -- ui.markdown_view / ui.html_view: a read-only document view over an MdDoc
2// (md_doc.rs) for help pages, README-like content, release notes and chat output
3//
4//   ui.markdown_view(id, &mut MdViewState, text, MV_*) -> MdEv   Markdown; parsed again only
5//        when the text changes (the state keeps a hash of it)
6//   ui.html_view(id, &mut MdViewState, html, MV_*) -> MdEv        HTML (the rich-text reader)
7//   ui.doc_view(id, &mut MdViewState, MV_*) -> MdEv                the document set before
8//        (st.set_markdown / set_html / set_doc)
9//   ui.markdown_fit(&mut MdViewState, text, w_px, MV_*) -> DIP     how high a view w_px wide must be to
10//        show the whole text without scrolling: for a short text among other widgets (a question
11//        in a form); every block is laid out now; give next_size(-1, h) to markdown_view
12//   MdEv { ev, link, hover, field, submit, click }   ev: EV_FOCUS, EV_HOVER, EV_ACT (a link was
13//        activated); link: the activated link's target (a click on it, Enter on the focused
14//        one); hover: the target of the link under the pointer; field: a form field the user
15//        changed (typed, toggled, chose) or a button without a submit / reset action he pressed;
16//        submit: a form was sent (MdSubmit: the form, its action and method, the name / value
17//        pairs, the button); click: the element of an HTML document a click on the text hit (the
18//        inline element around the character under the pointer, else the block's, the document's
19//        root beside the blocks; none after a drag; Enter on a link: its element; a field's
20//        widget takes its own clicks: field, submit).  The view fills the place it gets (natural
21//        480 x 320 DIP; next_size(-1, -1) fills).
22//   MdViewState::new() / base (where relative image paths start, e.g. "app://help/") / label
23//        (the name for screen readers; "" = "Dokument") / doc() / selection() /
24//        set_selection(sel) / select_all() / selected_text() / selected_html() /
25//        scroll_to_block(i) / scroll_to_end() (the end, also while the document grows: chat
26//        output; scrolling to the end of a long document does the same until the user scrolls
27//        away) / first_visible() -> (block, px above the view) / focused_link()
28//   Changes of the document (HTML: its elements, md_dom.rs -- page scripts): st.doc_mut() ->
29//        &mut MdDoc; the view takes the changes every frame (st.update(): now): the blocks
30//        derived again (MdDoc::update), the layouts and heights of the blocks that stay kept,
31//        the view on the block it showed, the selection and the focused link with their blocks,
32//        the fields' values by their elements (what the user or the app gave a field stays;
33//        the others follow the document's values), a focused field's widget moved with it
34//   Images from the app: st.images = MI_LOCAL (default: VFS names and paths, other addresses
35//        show their alt text), MI_ASK (as MI_LOCAL; http:, https: and data: addresses are asked
36//        from the app) or MI_APP (every image is asked: nothing is read by the view -- pages
37//        from the network).  st.image_requests() -> the addresses the view waits for (each
38//        once; relative ones after base, the rest as written); st.set_image(addr, Some(bytes) |
39//        None) answers (decoded at once by the image decoders: SVG or img_core; None or bytes
40//        it cannot decode: the alt text) -- in a frame before the view, or the app asks for one
41//        (invalidate) when the bytes come outside.  Until then a frame with the alt text stands
42//        in (at the width / height the HTML gives, else the frame's size); the image's block is
43//        laid out again when its bytes come, the view stays where it is.
44//   Forms (HTML: md_doc.rs "Forms"): the fields are the library's widgets in the text flow --
45//        text boxes (password fields: dots, nothing to copy or read out), text areas, check
46//        boxes, radios, drop-down lists, buttons -- with the style's look, high contrast, text
47//        scale and mirroring.  Their values live in the state: field_text(i) / field_checked(i)
48//        / field_selected(i), set_field_text / set_field_checked / set_field_selected,
49//        form_data(form, button) (the pairs a browser sends), reset_form(form),
50//        focused_field(); a new document starts with its first values (a changed one keeps
51//        them, above).  Keys: Tab / Shift+Tab
52//        go through links and fields in reading order (a group of radios is one stop: the
53//        checked one; Up / Down and Left / Right -- as the radios lie in their paragraph --
54//        choose inside it), Space toggles, Enter in a text field, check
55//        box or radio sends its form (with its first submit button, none when that is
56//        disabled), Enter / Space on a button presses it, Esc in a text field returns to the
57//        view, PgUp / PgDn that a field does not take scroll the view.  The fields follow the
58//        window's mirroring (as every widget), not the direction of their paragraph.  A
59//        document without fields reads as before.
60//   Flags: MV_NO_FRAME (no card, a smaller padding), MV_OPEN_LINKS (an activated link is also
61//        opened with open_url: its scheme rules and the app's policy apply; never without the
62//        user's click or Enter), MV_RTL / MV_LTR (the document's direction; default: that of
63//        its first strong character, else the window's)
64//
65// Layout: the blocks in a column; block quotes indent with a bar, lists with their markers in
66// front (bullets by depth: disc, ring, square; numbers; task boxes), headings 2 / 1.5 / 1.25 /
67// 1 / 0.875 / 0.85 times the body text in bold (h1 and h2 underlined), code blocks in the
68// monospace font on a tinted box with the code editor's highlighting (Highlighter::for_name of
69// the fence's language, ui.code_theme colours), tables with column widths from their cells
70// (wrapped to fit the column), images at their size (DIP; wider than the column: scaled down;
71// SVGs through img_svg, sharp at every size), rules.  Boxes of CSS (MdDoc::boxes) at their
72// place among the indents: margins and padding (growing with the text), borders, a background
73// under their blocks; a margin CSS gives at a box's top or bottom takes the place of the gap
74// between blocks; on a background the text without a colour of its own takes one that reads on
75// it (fields there get the theme's background behind them); high contrast draws no
76// backgrounds and the borders in the text's colour; left and right stay where they are in a
77// right-to-left document.  Text wraps at text::segment's line
78// breaks (clusters inside a word too long).  A paragraph's direction comes from its first
79// strong character: Hebrew and Arabic run right to left (their runs shaped and reordered by
80// text_shape); indents and markers follow the document's direction.  Only the blocks in view
81// are laid out (cached per width, dpi, text scale and fonts) and drawn; scrolling is anchored at
82// the first visible block (vlist.rs Rows), so blocks laid out for the first time never move
83// what is shown.
84// Mouse: a drag selects (the view scrolls at its edges), Shift+click extends, a double click
85// selects a word; a click on a link activates it.  Keys (focused): Up / Down scroll by a line,
86// PgUp / PgDn and Space / Shift+Space by a view, Home / End to the ends, Tab / Shift+Tab
87// through the links and fields (the focused link is framed, a field gets the focus; scrolled
88// into view; past the last one the focus moves on), Enter activates the focused link, Esc
89// drops it, Ctrl+A selects all,
90// Ctrl+C / Ctrl+Insert copy the selection as text (list markers, cells by tabs) and HTML.
91// Images: names of the VFS ("app://", "data://" ...) and paths (relative ones after base) are
92// read on first view and kept in the state; http(s) addresses are never fetched by the view
93// (MI_LOCAL: the alt text shows in a frame instead; MI_ASK / MI_APP: the app fetches them).
94// Records: K_BOX id (label: the label, value: the scroll position in DIP); per visible block
95// K_HEADING (value: the level), K_TEXT (paragraphs, code, table cells), K_LISTITEM (the first
96// block of a list item, value: its number), K_CHECKBOX (a task's box, checked), K_LINK per
97// link, K_IMAGE (label: the alt text) per image; per visible field the widget's record
98// (K_TEXTBOX, K_CHECKBOX, K_RADIO, K_COMBO, K_BUTTON: label the field's name -- its <label>,
99// aria-label, title, placeholder -- or a button's caption; text boxes and lists take it from a
100// K_TEXT record without area along their top edge).  Ids: id (focus, keys), part_id(id, 1)
101// the scroll bar, md_field_id(id, i) the fields.
102
103use alloc::collections::{BTreeMap, BTreeSet};
104use alloc::format;
105use alloc::rc::Rc;
106use alloc::string::String;
107use alloc::vec::Vec;
108
109use gfx::Surface;
110use img_svg::Svg;
111use text::segment;
112use text::{FS_SYN_BOLD, FS_SYN_ITALIC};
113
114use crate::cmd::{Draw, DrawRef, Gfx};
115use crate::consts::*;
116use crate::ed_core::{word_range, TextPos, TextSel};
117use crate::highlight::{Highlighter, Token};
118use crate::i18n::tr;
119use crate::imgview::SvgDraw;
120use crate::keys::*;
121use crate::list::{anim_decay, anim_steps};
122use crate::md_doc::*;
123use crate::md_dom::MdNode;
124use crate::theme::color_mix;
125use crate::value::part_id;
126use crate::vlist::Rows;
127use crate::{Sp, Ui};
128
129/// markdown_view flags: no card around the document
130pub const MV_NO_FRAME: u32 = 1;
131/// an activated link is also opened (open_url)
132pub const MV_OPEN_LINKS: u32 = 2;
133/// the document runs right to left (indents and markers on the right)
134pub const MV_RTL: u32 = 4;
135/// the document runs left to right (also in a mirrored window)
136pub const MV_LTR: u32 = 8;
137
138/// MdViewState::images: VFS names and paths are read, other addresses show their alt text
139pub const MI_LOCAL: u8 = 0;
140/// as MI_LOCAL, but http:, https: and data: addresses are asked from the app
141pub const MI_ASK: u8 = 1;
142/// every image is asked from the app (the view reads no file)
143pub const MI_APP: u8 = 2;
144
145const NAT_W: i32 = 480;
146const NAT_H: i32 = 320;
147const PAD: i32 = 16;
148const PAD_BARE: i32 = 4;
149const PARA_GAP: i32 = 12;
150const TIGHT_GAP: i32 = 4;
151const LIST_W: i32 = 28;
152const QUOTE_W: i32 = 18;
153const CODE_PX: i32 = 12;
154const CODE_PY: i32 = 10;
155const CODE_PAD: i32 = 3;
156const CELL_PX: i32 = 10;
157const CELL_PY: i32 = 6;
158const RULE_H: i32 = 20;
159const LINE_DIP: i32 = 40;
160const WHEEL_DIP: i32 = 48;
161const IMG_BYTES: usize = 32 << 20;
162const HEAD_SCALE: [u32; 6] = [200, 150, 125, 100, 88, 85];
163const TABLE_SAMPLE: usize = 200;
164const LAYS_KEEP: usize = 400;
165const LABEL_MAX: usize = 4000;
166const FIELD_CHARS: u32 = 20; // a text field's width in characters (HTML's default size / cols)
167const FIELD_ROWS: u32 = 2; // a text area's lines (rows)
168const FIELD_GAP: i32 = 2; // room above and below a field in its row (DIP)
169const FIELD_CAP: u32 = 1 << 20; // bytes a field holds without maxlength
170
171/// what a document view reports (see the module header)
172#[derive(Clone, Debug, Default)]
173pub struct MdEv {
174    /// EV_FOCUS, EV_HOVER, EV_ACT (a link was activated)
175    pub ev: u32,
176    /// the target of the link activated this frame
177    pub link: Option<String>,
178    /// the target of the link under the pointer
179    pub hover: Option<String>,
180    /// a field the user changed this frame, or a button (MF_BUTTON) he pressed (index of
181    /// doc().fields())
182    pub field: Option<usize>,
183    /// a form sent this frame (Enter in a field, a submit button)
184    pub submit: Option<MdSubmit>,
185    /// the element a click hit this frame, or the link Enter activated (see the module header)
186    pub click: Option<MdNode>,
187}
188
189/// a form being sent: what a browser would send (MdViewState::form_data)
190#[derive(Clone, Debug, Default, PartialEq, Eq)]
191pub struct MdSubmit {
192    /// the form (index of doc().forms()), its action (as written) and method ("get" / "post")
193    pub form: usize,
194    pub action: String,
195    pub method: String,
196    /// the name / value pairs in document order
197    pub fields: Vec<(String, String)>,
198    /// the submit button that sent it (None: Enter in a form without one)
199    pub button: Option<usize>,
200}
201
202// an image of the document as loaded (Pending: asked from the app, its address)
203#[derive(Clone)]
204enum Img {
205    Unread,
206    Failed,
207    Pending(Rc<str>),
208    Bitmap(Rc<Surface>),
209    Vector(Rc<Svg>),
210}
211
212// an address the app supplies: asked, or its picture
213enum Req {
214    Asked,
215    Got(Img),
216}
217
218// the current value of a field: text (text fields, hidden ones), checked, the chosen option;
219// the field's kind and element, whether the user or the app changed it (dirty: changes of the
220// document keep it; else it follows the field's first value)
221#[derive(Clone, Default)]
222struct Val {
223    text: String,
224    on: bool,
225    sel: i32,
226    kind: u8,
227    node: MdNode,
228    dirty: bool,
229}
230
231// a stop of Tab: a link spot, a field
232#[derive(Clone, Copy, PartialEq, Eq)]
233enum Stop {
234    Link(usize),
235    Field(usize),
236}
237
238/// the widget id of field i of the view `id` (the drop-down lists' rows take ids of their own)
239pub fn md_field_id(id: u32, i: usize) -> u32 {
240    (id.wrapping_mul(0x9E37_79B1) & 0x0FFF_0000) | 0xE000_0000 | (i as u32 + 1) & 0xFFFF
241}
242
243// the field of view `id` whose widget id is fid (n fields)
244fn field_of(id: u32, fid: u32, n: usize) -> Option<usize> {
245    let k = (fid & 0xFFFF) as usize;
246    (fid & 0xFFFF_0000 == md_field_id(id, 0) & 0xFFFF_0000 && k >= 1 && k <= n).then(|| k - 1)
247}
248
249// a field's name for screen readers: its label, else its placeholder, else its name
250fn field_name(f: &MdField) -> &str {
251    if !f.label.is_empty() {
252        &f.label
253    } else if !f.placeholder.is_empty() {
254        &f.placeholder
255    } else {
256        &f.name
257    }
258}
259
260// a scheme at the start of an address ("http:", "data:"; not a drive letter)
261fn has_scheme(s: &str) -> bool {
262    match s.find(':') {
263        Some(i) if i >= 2 => s.as_bytes()[0].is_ascii_alphabetic() && s[..i].bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'-' | b'.')),
264        _ => false,
265    }
266}
267
268// where an image comes from: (a file to read, else an address the app supplies); (None, None):
269// nowhere (the alt text)
270fn img_at(src: &str, base: &str, mode: u8) -> (Option<String>, Option<String>) {
271    let src = src.trim();
272    let low = src.to_ascii_lowercase();
273    if src.is_empty() || low.starts_with("javascript:") {
274        return (None, None);
275    }
276    let remote = low.starts_with("http:") || low.starts_with("https:") || low.starts_with("data:");
277    let path = if src.contains("://") || src.starts_with('/') || base.is_empty() || (mode == MI_APP && has_scheme(src)) {
278        String::from(src)
279    } else {
280        let s = src.strip_prefix("./").unwrap_or(src);
281        if base.ends_with('/') { format!("{}{}", base, s) } else { format!("{}/{}", base, s) }
282    };
283    match mode {
284        MI_APP => (None, Some(path)),
285        MI_ASK if remote => (None, Some(String::from(src))),
286        _ if remote => (None, None),
287        _ => (Some(path), None),
288    }
289}
290
291// a picture from its bytes (an SVG by its name or its first character, else what img_core
292// decodes)
293fn img_decode(d: &[u8], name: &str) -> Img {
294    if d.len() > IMG_BYTES {
295        return Img::Failed;
296    }
297    let low = name.to_ascii_lowercase();
298    let head = core::str::from_utf8(&d[..d.len().min(256)]).unwrap_or("");
299    let svg = low.ends_with(".svg") || low.ends_with(".svgz") || head.trim_start_matches('\u{FEFF}').trim_start().starts_with('<');
300    if svg {
301        Svg::parse(d).map(|s| Img::Vector(Rc::new(s))).unwrap_or(Img::Failed)
302    } else {
303        img_core::io::decode(d).map(|s| Img::Bitmap(Rc::new(s))).unwrap_or(Img::Failed)
304    }
305}
306
307// a piece of a row: bytes a .. b of the block's text in one font and colour, an image or a
308// field (asc: its height above the baseline)
309#[derive(Clone)]
310struct Piece {
311    a: u32,
312    b: u32,
313    x: i32,
314    w: i32,
315    font: u32,
316    size: i32,
317    asc: i32,
318    style: u16,
319    color: u32,
320    bg: u32,
321    link: u32,
322    image: u32,
323    ih: i32,
324    syn: u8,
325    shaped: bool,
326    field: u32,
327}
328
329// a visual row: its bytes, top, height, baseline, pieces and caret stops (byte, x)
330struct LRow {
331    a: u32,
332    b: u32,
333    y: i32,
334    h: i32,
335    base: i32,
336    pieces: Vec<Piece>,
337    stops: Vec<(u32, i32)>,
338}
339
340impl LRow {
341    fn hit(&self, x: i32) -> u32 {
342        self.stops.iter().min_by_key(|s| (s.1 - x).abs()).map_or(self.a, |s| s.0)
343    }
344}
345
346// laid out text: rows, their height, the widest row
347#[derive(Default)]
348struct Flow {
349    rows: Vec<LRow>,
350    h: i32,
351    w: i32,
352}
353
354// a text area of a block: x (from the column's start), y (from the content's top), width
355struct FBox {
356    x: i32,
357    y: i32,
358    w: i32,
359    flow: Flow,
360}
361
362// the layout of a block
363struct BLay {
364    // the whole height (gap included), the gap above, the content's height
365    h: i32,
366    gap: i32,
367    ch: i32,
368    // the content column: x from the column's start, width
369    x: i32,
370    w: i32,
371    // the text areas (one; table rows: one per cell)
372    boxes: Vec<FBox>,
373    // quote bars: x from the column's start
374    bars: Vec<i32>,
375    // the boxes of CSS around it (outermost first), the x the list marker's gutter ends at
376    css: Vec<CssBox>,
377    mx: i32,
378}
379
380// a box of CSS at a block: the box (an index of MdDoc::boxes), its border's x and width from
381// the column's start, its y0 .. y1 from the block's top, its top / bottom border at this block
382struct CssBox {
383    b: u32,
384    x: i32,
385    w: i32,
386    y0: i32,
387    y1: i32,
388    first: bool,
389    last: bool,
390}
391
392// the faces: [monospace][bold | italic << 1], the body text size (24.8)
393#[derive(Clone, Copy, Default, PartialEq)]
394struct Faces {
395    f: [[u32; 4]; 2],
396    size: i32,
397}
398
399impl Faces {
400    fn get(&self, mono: bool, bold: bool, italic: bool) -> u32 {
401        self.f[mono as usize][bold as usize | (italic as usize) << 1]
402    }
403}
404
405// a run of text with its look (layout input)
406#[derive(Clone)]
407struct Seg {
408    a: usize,
409    b: usize,
410    font: u32,
411    size: i32,
412    asc: i32,
413    desc: i32,
414    lh: i32,
415    style: u16,
416    color: u32,
417    bg: u32,
418    link: u32,
419    image: u32,
420    iw: i32,
421    ih: i32,
422    syn: u8,
423    shaped: bool,
424    // room at both ends (inline code's box)
425    pad: i32,
426    // a field (index + 1): iw x ih, asc above the baseline, desc below it (its row's room
427    // included)
428    field: u32,
429}
430
431// the layout cache of a view
432#[derive(Default)]
433struct Cache {
434    key: (i32, i32, u32, bool, Faces),
435    lays: BTreeMap<usize, Rc<BLay>>,
436    cols: BTreeMap<u32, Rc<Vec<i32>>>,
437    rows_of: BTreeMap<u32, Vec<usize>>,
438    imgs: Vec<Img>,
439    scaled: BTreeMap<(usize, i32, i32), Rc<Surface>>,
440    svgs: BTreeMap<(usize, u32), Rc<SvgDraw>>,
441    // where the images come from (MI_*), the addresses the app supplies, the ones it has not
442    // taken yet (image_requests)
443    mode: u8,
444    req: BTreeMap<String, Req>,
445    queue: Vec<String>,
446}
447
448/// the state of a document view (see the module header)
449pub struct MdViewState {
450    doc: MdDoc,
451    // the source: kind (1 Markdown, 2 HTML, 3 set_doc), hash, length
452    src: (u8, u64, usize),
453    /// where relative image paths start ("app://help/"; "" = as given)
454    pub base: String,
455    /// the name for screen readers ("" = "Dokument")
456    pub label: String,
457    /// where the images come from: MI_LOCAL (default), MI_ASK, MI_APP (see the module header)
458    pub images: u8,
459    // the block of every image
460    img_block: Vec<usize>,
461    // the fields' values, the Tab stops (links and fields in reading order), the first radio of
462    // every radio's group (+ 1; 0: no group), the field with the focus (last pass)
463    vals: Vec<Val>,
464    stops: Vec<Stop>,
465    group: Vec<usize>,
466    ffocus: Option<usize>,
467    rows: Rows,
468    sel: TextSel,
469    link: Option<usize>,
470    lc: Cache,
471    // the rows were sized for this document
472    sized: bool,
473    // the document's direction by its first strong character (None: it has none)
474    auto_rtl: Option<bool>,
475    // the view stays at the end (End, scroll_to_end, scrolled to the end of a long document):
476    // a growing document (chat output) keeps showing its end
477    follow: bool,
478    drag: u8,
479    dragged: bool,
480    word: (TextPos, TextPos),
481    press_link: Option<usize>,
482    pending: i32,
483    t0: i32,
484    // a block range to show: block, y0, y1 (px from the block's top)
485    reveal: Option<(usize, i32, i32)>,
486    // the pointer's y and the bar's position when the scroll bar last moved the view
487    sb_y: i32,
488    sb_pos: i32,
489    // after changes of the document: the focused field's old and new index (the widget's id
490    // moves with it), blocks without a height estimate
491    refocus: Option<(usize, Option<usize>)>,
492    reguess: bool,
493}
494
495impl Default for MdViewState {
496    fn default() -> MdViewState {
497        MdViewState::new()
498    }
499}
500
501// a quick hash of a text (no cryptography: tells a changed source apart)
502fn text_hash(s: &str) -> u64 {
503    let b = s.as_bytes();
504    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
505    let mut it = b.chunks_exact(8);
506    for c in &mut it {
507        let v = u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]);
508        h = (h ^ v).wrapping_mul(0x0000_0100_0000_01B3).rotate_left(29);
509    }
510    for &x in it.remainder() {
511        h = (h ^ x as u64).wrapping_mul(0x0000_0100_0000_01B3);
512    }
513    h ^ b.len() as u64
514}
515
516// a right-to-left character (Hebrew, Arabic, Syriac, Thaana, N'Ko, their presentation forms)
517fn rtl_char(c: char) -> bool {
518    matches!(c as u32, 0x0590..=0x08FF | 0xFB1D..=0xFDFF | 0xFE70..=0xFEFF | 0x10800..=0x10FFF | 0x1E800..=0x1EFFF)
519}
520
521// the direction of a text by its first strong character (None: none)
522fn text_rtl(s: &str) -> Option<bool> {
523    s.chars().find(|c| c.is_alphabetic()).map(rtl_char)
524}
525
526fn opaque(c: u32) -> u32 {
527    if c >> 24 == 0 { c | 0xFF00_0000 } else { c }
528}
529
530// the text colours that read on a background of CSS (text, secondary text, links): dark on a
531// light one, light on a dark one
532fn on_bg(bg: u32) -> (u32, u32, u32) {
533    let (r, g, b) = ((bg >> 16) & 255, (bg >> 8) & 255, bg & 255);
534    if r * 2126 + g * 7152 + b * 722 > 255 * 5500 {
535        (0xFF1B_1B1B, 0xFF5C_5C5C, 0xFF0A_55B0)
536    } else {
537        (0xFFF2_F2F2, 0xFFBE_BEBE, 0xFF8A_B4F8)
538    }
539}
540
541impl MdViewState {
542    /// an empty document
543    pub fn new() -> MdViewState {
544        MdViewState {
545            doc: MdDoc::default(),
546            src: (0, 0, 0),
547            base: String::new(),
548            label: String::new(),
549            images: MI_LOCAL,
550            img_block: Vec::new(),
551            vals: Vec::new(),
552            stops: Vec::new(),
553            group: Vec::new(),
554            ffocus: None,
555            rows: Rows::default(),
556            sel: TextSel::default(),
557            link: None,
558            lc: Cache::default(),
559            sized: false,
560            auto_rtl: None,
561            follow: false,
562            drag: 0,
563            dragged: false,
564            word: (TextPos::default(), TextPos::default()),
565            press_link: None,
566            pending: 0,
567            t0: 0,
568            reveal: None,
569            sb_y: i32::MIN,
570            sb_pos: 0,
571            refocus: None,
572            reguess: false,
573        }
574    }
575
576    /// the document shown
577    pub fn doc(&self) -> &MdDoc {
578        &self.doc
579    }
580
581    /// the document to change (an HTML document's elements, md_dom.rs): the view takes the
582    /// changes at its next frame (or update()) and keeps its place, selection, focus and the
583    /// values the user gave the fields that stay
584    pub fn doc_mut(&mut self) -> &mut MdDoc {
585        &mut self.doc
586    }
587
588    /// take the document's changes now (MdDoc::update; the view does it every frame) -> there
589    /// were changes
590    pub fn update(&mut self) -> bool {
591        self.sync()
592    }
593
594    /// show Markdown (parsed now unless it is the text shown already)
595    pub fn set_markdown(&mut self, s: &str) {
596        let key = (1, text_hash(s), s.len());
597        if key != self.src {
598            self.src = key;
599            self.set_doc_inner(MdDoc::markdown(s));
600        }
601    }
602
603    /// show HTML (parsed now unless it is the text shown already)
604    pub fn set_html(&mut self, s: &str) {
605        let key = (2, text_hash(s), s.len());
606        if key != self.src {
607            self.src = key;
608            self.set_doc_inner(MdDoc::html(s));
609        }
610    }
611
612    /// show a document
613    pub fn set_doc(&mut self, doc: MdDoc) {
614        self.src = (3, 0, 0);
615        self.set_doc_inner(doc);
616    }
617
618    // a new document: the place in it is kept (at the end it stays at the end, as for chat
619    // output), the selection and the layouts go
620    fn set_doc_inner(&mut self, doc: MdDoc) {
621        self.doc = doc;
622        self.doc.update();
623        self.doc.take_change();
624        self.refocus = None;
625        self.sel = TextSel::default();
626        self.link = None;
627        self.press_link = None;
628        self.drag = 0;
629        self.reveal = None;
630        self.lc.lays.clear();
631        self.lc.cols.clear();
632        self.lc.scaled.clear();
633        self.lc.svgs.clear();
634        self.lc.imgs = alloc::vec![Img::Unread; self.doc.images().len()];
635        self.blocks_index();
636        // the app's images: the ones this document uses stay (asked or supplied)
637        if !self.lc.req.is_empty() {
638            let used: BTreeSet<String> = self.doc.images().iter().filter_map(|m| img_at(&m.src, &self.base, self.lc.mode).1).collect();
639            self.lc.req.retain(|a, _| used.contains(a));
640            self.lc.queue.retain(|a| used.contains(a));
641        }
642        self.fields_init(&[]);
643        self.ffocus = None;
644        self.sized = false;
645        self.auto_rtl = self.doc.blocks().iter().filter(|b| b.kind != MB_CODE).find_map(|b| text_rtl(&b.text));
646    }
647
648    // the rows of every table, the block of every image
649    fn blocks_index(&mut self) {
650        self.lc.rows_of.clear();
651        self.img_block = alloc::vec![0; self.doc.images().len()];
652        for (k, b) in self.doc.blocks().iter().enumerate() {
653            if b.kind == MB_ROW {
654                self.lc.rows_of.entry(b.table).or_default().push(k);
655            }
656            for s in b.spans.iter().filter(|s| s.image != 0) {
657                if let Some(x) = self.img_block.get_mut(s.image as usize - 1) {
658                    *x = k;
659                }
660            }
661        }
662    }
663
664    // the fields' values (first values; a field of `old` with the same element and kind that the
665    // user or the app changed keeps its value), the radios' groups (form and name), the Tab stops
666    fn fields_init(&mut self, old: &[Val]) {
667        let by: BTreeMap<MdNode, &Val> = old.iter().filter(|v| !v.node.is_none()).map(|v| (v.node, v)).collect();
668        self.vals = self
669            .doc
670            .fields()
671            .iter()
672            .map(|f| match by.get(&f.node) {
673                Some(v) if v.dirty && v.kind == f.kind => {
674                    let mut v = (*v).clone();
675                    if v.sel >= f.options.len() as i32 {
676                        v.sel = f.selected;
677                    }
678                    v
679                }
680                _ => Val {
681                    text: if f.kind == MF_CHECKBOX || f.kind == MF_RADIO { String::new() } else { f.value.clone() },
682                    on: f.checked,
683                    sel: f.selected,
684                    kind: f.kind,
685                    node: f.node,
686                    dirty: false,
687                },
688            })
689            .collect();
690        let mut first: BTreeMap<(u32, &str), usize> = BTreeMap::new();
691        self.group = alloc::vec![0; self.doc.fields().len()];
692        for (i, f) in self.doc.fields().iter().enumerate() {
693            if f.kind == MF_RADIO && !f.name.is_empty() {
694                self.group[i] = *first.entry((f.form, f.name.as_str())).or_insert(i) + 1;
695            }
696        }
697        // (one checked radio per group: one the user chose, else the last one, as browsers
698        // parse it)
699        let mut checked: BTreeMap<usize, usize> = BTreeMap::new();
700        for i in 0..self.vals.len() {
701            if self.vals[i].on && self.group[i] != 0 {
702                match checked.get(&self.group[i]).copied() {
703                    Some(p) if self.vals[p].dirty && !self.vals[i].dirty => self.vals[i].on = false,
704                    Some(p) => {
705                        self.vals[p].on = false;
706                        checked.insert(self.group[i], i);
707                    }
708                    None => {
709                        checked.insert(self.group[i], i);
710                    }
711                }
712            }
713        }
714        let (links, fields) = (self.doc.link_spots(), self.doc.field_spots());
715        self.stops = Vec::with_capacity(links.len() + fields.len());
716        let (mut a, mut b) = (0, 0);
717        while a < links.len() || b < fields.len() {
718            let take_link = b >= fields.len() || (a < links.len() && (links[a].block, links[a].a) <= (fields[b].block, fields[b].a));
719            if take_link {
720                self.stops.push(Stop::Link(a));
721                a += 1;
722            } else {
723                self.stops.push(Stop::Field(fields[b].field as usize - 1));
724                b += 1;
725            }
726        }
727    }
728
729    // the document's changes (MdDoc::update): the blocks that stay keep their layouts and
730    // heights, the view stays on the block it shows (scrolling is anchored there), the selection
731    // and the focused link move with their blocks, the fields keep the values the user gave them
732    // (by their elements) and the focus -> there were changes
733    fn sync(&mut self) -> bool {
734        self.doc.update();
735        let Some(ch) = self.doc.take_change() else { return false };
736        let n = self.doc.blocks().len();
737        // an old block -> the new one (inside the changed range: clamped into it)
738        let range = ch.range;
739        let map = |k: usize| -> (usize, bool) {
740            match range {
741                Some((a, o, nn)) if k >= a + o => (k - o + nn, false),
742                Some((a, o, nn)) if k >= a => (a + (k - a).min(nn.saturating_sub(1)), o != nn),
743                _ => (k, false),
744            }
745        };
746        if let Some((a, o, nn)) = range {
747            // heights: the new blocks get estimates (reguess), the anchor stays on its block
748            let (top, off) = (self.rows.top, self.rows.off);
749            if o != nn {
750                self.rows.remove(a, o);
751                self.rows.insert(a, nn);
752            } else {
753                for k in a..a + o {
754                    self.rows.forget(k);
755                }
756            }
757            self.rows.top = map(top).0.min(n.saturating_sub(1));
758            self.rows.off = off;
759            let lays = core::mem::take(&mut self.lc.lays);
760            for (k, l) in lays {
761                if k < a {
762                    self.lc.lays.insert(k, l);
763                } else if k >= a + o {
764                    self.lc.lays.insert(k - o + nn, l);
765                }
766            }
767            // (the blocks around the range: the boxes of their elements may end elsewhere now)
768            self.lc.lays.remove(&a.wrapping_sub(1));
769            self.lc.lays.remove(&(a + nn));
770            self.reguess = true;
771        }
772        for &k in &ch.redo {
773            // (and its neighbours: a box's margin there decides their gaps)
774            self.lc.lays.remove(&k);
775            self.lc.lays.remove(&k.wrapping_sub(1));
776            self.lc.lays.remove(&(k + 1));
777        }
778        if ch.full {
779            // the pictures by address, the tables' columns again, the fields by element
780            let mut pics: BTreeMap<&str, Img> = BTreeMap::new();
781            for (m, img) in ch.images.iter().zip(self.lc.imgs.iter()) {
782                pics.entry(m.src.as_str()).or_insert_with(|| img.clone());
783            }
784            self.lc.imgs = self.doc.images().iter().map(|m| pics.get(m.src.as_str()).cloned().unwrap_or(Img::Unread)).collect();
785            self.lc.scaled.clear();
786            self.lc.svgs.clear();
787            self.lc.cols.clear();
788            let rows: Vec<usize> = self.lc.lays.keys().copied().filter(|&k| self.doc.blocks().get(k).is_some_and(|b| b.kind == MB_ROW)).collect();
789            for k in rows {
790                self.lc.lays.remove(&k);
791            }
792            self.blocks_index();
793            let old = core::mem::take(&mut self.vals);
794            let focus = self.ffocus.and_then(|i| old.get(i)).map(|v| v.node);
795            self.fields_init(&old);
796            if let Some(i) = self.ffocus {
797                let j = focus.and_then(|nd| self.doc.field_of(nd));
798                if j != Some(i) {
799                    self.refocus = Some((i, j));
800                    self.ffocus = j;
801                }
802            }
803            self.link = self.link.and_then(|k| {
804                let s = ch.spots.get(k)?;
805                let (b, moved) = map(s.block);
806                if moved {
807                    return None;
808                }
809                self.doc.link_spots().iter().position(|t| t.block == b && t.a == s.a && t.link != 0)
810            });
811            self.press_link = None;
812        }
813        // the selection, a word being dragged, a range to show
814        let mp = |p: TextPos| {
815            let (line, moved) = map(p.line);
816            TextPos::new(line, if moved { 0 } else { p.byte })
817        };
818        self.sel = TextSel::new(self.clamp(mp(self.sel.anchor)), self.clamp(mp(self.sel.caret)));
819        self.word = (self.clamp(mp(self.word.0)), self.clamp(mp(self.word.1)));
820        self.reveal = self.reveal.map(|(b, y0, y1)| (map(b).0, y0, y1));
821        self.auto_rtl = self.doc.blocks().iter().filter(|b| b.kind != MB_CODE).find_map(|b| text_rtl(&b.text));
822        true
823    }
824
825    /// the selection (TextPos: block, byte of its text)
826    pub fn selection(&self) -> TextSel {
827        self.sel
828    }
829
830    /// select a range (clamped to the document)
831    pub fn set_selection(&mut self, sel: TextSel) {
832        self.sel = TextSel::new(self.clamp(sel.anchor), self.clamp(sel.caret));
833    }
834
835    /// select the whole document
836    pub fn select_all(&mut self) {
837        self.sel = TextSel::new(TextPos::default(), self.doc.end());
838    }
839
840    /// the selection as text (list markers, cells by tabs, images as their alt texts)
841    pub fn selected_text(&self) -> String {
842        if self.sel.is_empty() {
843            return String::new();
844        }
845        self.doc.plain_range(self.sel.start(), self.sel.end())
846    }
847
848    /// the selection as HTML
849    pub fn selected_html(&self) -> String {
850        if self.sel.is_empty() {
851            return String::new();
852        }
853        self.doc.html_range(self.sel.start(), self.sel.end())
854    }
855
856    /// show block i at the top
857    pub fn scroll_to_block(&mut self, i: usize) {
858        self.rows.top = i.min(self.doc.blocks().len().saturating_sub(1));
859        self.rows.off = 0;
860        self.pending = 0;
861        self.reveal = None;
862        self.follow = false;
863    }
864
865    /// show the end, also while the document grows (until the user scrolls away)
866    pub fn scroll_to_end(&mut self) {
867        self.pending = 0;
868        self.reveal = None;
869        self.follow = true;
870    }
871
872    /// the first visible block and how many px of it are above the view
873    pub fn first_visible(&self) -> (usize, i32) {
874        (self.rows.top, self.rows.off)
875    }
876
877    /// the link focused with Tab (an index of doc().link_spots())
878    pub fn focused_link(&self) -> Option<usize> {
879        self.link
880    }
881
882    // ------------------------------------------------------------------ images from the app
883
884    /// the image addresses the view waits for since the last call (MI_ASK / MI_APP; each one
885    /// once): the app fetches them and answers with set_image
886    pub fn image_requests(&mut self) -> Vec<String> {
887        core::mem::take(&mut self.lc.queue)
888    }
889
890    /// the bytes of an image address (None: it could not be had): decoded now, shown from the
891    /// next frame on (the alt text when they are no picture) -> an image of the document waited
892    /// for it.  An address not asked yet is kept for the images that need it later.
893    pub fn set_image(&mut self, addr: &str, data: Option<&[u8]>) -> bool {
894        let img = data.map_or(Img::Failed, |d| img_decode(d, addr));
895        self.lc.req.insert(String::from(addr), Req::Got(img.clone()));
896        self.lc.queue.retain(|a| a != addr);
897        let mut hit = false;
898        for i in 0..self.lc.imgs.len() {
899            if matches!(&self.lc.imgs[i], Img::Pending(a) if &**a == addr) {
900                self.lc.imgs[i] = img.clone();
901                self.lc.scaled.retain(|k, _| k.0 != i);
902                self.lc.svgs.retain(|k, _| k.0 != i);
903                // its block is laid out again (a table row: the whole table, its columns may
904                // change)
905                let k = self.img_block.get(i).copied().unwrap_or(0);
906                match self.doc.blocks().get(k) {
907                    Some(b) if b.kind == MB_ROW => {
908                        self.lc.cols.remove(&b.table);
909                        for r in self.lc.rows_of.get(&b.table).into_iter().flatten() {
910                            self.lc.lays.remove(r);
911                        }
912                    }
913                    _ => {
914                        self.lc.lays.remove(&k);
915                    }
916                }
917                hit = true;
918            }
919        }
920        hit
921    }
922
923    // ------------------------------------------------------------------ forms
924
925    /// field i's text (text fields, text areas, hidden fields; "" for the other kinds)
926    pub fn field_text(&self, i: usize) -> &str {
927        self.vals.get(i).map_or("", |v| v.text.as_str())
928    }
929
930    /// field i is checked (check boxes, radios)
931    pub fn field_checked(&self, i: usize) -> bool {
932        self.vals.get(i).is_some_and(|v| v.on)
933    }
934
935    /// the option chosen in field i (drop-down lists; -1: none)
936    pub fn field_selected(&self, i: usize) -> i32 {
937        self.vals.get(i).map_or(-1, |v| v.sel)
938    }
939
940    /// set field i's text (text fields, text areas, hidden fields; changes of the document keep
941    /// it, as the user's input)
942    pub fn set_field_text(&mut self, i: usize, s: &str) {
943        if let Some(v) = self.vals.get_mut(i) {
944            v.text = String::from(s);
945            v.dirty = true;
946        }
947    }
948
949    /// check field i or not (a radio: the others of its group go off)
950    pub fn set_field_checked(&mut self, i: usize, on: bool) {
951        if i >= self.vals.len() {
952            return;
953        }
954        let g = self.group[i];
955        if on && g != 0 {
956            for (v, &h) in self.vals.iter_mut().zip(self.group.iter()) {
957                if h == g {
958                    v.on = false;
959                    v.dirty = true;
960                }
961            }
962        }
963        self.vals[i].on = on;
964        self.vals[i].dirty = true;
965    }
966
967    /// choose option k of field i (a drop-down list; -1: none)
968    pub fn set_field_selected(&mut self, i: usize, k: i32) {
969        let n = self.doc.fields().get(i).map_or(0, |f| f.options.len()) as i32;
970        if let Some(v) = self.vals.get_mut(i) {
971            v.sel = if k >= 0 && k < n { k } else { -1 };
972            v.dirty = true;
973        }
974    }
975
976    /// the name / value pairs form `form` sends with the submit button `button` (as browsers
977    /// build them: fields with a name that are not disabled; check boxes and radios when
978    /// checked; a list's chosen value; of the buttons only `button`)
979    pub fn form_data(&self, form: usize, button: Option<usize>) -> Vec<(String, String)> {
980        let mut out = Vec::new();
981        for (i, f) in self.doc.fields().iter().enumerate() {
982            if f.form as usize != form + 1 || f.disabled || f.name.is_empty() {
983                continue;
984            }
985            let v = &self.vals[i];
986            let value = match f.kind {
987                MF_CHECKBOX | MF_RADIO if v.on => f.value.clone(),
988                MF_SELECT if v.sel >= 0 => f.options.get(v.sel as usize).map_or(String::new(), |o| o.value.clone()),
989                MF_SUBMIT if button == Some(i) => f.value.clone(),
990                MF_TEXT | MF_PASSWORD | MF_TEXTAREA | MF_HIDDEN => v.text.clone(),
991                _ => continue,
992            };
993            out.push((f.name.clone(), value));
994        }
995        out
996    }
997
998    /// the fields of form `form` get their first values back
999    pub fn reset_form(&mut self, form: usize) {
1000        for (i, f) in self.doc.fields().iter().enumerate() {
1001            if f.form as usize == form + 1 {
1002                let v = &mut self.vals[i];
1003                if f.kind != MF_CHECKBOX && f.kind != MF_RADIO {
1004                    v.text = f.value.clone();
1005                }
1006                v.on = f.checked;
1007                v.sel = f.selected;
1008                v.dirty = false;
1009            }
1010        }
1011    }
1012
1013    /// the field with the keyboard focus (an index of doc().fields())
1014    pub fn focused_field(&self) -> Option<usize> {
1015        self.ffocus
1016    }
1017
1018    // the form field i sends with Enter: its form with the first submit button (none when
1019    // that is disabled; a field outside a form sends nothing)
1020    fn implicit(&self, i: usize) -> Option<MdSubmit> {
1021        let f = self.doc.fields().get(i)?;
1022        let form = (f.form as usize).checked_sub(1)?;
1023        let button = self.doc.fields().iter().position(|b| b.kind == MF_SUBMIT && b.form == f.form);
1024        if button.is_some_and(|b| self.doc.fields()[b].disabled) {
1025            return None;
1026        }
1027        Some(self.submit(form, button))
1028    }
1029
1030    // form `form` sent with `button`
1031    fn submit(&self, form: usize, button: Option<usize>) -> MdSubmit {
1032        let fm = self.doc.forms().get(form);
1033        MdSubmit {
1034            form,
1035            action: fm.map_or(String::new(), |m| m.action.clone()),
1036            method: fm.map_or(String::from("get"), |m| m.method.clone()),
1037            fields: self.form_data(form, button),
1038            button,
1039        }
1040    }
1041
1042    // the next / previous Tab stop after stop `cur` (None: before the first) -> None: past the
1043    // ends.  A field is one when it is not disabled; of a group of radios only the checked one
1044    // (else its first).
1045    fn step(&self, cur: Option<usize>, back: bool) -> Option<Stop> {
1046        let n = self.stops.len();
1047        let checked: BTreeMap<usize, usize> = (0..self.vals.len()).filter(|&j| self.group[j] != 0 && self.vals[j].on).map(|j| (self.group[j], j)).collect();
1048        let stop = |i: usize| {
1049            let Some(f) = self.doc.fields().get(i) else { return false };
1050            let g = self.group[i];
1051            !f.disabled && (g == 0 || checked.get(&g).copied().unwrap_or(g - 1) == i)
1052        };
1053        let mut k = cur;
1054        loop {
1055            k = match (k, back) {
1056                (None, false) => (n > 0).then_some(0),
1057                (None, true) => None,
1058                (Some(i), false) => (i + 1 < n).then_some(i + 1),
1059                (Some(i), true) => i.checked_sub(1),
1060            };
1061            let s = self.stops[k?];
1062            match s {
1063                Stop::Field(f) if !stop(f) => continue,
1064                _ => return Some(s),
1065            }
1066        }
1067    }
1068
1069    // the radio before / after radio i in its group (wrapping)
1070    fn radio_step(&self, i: usize, back: bool) -> Option<usize> {
1071        let g = *self.group.get(i)?;
1072        if g == 0 {
1073            return None;
1074        }
1075        let members: Vec<usize> = (0..self.group.len()).filter(|&j| self.group[j] == g && !self.doc.fields()[j].disabled).collect();
1076        let p = members.iter().position(|&j| j == i)?;
1077        let n = members.len();
1078        Some(members[if back { (p + n - 1) % n } else { (p + 1) % n }])
1079    }
1080
1081    fn clamp(&self, p: TextPos) -> TextPos {
1082        let bl = self.doc.blocks();
1083        if bl.is_empty() {
1084            return TextPos::default();
1085        }
1086        let line = p.line.min(bl.len() - 1);
1087        let t = &bl[line].text;
1088        let mut b = p.byte.min(t.len());
1089        while !t.is_char_boundary(b) {
1090            b -= 1;
1091        }
1092        TextPos::new(line, b)
1093    }
1094}
1095
1096// the first link spot of block k that matches (the spots are in reading order)
1097fn spot_of(spots: &[MdLinkSpot], k: usize, f: impl Fn(&MdLinkSpot) -> bool) -> Option<usize> {
1098    let i0 = spots.partition_point(|s| s.block < k);
1099    spots[i0..].iter().take_while(|s| s.block == k).position(f).map(|i| i0 + i)
1100}
1101
1102// a text area's x on the screen: the column mirrored for a right-to-left document
1103fn box_x(cx: i32, cw: i32, x: i32, w: i32, rtl: bool) -> i32 {
1104    if rtl { cx + cw - x - w } else { cx + x }
1105}
1106
1107impl Ui {
1108    /// ui_markdown_view(id, &mut st, text, MV_*) -> MdEv (see the module header of md_view.rs)
1109    #[cfg_attr(jfgui_dev, track_caller)]
1110    pub fn markdown_view(&mut self, id: u32, st: &mut MdViewState, text: &str, flags: u32) -> MdEv {
1111        let _src = self.src_scope();
1112        st.set_markdown(text);
1113        self.doc_view(id, st, flags)
1114    }
1115
1116    /// ui_markdown_fit(&mut st, text, w_px, MV_*) -> the height in DIP a view w_px wide needs for the whole
1117    /// text (see the module header)
1118    pub fn markdown_fit(&mut self, st: &mut MdViewState, text: &str, w_px: i32, flags: u32) -> i32 {
1119        st.set_markdown(text);
1120        let pad = self.dp(if flags & MV_NO_FRAME == 0 { PAD } else { PAD_BARE });
1121        let sbw = self.style.scrollbar_thickness(self);
1122        let hair = self.hair().max(1);
1123        let drtl = if flags & MV_RTL != 0 {
1124            true
1125        } else if flags & MV_LTR != 0 {
1126            false
1127        } else {
1128            st.auto_rtl.unwrap_or(self.rtl)
1129        };
1130        // the same column as the view's, so that it finds these layouts
1131        let cw = ((w_px - hair * 2).max(1) - pad * 2 - sbw).max(self.dp(80));
1132        st.sync();
1133        let fc = self.md_faces();
1134        let key = (self.wp.rt.dpi, cw, self.layout_scale(), drtl, fc);
1135        let n = st.doc.blocks().len();
1136        if key != st.lc.key || !st.sized || st.rows.len() != n {
1137            if key != st.lc.key {
1138                st.lc.lays.clear();
1139                st.lc.cols.clear();
1140                st.lc.scaled.clear();
1141            }
1142            st.lc.key = key;
1143            st.rows.forget_all();
1144            self.md_guess(st, cw, &fc);
1145            st.sized = true;
1146        }
1147        for k in 0..n {
1148            self.md_lay(st, k, cw, drtl, &fc);
1149        }
1150        let total = st.rows.total().clamp(0, i32::MAX as i64 / 2) as i32;
1151        self.px2dip(total + hair * 2) + 2
1152    }
1153
1154    /// ui_html_view(id, &mut st, html, MV_*) -> MdEv: the HTML counterpart of markdown_view
1155    #[cfg_attr(jfgui_dev, track_caller)]
1156    pub fn html_view(&mut self, id: u32, st: &mut MdViewState, html: &str, flags: u32) -> MdEv {
1157        let _src = self.src_scope();
1158        st.set_html(html);
1159        self.doc_view(id, st, flags)
1160    }
1161
1162    // ------------------------------------------------------------------ layout
1163
1164    fn md_faces(&mut self) -> Faces {
1165        self.ts_font(TS_BODY);
1166        let (_, size) = self.text.get_font();
1167        let mut f = Faces { size, ..Faces::default() };
1168        for mono in 0..2 {
1169            for k in 0..4 {
1170                let fam = if mono == 1 { "monospace" } else { "" };
1171                f.f[mono][k] = self.rt_face(fam, k & 1 != 0, k & 2 != 0);
1172            }
1173        }
1174        f
1175    }
1176
1177    // metrics of a font: ascent, descent, line height
1178    fn md_metrics(&mut self, font: u32, size: i32) -> (i32, i32, i32) {
1179        self.text.set_font(font, size);
1180        let (asc, desc) = (self.text.ascent(), self.text.descent());
1181        (asc, desc, self.text.line_h().max(asc + desc))
1182    }
1183
1184    // an image: loaded on first use (VFS names, paths after base; the app's addresses asked
1185    // once: Pending until they come)
1186    fn md_img(&mut self, lc: &mut Cache, doc: &MdDoc, base: &str, i: usize) -> Img {
1187        if let Some(Img::Unread) = lc.imgs.get(i) {
1188            let src = doc.images().get(i).map_or("", |m| m.src.as_str()).trim();
1189            let img = match img_at(src, base, lc.mode) {
1190                (Some(path), _) => match plat::read_file(&path) {
1191                    Some(d) => img_decode(&d, src),
1192                    None => Img::Failed,
1193                },
1194                (None, Some(addr)) => match lc.req.get(&addr) {
1195                    Some(Req::Got(img)) => img.clone(),
1196                    Some(Req::Asked) => Img::Pending(Rc::from(addr.as_str())),
1197                    None => {
1198                        lc.queue.push(addr.clone());
1199                        let p = Img::Pending(Rc::from(addr.as_str()));
1200                        lc.req.insert(addr, Req::Asked);
1201                        p
1202                    }
1203                },
1204                _ => Img::Failed,
1205            };
1206            lc.imgs[i] = img;
1207        }
1208        lc.imgs.get(i).cloned().unwrap_or(Img::Failed)
1209    }
1210
1211    // an image's size in px at most `avail` wide (a frame with the alt text when it failed;
1212    // while it is asked: the size the HTML gives, else that frame)
1213    fn md_img_size(&mut self, lc: &mut Cache, doc: &MdDoc, base: &str, i: usize, avail: i32, fc: &Faces) -> (i32, i32) {
1214        let m = doc.images().get(i);
1215        let (aw, ah) = m.map_or((0, 0), |m| (m.w as i32, m.h as i32));
1216        let nat = match self.md_img(lc, doc, base, i) {
1217            Img::Bitmap(sf) => Some((sf.w as f64, sf.h as f64)),
1218            Img::Vector(s) => Some(s.size()),
1219            Img::Pending(_) if aw > 0 && ah > 0 => Some((aw as f64, ah as f64)),
1220            _ => None,
1221        };
1222        let (w, h) = match nat {
1223            Some((w, h)) if w > 0.0 && h > 0.0 => {
1224                let (w, h) = match (aw > 0, ah > 0) {
1225                    (true, true) => (aw as f64, ah as f64),
1226                    (true, false) => (aw as f64, aw as f64 * h / w),
1227                    (false, true) => (ah as f64 * w / h, ah as f64),
1228                    _ => (w, h),
1229                };
1230                (self.dp((w + 0.5).min(100_000.0) as i32).max(1), self.dp((h + 0.5).min(100_000.0) as i32).max(1))
1231            }
1232            _ => {
1233                // the alt text in a frame
1234                let alt = m.map_or("", |m| m.alt.as_str());
1235                let alt = if alt.is_empty() { tr("Bild") } else { alt };
1236                let (_, _, lh) = self.md_metrics(fc.get(false, false, false), fc.size);
1237                let tw = self.text.width(alt);
1238                (tw + self.dp(16), lh + self.dp(8))
1239            }
1240        };
1241        if w > avail && avail > 0 {
1242            (avail, ((h as i64 * avail as i64) / w as i64).max(1) as i32)
1243        } else {
1244            (w, h)
1245        }
1246    }
1247
1248    // a field's box in px (at most `avail` wide; the style's sizes, text fields as wide as their
1249    // characters) and its room above / below the baseline: its middle on the middle of the
1250    // body text, a gap above and below
1251    fn md_field_size(&mut self, doc: &MdDoc, i: usize, avail: i32, fc: &Faces) -> (i32, i32, i32, i32) {
1252        let Some(f) = doc.fields().get(i) else { return (1, 1, 1, 0) };
1253        let st = self.style;
1254        self.ts_font(TS_BODY);
1255        let cw = self.text.width("0").max(1);
1256        let (w, h) = match f.kind {
1257            MF_TEXT | MF_PASSWORD | MF_TEXTAREA => {
1258                let multi = f.kind == MF_TEXTAREA;
1259                let lh = self.ts_line(TS_BODY);
1260                let mut sp = Sp { value2: lh, flags: multi as u32, ..Sp::default() };
1261                let (_, h) = st.textbox_metrics(self, &mut sp);
1262                let n = if f.size > 0 { f.size.min(1000) } else { FIELD_CHARS } as i32;
1263                let w = sp.s[0] + sp.s[1] + cw * n;
1264                if multi {
1265                    let rows = if f.rows > 0 { f.rows.min(200) } else { FIELD_ROWS } as i32;
1266                    (w, sp.s[2] * 2 + lh * rows)
1267                } else {
1268                    self.ctrl_size(w, h, 0, false)
1269                }
1270            }
1271            MF_CHECKBOX | MF_RADIO => {
1272                let mut sp = Sp::default();
1273                if f.kind == MF_CHECKBOX {
1274                    st.checkbox_size(self, &mut sp);
1275                } else {
1276                    st.radio_size(self, &mut sp);
1277                }
1278                (sp.s[0], sp.s[0])
1279            }
1280            MF_SELECT => {
1281                self.ts_font(TS_BODY);
1282                let widest = f.options.iter().map(|o| self.text.width(&o.label)).max().unwrap_or(0);
1283                let mut sp = Sp { value: widest, ..Sp::default() };
1284                let (w, h) = st.combo_size(self, &mut sp);
1285                self.ctrl_size(w, h, 0, false)
1286            }
1287            MF_HIDDEN => (1, 1),
1288            _ => {
1289                let mut sp = Sp { label: Some(f.caption()), ..Sp::default() };
1290                let (w, h) = st.button_size(self, &mut sp);
1291                self.ctrl_size(w, h, 0, false)
1292            }
1293        };
1294        let (w, h) = (w.min(avail.max(1)).max(1), h.max(1));
1295        let (asc, desc, _) = self.md_metrics(fc.get(false, false, false), fc.size);
1296        let up = ((h + asc - desc) / 2).clamp(0, h);
1297        let gap = self.dp(FIELD_GAP);
1298        (w, h, up + gap, h - up + gap)
1299    }
1300
1301    // the runs of bytes a .. b of a block's text from its spans
1302    #[allow(clippy::too_many_arguments)]
1303    fn md_segs(&mut self, lc: &mut Cache, doc: &MdDoc, base: &str, k: usize, a: usize, b: usize, scale: u32, bold: bool, avail: i32, fc: &Faces) -> Vec<Seg> {
1304        let bl = &doc.blocks()[k];
1305        let text = bl.text.as_str();
1306        let size = (fc.size as i64 * scale as i64 / 100) as i32;
1307        let mut out: Vec<Seg> = Vec::new();
1308        let mut metr: Vec<((u32, i32), (i32, i32, i32))> = Vec::new();
1309        let mut add = |ui: &mut Ui, lc: &mut Cache, s0: usize, s1: usize, sp: Option<&MdSpan>| {
1310            if s0 >= s1 {
1311                return;
1312            }
1313            let style = sp.map_or(0, |s| s.style);
1314            let code = style & MS_CODE != 0;
1315            let (bo, it) = (bold || style & MS_BOLD != 0, style & MS_ITALIC != 0);
1316            let shaped = text[s0..s1].chars().any(rtl_char);
1317            let mut font = fc.get(code, bo, it);
1318            if shaped {
1319                font = ui.md_rtl_font(font, &text[s0..s1], bo, it);
1320            }
1321            let sz = if code { size * 9 / 10 } else { size };
1322            let m = match metr.iter().find(|m| m.0 == (font, sz)) {
1323                Some(m) => m.1,
1324                None => {
1325                    let m = ui.md_metrics(font, sz);
1326                    metr.push(((font, sz), m));
1327                    m
1328                }
1329            };
1330            let image = sp.map_or(0, |s| s.image);
1331            let (iw, ih) = if image != 0 { ui.md_img_size(lc, doc, base, image as usize - 1, avail, fc) } else { (0, 0) };
1332            // a field: its box and its room above / below the baseline
1333            let field = sp.map_or(0, |s| s.field);
1334            let (iw, ih, m) = if field != 0 {
1335                let (w, h, up, down) = ui.md_field_size(doc, field as usize - 1, avail, fc);
1336                (w, h, (up, down, up + down))
1337            } else {
1338                (iw, ih, m)
1339            };
1340            out.push(Seg {
1341                a: s0,
1342                b: s1,
1343                font,
1344                size: sz,
1345                asc: m.0,
1346                desc: m.1,
1347                lh: m.2,
1348                style,
1349                color: sp.map_or(0, |s| s.color),
1350                bg: sp.map_or(0, |s| s.bg),
1351                link: sp.map_or(0, |s| s.link),
1352                image,
1353                iw,
1354                ih,
1355                syn: 0,
1356                shaped,
1357                pad: if code { ui.dp_text(CODE_PAD) } else { 0 },
1358                field,
1359            });
1360        };
1361        let mut i = a;
1362        for s in &bl.spans {
1363            let (sa, sb) = ((s.a as usize).max(a), (s.b as usize).min(b));
1364            if sb <= sa {
1365                continue;
1366            }
1367            if sa > i {
1368                add(self, lc, i, sa, None);
1369            }
1370            add(self, lc, sa.max(i), sb, Some(s));
1371            i = i.max(sb);
1372        }
1373        if i < b {
1374            add(self, lc, i, b, None);
1375        }
1376        out
1377    }
1378
1379    // the font of a right-to-left run: the run's own when it has the script, else the font of
1380    // the fallback chain that has it (shaping takes the script's tables from one font), styled
1381    // alike
1382    fn md_rtl_font(&mut self, font: u32, s: &str, bold: bool, italic: bool) -> u32 {
1383        let Some(c) = s.chars().find(|&c| rtl_char(c) && c.is_alphabetic()) else { return font };
1384        if self.text.has(font, c as u32) {
1385            return font;
1386        }
1387        let Some(f) = self.text.fallback_for(c as u32) else { return font };
1388        let flags = if bold { FS_SYN_BOLD } else { 0 } | if italic { FS_SYN_ITALIC } else { 0 };
1389        if flags == 0 {
1390            return f;
1391        }
1392        let r = self.text.style(f, flags);
1393        if r >= 0 { r as u32 } else { f }
1394    }
1395
1396    // the runs of a code block: the highlighter's tokens of every line
1397    fn md_code_segs(&mut self, text: &str, lang: &str, fc: &Faces) -> Vec<Seg> {
1398        let h = Highlighter::for_name(lang);
1399        let font = fc.get(true, false, false);
1400        let size = fc.size * 9 / 10;
1401        let (asc, desc, lh) = self.md_metrics(font, size);
1402        let fb = fc.get(true, true, false);
1403        let (basc, bdesc, blh) = self.md_metrics(fb, size);
1404        let theme = self.code_theme();
1405        let mut out: Vec<Seg> = Vec::new();
1406        let mut push = |a: usize, b: usize, kind: u8| {
1407            if a >= b {
1408                return;
1409            }
1410            let bold = theme.bold & (1 << kind) != 0;
1411            if let Some(l) = out.last_mut() {
1412                if l.b == a && l.syn == kind + 1 {
1413                    l.b = b;
1414                    return;
1415                }
1416            }
1417            let (f, m) = if bold { (fb, (basc, bdesc, blh)) } else { (font, (asc, desc, lh)) };
1418            out.push(Seg {
1419                a,
1420                b,
1421                font: f,
1422                size,
1423                asc: m.0,
1424                desc: m.1,
1425                lh: m.2,
1426                style: if theme.italic & (1 << kind) != 0 { MS_ITALIC } else { 0 },
1427                color: 0,
1428                bg: 0,
1429                link: 0,
1430                image: 0,
1431                iw: 0,
1432                ih: 0,
1433                syn: kind + 1,
1434                shaped: false,
1435                pad: 0,
1436                field: 0,
1437            });
1438        };
1439        let mut state = 0;
1440        let mut toks: Vec<Token> = Vec::new();
1441        let mut o = 0;
1442        for line in text.split('\n') {
1443            state = h.line(line, state, &mut toks);
1444            let mut i = 0;
1445            for t in &toks {
1446                let (ta, tb) = (t.start.min(line.len()), t.end.min(line.len()));
1447                if ta > i {
1448                    push(o + i, o + ta, 0);
1449                }
1450                push(o + ta.max(i), o + tb, t.kind);
1451                i = i.max(tb);
1452            }
1453            push(o + i, o + line.len(), 0);
1454            if o + line.len() < text.len() {
1455                push(o + line.len(), o + line.len() + 1, 0);
1456            }
1457            o += line.len() + 1;
1458        }
1459        out
1460    }
1461
1462    // lay out bytes a .. b of a text in rows at most `avail` px wide (align: 0 start, 1 centre,
1463    // 2 end; rtl: the rows run right to left; stops: the caret stops are needed).  Linear in
1464    // the text: the runs are found by binary search, an atom wider than a row is split by
1465    // its clusters' widths.
1466    #[allow(clippy::too_many_arguments)]
1467    fn md_flow(&mut self, text: &str, a: usize, b: usize, segs: &[Seg], avail: i32, align: u8, rtl: bool, empty: (i32, i32, i32), stops_too: bool) -> Flow {
1468        let tb = text.as_bytes();
1469        let shaping0 = self.text.set_shaping(false);
1470        let avail = avail.max(1);
1471        let lim = (avail as i64) << 8;
1472        // the width (24.8) of x .. y
1473        let width = |ui: &mut Ui, x: usize, y: usize| -> i64 {
1474            let mut pen = 0i64;
1475            let k0 = segs.partition_point(|s| s.b <= x);
1476            for s in &segs[k0..] {
1477                if s.a >= y {
1478                    break;
1479                }
1480                let (s0, s1) = (s.a.max(x), s.b.min(y));
1481                if s0 >= s1 {
1482                    continue;
1483                }
1484                if s.image != 0 || s.field != 0 {
1485                    pen += (s.iw as i64) << 8;
1486                    continue;
1487                }
1488                ui.text.set_font(s.font, s.size);
1489                ui.text.set_shaping(s.shaped);
1490                pen += ui.text.measure_n(&tb[s0..], s1 - s0) as i64;
1491                if s.pad > 0 {
1492                    pen += (((s0 == s.a) as i64) + ((s1 == s.b) as i64)) * ((s.pad as i64) << 8);
1493                }
1494            }
1495            pen
1496        };
1497        // the end of x .. e without its trailing blanks and line end
1498        let vis = |e: usize, s: usize| {
1499            let mut v = e;
1500            while v > s && matches!(tb[v - 1], b' ' | b'\t' | b'\n') {
1501                v -= 1;
1502            }
1503            v
1504        };
1505        let sub = &text[a..b];
1506        let mut rows_ab: Vec<(usize, usize)> = Vec::new();
1507        if !sub.is_empty() {
1508            let ops: Vec<(usize, bool)> = segment::line_breaks(sub).into_iter().map(|o| (o.0 + a, o.1)).collect();
1509            let mut start = a;
1510            let mut x = 0i64;
1511            let mut prev = a;
1512            for &(o, mand) in &ops {
1513                if o <= prev {
1514                    continue;
1515                }
1516                let atom = width(self, prev, vis(o, prev));
1517                if x + atom > lim && prev > start {
1518                    rows_ab.push((start, prev));
1519                    start = prev;
1520                    x = 0;
1521                }
1522                if x == 0 && atom > lim {
1523                    // an atom wider than a row: rows of the clusters that fit
1524                    let (mut s, mut acc, mut e) = (start, 0i64, start);
1525                    let ve = vis(o, start);
1526                    while e < ve {
1527                        let n = segment::grapheme_next(text, e).min(ve);
1528                        let gw = width(self, e, n);
1529                        if acc + gw > lim && e > s {
1530                            rows_ab.push((s, e));
1531                            s = e;
1532                            acc = 0;
1533                        }
1534                        acc += gw;
1535                        e = n;
1536                    }
1537                    start = s;
1538                }
1539                x += width(self, prev.max(start), o);
1540                prev = o;
1541                if mand && o < b {
1542                    rows_ab.push((start, o));
1543                    start = o;
1544                    x = 0;
1545                }
1546            }
1547            if start < b || rows_ab.is_empty() {
1548                rows_ab.push((start, b));
1549            }
1550        } else {
1551            rows_ab.push((a, a));
1552        }
1553        let mut flow = Flow::default();
1554        let mut y = 0;
1555        for &(ra, rb) in &rows_ab {
1556            let mut pieces: Vec<Piece> = Vec::new();
1557            let (mut asc, mut desc, mut lh) = (0, 0, 0);
1558            let mut pen = 0i64;
1559            let k0 = segs.partition_point(|s| s.b <= ra);
1560            for s in &segs[k0..] {
1561                if s.a >= rb {
1562                    break;
1563                }
1564                let (s0, s1) = (s.a.max(ra), s.b.min(rb));
1565                if s0 >= s1 {
1566                    continue;
1567                }
1568                if s.pad > 0 && s0 == s.a {
1569                    pen += (s.pad as i64) << 8;
1570                }
1571                let base = Piece {
1572                    a: s0 as u32,
1573                    b: s1 as u32,
1574                    x: ((pen + 128) >> 8) as i32,
1575                    w: 0,
1576                    font: s.font,
1577                    size: s.size,
1578                    asc: s.asc,
1579                    style: s.style,
1580                    color: s.color,
1581                    bg: s.bg,
1582                    link: s.link,
1583                    image: s.image,
1584                    ih: s.ih,
1585                    syn: s.syn,
1586                    shaped: s.shaped,
1587                    field: s.field,
1588                };
1589                if s.field != 0 {
1590                    asc = asc.max(s.asc);
1591                    desc = desc.max(s.desc);
1592                    lh = lh.max(s.lh);
1593                    pieces.push(Piece { w: s.iw, ..base });
1594                    pen += (s.iw as i64) << 8;
1595                    continue;
1596                }
1597                if s.image != 0 {
1598                    asc = asc.max(s.ih);
1599                    lh = lh.max(s.ih);
1600                    pieces.push(Piece { w: s.iw, ..base });
1601                    pen += (s.iw as i64) << 8;
1602                    continue;
1603                }
1604                asc = asc.max(s.asc);
1605                desc = desc.max(s.desc);
1606                lh = lh.max(s.lh);
1607                self.text.set_font(s.font, s.size);
1608                self.text.set_shaping(s.shaped);
1609                let w = self.text.measure_n(&tb[s0..], s1 - s0) as i64;
1610                pieces.push(Piece { w: ((w + 255) >> 8) as i32, ..base });
1611                pen += w;
1612                if s.pad > 0 && s1 == s.b {
1613                    pen += (s.pad as i64) << 8;
1614                }
1615            }
1616            if pieces.is_empty() {
1617                (asc, desc, lh) = empty;
1618            }
1619            let h = lh.max(asc + desc).max(1);
1620            let base = asc + (h - asc - desc) / 2;
1621            let ve = vis(rb, ra);
1622            let vis_w = ((width(self, ra, ve) + 255) >> 8) as i32;
1623            flow.w = flow.w.max(vis_w);
1624            if rtl {
1625                for p in pieces.iter_mut() {
1626                    p.x = vis_w - p.x - p.w;
1627                }
1628            }
1629            let spare = (avail - vis_w).max(0);
1630            let off = match (align, rtl) {
1631                (1, _) => spare / 2,
1632                (0, true) | (2, false) => spare,
1633                _ => 0,
1634            };
1635            // the caret stops: the grapheme boundaries of every piece (measured from the
1636            // piece's start, kerning included; very long pieces cluster by cluster)
1637            let mut stops: Vec<(u32, i32)> = Vec::new();
1638            for p in pieces.iter_mut() {
1639                p.x += off;
1640                if !stops_too {
1641                    continue;
1642                }
1643                if p.image != 0 || p.field != 0 {
1644                    let (l, r) = if rtl { (p.x + p.w, p.x) } else { (p.x, p.x + p.w) };
1645                    stops.push((p.a, l));
1646                    stops.push((p.b, r));
1647                    continue;
1648                }
1649                let s = &tb[p.a as usize..p.b as usize];
1650                self.text.set_font(p.font, p.size);
1651                self.text.set_shaping(p.shaped);
1652                let long = s.len() > 512 && !p.shaped;
1653                let (mut g, mut acc) = (p.a as usize, 0i64);
1654                loop {
1655                    let k = g - p.a as usize;
1656                    let rel = if p.shaped {
1657                        self.text.caret_x(s, k)
1658                    } else if long {
1659                        ((acc + 128) >> 8) as i32
1660                    } else {
1661                        (self.text.measure_n(s, k) + 128) >> 8
1662                    };
1663                    stops.push((g as u32, p.x + rel));
1664                    if g >= p.b as usize {
1665                        break;
1666                    }
1667                    let n = segment::grapheme_next(text, g).min(p.b as usize);
1668                    if long {
1669                        acc += self.text.measure_n(&tb[g..], n - g) as i64;
1670                    }
1671                    g = n;
1672                }
1673            }
1674            if stops.is_empty() {
1675                stops.push((ra as u32, if rtl { avail - off.min(avail) } else { off }));
1676            }
1677            stops.dedup_by_key(|s| s.0);
1678            flow.rows.push(LRow { a: ra as u32, b: rb as u32, y, h, base, pieces, stops });
1679            y += h;
1680        }
1681        flow.h = y;
1682        self.text.set_shaping(shaping0);
1683        flow
1684    }
1685
1686    // the column widths of table t in a column `cw` px wide (from the cells' natural widths)
1687    fn md_cols(&mut self, lc: &mut Cache, doc: &MdDoc, base: &str, t: u32, cw: i32, fc: &Faces) -> Rc<Vec<i32>> {
1688        if let Some(c) = lc.cols.get(&t) {
1689            return c.clone();
1690        }
1691        let rows: Vec<usize> = lc.rows_of.get(&t).map(|v| v.iter().copied().take(TABLE_SAMPLE).collect()).unwrap_or_default();
1692        let ncols = doc.tables().get(t as usize).map_or(0, |x| x.aligns.len()).max(rows.iter().map(|&k| doc.blocks()[k].cells.len()).max().unwrap_or(0)).max(1);
1693        let pad = self.dp_text(CELL_PX) * 2;
1694        let mut nat = alloc::vec![self.dp(24); ncols];
1695        let empty = self.md_metrics(fc.get(false, false, false), fc.size);
1696        for &k in &rows {
1697            let bl = &doc.blocks()[k];
1698            for (c, &(ca, cb)) in bl.cells.iter().enumerate().take(ncols) {
1699                let segs = self.md_segs(lc, doc, base, k, ca as usize, cb as usize, 100, bl.head, cw, fc);
1700                let f = self.md_flow(&bl.text, ca as usize, cb as usize, &segs, 1 << 22, 0, false, empty, false);
1701                nat[c] = nat[c].max(f.w + pad);
1702            }
1703        }
1704        let total: i64 = nat.iter().map(|&w| w as i64).sum();
1705        let mut wd = nat.clone();
1706        if total > cw as i64 {
1707            // narrow columns keep their width, the others share the rest
1708            let mut fixed = alloc::vec![false; ncols];
1709            loop {
1710                let free: i64 = cw as i64 - (0..ncols).filter(|&c| fixed[c]).map(|c| nat[c] as i64).sum::<i64>();
1711                let n = fixed.iter().filter(|f| !**f).count().max(1) as i64;
1712                let share = (free / n).max(self.dp(48) as i64);
1713                let mut changed = false;
1714                for c in 0..ncols {
1715                    if !fixed[c] && (nat[c] as i64) <= share {
1716                        fixed[c] = true;
1717                        changed = true;
1718                    }
1719                }
1720                if !changed {
1721                    for c in 0..ncols {
1722                        if !fixed[c] {
1723                            wd[c] = share as i32;
1724                        }
1725                    }
1726                    break;
1727                }
1728            }
1729        }
1730        let rc = Rc::new(wd);
1731        lc.cols.insert(t, rc.clone());
1732        rc
1733    }
1734
1735    // the layout of block k in a column cw px wide
1736    #[allow(clippy::too_many_arguments)]
1737    fn md_block(&mut self, lc: &mut Cache, doc: &MdDoc, base: &str, k: usize, cw: i32, drtl: bool, fc: &Faces) -> BLay {
1738        let bl = &doc.blocks()[k];
1739        let depth = (bl.quote as usize + bl.list as usize).min(32);
1740        // the boxes of CSS at their place among the indents of quotes and lists: (box, its
1741        // border's x, what it leaves at the end); left and right as on the screen (a right-to-left
1742        // document's indents start on the right)
1743        let css: Vec<(u32, &MdBox)> = bl.boxes.iter().filter_map(|&i| doc.boxes().get(i as usize).map(|b| (i, b))).collect();
1744        let lr = |v: [i32; 4]| if drtl { (v[1], v[3]) } else { (v[3], v[1]) };
1745        let mut geo: Vec<(u32, i32, i32)> = Vec::new();
1746        let (mut x, mut right, mut bi, mut mx) = (0, 0, 0, 0);
1747        let mut bars = Vec::new();
1748        for lv in 0..=depth {
1749            if lv == depth {
1750                mx = x;
1751            }
1752            while bi < css.len() && (css[bi].1.depth as usize <= lv || lv == depth) {
1753                let (i, b) = css[bi];
1754                let (ms, me) = lr(b.margin.map(|v| self.dp_text(v as i32)));
1755                let (ps, pe) = lr(b.padding.map(|v| self.dp_text(v as i32)));
1756                let (bs, be) = lr(b.border.map(|v| self.dp(v as i32)));
1757                x += ms;
1758                right += me;
1759                geo.push((i, x, right));
1760                x += bs + ps;
1761                right += be + pe;
1762                bi += 1;
1763            }
1764            if lv == depth {
1765                break;
1766            }
1767            if bl.path >> lv & 1 != 0 {
1768                bars.push(x + self.dp(2));
1769                x += self.dp_text(QUOTE_W);
1770            } else {
1771                x += self.dp_text(LIST_W);
1772            }
1773        }
1774        let w = (cw - x - right).max(self.dp(48));
1775        let mut gap = match bl.space {
1776            0 => 0,
1777            1 => self.dp_text(TIGHT_GAP),
1778            _ => self.dp_text(PARA_GAP),
1779        };
1780        let rtl = text_rtl(&bl.text).unwrap_or(drtl);
1781        let empty = self.md_metrics(fc.get(false, false, false), fc.size);
1782        let align = match (bl.align, rtl) {
1783            (1, _) => 1,
1784            (2, false) | (0, true) => 2,
1785            _ => 0,
1786        };
1787        // align 2 for a right-to-left block means its start (the flow takes the direction)
1788        let falign = if rtl { [0, 1, 0][align as usize] } else { align };
1789        let falign = if bl.align == 2 && rtl { 2 } else { falign };
1790        let mut boxes = Vec::new();
1791        let ch;
1792        match bl.kind {
1793            MB_HEADING => {
1794                let lv = bl.level.clamp(1, 6) as usize;
1795                if bl.space > 0 {
1796                    gap = gap.max(self.dp_text(if lv <= 2 { 22 } else { 16 }));
1797                }
1798                let scale = HEAD_SCALE[lv - 1];
1799                let segs = self.md_segs(lc, doc, base, k, 0, bl.text.len(), scale, true, w, fc);
1800                let hm = self.md_metrics(fc.get(false, true, false), (fc.size as i64 * scale as i64 / 100) as i32);
1801                let flow = self.md_flow(&bl.text, 0, bl.text.len(), &segs, w, falign, rtl, hm, true);
1802                ch = flow.h + if lv <= 2 { self.dp(10) } else { 0 };
1803                boxes.push(FBox { x, y: 0, w, flow });
1804            }
1805            MB_CODE => {
1806                let (px, py) = (self.dp_text(CODE_PX), self.dp_text(CODE_PY));
1807                let segs = self.md_code_segs(&bl.text, &bl.lang, fc);
1808                let mono = self.md_metrics(fc.get(true, false, false), fc.size * 9 / 10);
1809                let flow = self.md_flow(&bl.text, 0, bl.text.len(), &segs, w - px * 2, 0, false, mono, true);
1810                ch = flow.h + py * 2;
1811                boxes.push(FBox { x: x + px, y: py, w: w - px * 2, flow });
1812            }
1813            MB_RULE => {
1814                ch = self.dp_text(RULE_H);
1815            }
1816            MB_ROW => {
1817                let cols = self.md_cols(lc, doc, base, bl.table, w, fc);
1818                let aligns = doc.tables().get(bl.table as usize).map(|t| t.aligns.clone()).unwrap_or_default();
1819                let (px, py) = (self.dp_text(CELL_PX), self.dp_text(CELL_PY));
1820                let mut cx = x;
1821                let mut hmax = 0;
1822                for (c, &cwd) in cols.iter().enumerate() {
1823                    let (ca, cb) = bl.cells.get(c).map_or((bl.text.len(), bl.text.len()), |&(a, b)| (a as usize, b as usize));
1824                    let segs = self.md_segs(lc, doc, base, k, ca, cb, 100, bl.head, cwd - px * 2, fc);
1825                    // a column's alignment is physical: right stays right in a right-to-left table
1826                    let al = match aligns.get(c).copied().unwrap_or(MA_NONE) {
1827                        MA_CENTER => 1,
1828                        MA_RIGHT => if rtl { 0 } else { 2 },
1829                        MA_LEFT => if rtl { 2 } else { 0 },
1830                        _ => 0,
1831                    };
1832                    let flow = self.md_flow(&bl.text, ca, cb, &segs, cwd - px * 2, al, rtl, empty, true);
1833                    hmax = hmax.max(flow.h);
1834                    boxes.push(FBox { x: cx + px, y: py, w: cwd - px * 2, flow });
1835                    cx += cwd;
1836                }
1837                ch = hmax + py * 2;
1838                // the cells of a right-to-left row from the right: mirror inside the table
1839                if rtl != drtl {
1840                    let tw = cx - x;
1841                    for b in boxes.iter_mut() {
1842                        b.x = x + tw - (b.x - x) - b.w;
1843                    }
1844                }
1845            }
1846            _ => {
1847                let segs = self.md_segs(lc, doc, base, k, 0, bl.text.len(), 100, false, w, fc);
1848                let flow = self.md_flow(&bl.text, 0, bl.text.len(), &segs, w, falign, rtl, empty, true);
1849                ch = flow.h;
1850                boxes.push(FBox { x, y: 0, w, flow });
1851            }
1852        }
1853        if geo.is_empty() && (k == 0 || doc.blocks()[k - 1].boxes.is_empty()) {
1854            return BLay { h: gap + ch, gap, ch, x, w, boxes, bars, css: Vec::new(), mx };
1855        }
1856        // a margin CSS gives at the top of a box starting here, or at the bottom of one that
1857        // ended at the block before, takes the place of the gap
1858        let starts = |i: u32| doc.box_ends(i).0 == k;
1859        let ends = |i: u32| doc.box_ends(i).1 == k;
1860        let own_mt = css.iter().any(|&(i, b)| starts(i) && b.mset & 1 != 0);
1861        let prev_mb = k > 0 && doc.blocks()[k - 1].boxes.iter().any(|&i| doc.box_ends(i).1 == k - 1 && doc.boxes().get(i as usize).is_some_and(|b| b.mset & 4 != 0));
1862        if own_mt || prev_mb {
1863            gap = 0;
1864        }
1865        // the boxes' tops (the gap outside the ones starting here) and bottoms
1866        let mut y = gap;
1867        let mut out: Vec<CssBox> = Vec::with_capacity(geo.len());
1868        for &(i, bx, br) in &geo {
1869            let b = &doc.boxes()[i as usize];
1870            let first = starts(i);
1871            let y0 = if first { y + self.dp_text(b.margin[0] as i32) } else { 0 };
1872            if first {
1873                y = y0 + self.dp(b.border[0] as i32) + self.dp_text(b.padding[0] as i32);
1874            }
1875            out.push(CssBox { b: i, x: bx, w: (cw - bx - br).max(1), y0, y1: 0, first, last: ends(i) });
1876        }
1877        let top = y;
1878        let mut yb = top + ch;
1879        for g in out.iter_mut().rev() {
1880            if g.last {
1881                let b = &doc.boxes()[g.b as usize];
1882                yb += self.dp_text(b.padding[2] as i32) + self.dp(b.border[2] as i32);
1883                g.y1 = yb;
1884                yb += self.dp_text(b.margin[2] as i32);
1885            }
1886        }
1887        for g in out.iter_mut().filter(|g| !g.last) {
1888            g.y1 = yb;
1889        }
1890        BLay { h: yb, gap: top, ch, x, w, boxes, bars, css: out, mx }
1891    }
1892
1893    // a block's layout from the cache (laid out now if needed; its height goes to the rows)
1894    fn md_lay(&mut self, st: &mut MdViewState, k: usize, cw: i32, drtl: bool, fc: &Faces) -> Rc<BLay> {
1895        if let Some(l) = st.lc.lays.get(&k) {
1896            return l.clone();
1897        }
1898        let l = Rc::new(self.md_block(&mut st.lc, &st.doc, &st.base, k, cw, drtl, fc));
1899        st.rows.set(k, l.h);
1900        st.lc.lays.insert(k, l.clone());
1901        l
1902    }
1903
1904    // the heights of the blocks not laid out yet (cheap guesses for the scroll bar)
1905    fn md_guess(&mut self, st: &mut MdViewState, cw: i32, fc: &Faces) {
1906        let px = (fc.size >> 8).max(1);
1907        let lh = px * 14 / 10;
1908        let cpl = ((cw / ((px / 2).max(1))).max(8)) as usize;
1909        let (gp, gt) = (self.dp_text(PARA_GAP), self.dp_text(TIGHT_GAP));
1910        let (cpy, cell) = (self.dp_text(CODE_PY) * 2, self.dp_text(CELL_PY) * 2);
1911        let rule = self.dp_text(RULE_H);
1912        let n = st.doc.blocks().len();
1913        st.rows.resize(n);
1914        for k in 0..n {
1915            let b = &st.doc.blocks()[k];
1916            let gap = match b.space {
1917                0 => 0,
1918                1 => gt,
1919                _ => gp,
1920            };
1921            let h = match b.kind {
1922                MB_CODE => (b.text.bytes().filter(|&c| c == b'\n').count() as i32 + 1) * lh + cpy,
1923                MB_RULE => rule,
1924                MB_ROW => lh + cell,
1925                MB_HEADING => {
1926                    let s = HEAD_SCALE[b.level.clamp(1, 6) as usize - 1] as i32;
1927                    lh * s / 100 * (b.text.len() / cpl.max(1) + 1) as i32 + gp
1928                }
1929                _ => lh * ((b.text.len() / cpl.max(1)) as i32 + 1),
1930            };
1931            st.rows.guess(k, gap + h.max(1));
1932        }
1933    }
1934
1935    // ------------------------------------------------------------------ the widget
1936
1937    /// ui_doc_view(id, &mut st, MV_*) -> MdEv: the document set in st (see the module header)
1938    #[cfg_attr(jfgui_dev, track_caller)]
1939    pub fn doc_view(&mut self, id: u32, st: &mut MdViewState, flags: u32) -> MdEv {
1940        let _src = self.src_scope();
1941        let mut out = MdEv::default();
1942        let stflags = self.take_flags();
1943        let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
1944        let frame = flags & MV_NO_FRAME == 0;
1945        let pad = self.dp(if frame { PAD } else { PAD_BARE });
1946        let sbw = self.style.scrollbar_thickness(self);
1947        let hair = self.hair().max(1);
1948        let drtl = if flags & MV_RTL != 0 {
1949            true
1950        } else if flags & MV_LTR != 0 {
1951            false
1952        } else {
1953            st.auto_rtl.unwrap_or(self.rtl)
1954        };
1955        // the column: the view inside its padding, the scroll bar's room at the end
1956        let (vx, vy, vw, vh) = (r[0] + hair, r[1] + hair, (r[2] - hair * 2).max(1), (r[3] - hair * 2).max(1));
1957        let cw = (vw - pad * 2 - sbw).max(self.dp(80));
1958        // (island coordinates: in a mirrored window the scroll bar is at the left)
1959        let cx = if self.rtl { vx + pad + sbw } else { vx + pad };
1960        let view_h = vh;
1961        // the images come from elsewhere now: read again
1962        if st.lc.mode != st.images {
1963            st.lc.mode = st.images;
1964            st.lc.imgs.iter_mut().for_each(|m| *m = Img::Unread);
1965            st.lc.lays.clear();
1966            st.lc.cols.clear();
1967            st.lc.scaled.clear();
1968            st.lc.svgs.clear();
1969        }
1970        // the document's changes (its elements, md_dom.rs): a focused field's widget moves with
1971        // its field
1972        st.sync();
1973        if let Some((old, new)) = st.refocus.take() {
1974            let fid = md_field_id(id, old);
1975            if self.wp.ctx.focus == fid {
1976                let nid = new.map_or(id, |j| md_field_id(id, j));
1977                self.wp.ctx.focus = nid;
1978                self.tb_rekey(fid, nid);
1979            }
1980        }
1981        // the layouts belong to a width, dpi, text scale, direction and fonts
1982        let fc = self.md_faces();
1983        let key = (self.wp.rt.dpi, cw, self.layout_scale(), drtl, fc);
1984        let n = st.doc.blocks().len();
1985        if key != st.lc.key || !st.sized || st.rows.len() != n {
1986            if key != st.lc.key {
1987                st.lc.lays.clear();
1988                st.lc.cols.clear();
1989                st.lc.scaled.clear();
1990            }
1991            st.lc.key = key;
1992            st.rows.forget_all();
1993            self.md_guess(st, cw, &fc);
1994            st.sized = true;
1995            st.rows.clamp(view_h);
1996        } else if st.reguess {
1997            // (blocks a change brought: estimates, the anchor where it is)
1998            self.md_guess(st, cw, &fc);
1999            st.rows.clamp(view_h);
2000        }
2001        st.reguess = false;
2002        // behave: one box for the pointer and the focus
2003        self.wp.ctx.last_rect = r;
2004        // (a pass on every pointer move: the link under the pointer, MdEv::hover)
2005        let mut hb = HB_FOCUS | HB_OWNKEYS | HB_MOVES | CUR_IBEAM << HB_CURSOR_SHIFT;
2006        if stflags & ST_DISABLED != 0 {
2007            hb |= HB_DISABLED;
2008        }
2009        let hit0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
2010        let ev = self.behave(id, hb);
2011        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
2012        let focused = !disabled && self.wp.ctx.focus == id;
2013        out.ev = ev & (EV_HOVER | EV_FOCUS);
2014        if !focused {
2015            st.link = None;
2016        }
2017        let sel0 = st.sel;
2018        let mut activate: Option<usize> = None;
2019        let mut clicked = false;
2020        // ---- keys
2021        if focused && n > 0 {
2022            activate = self.md_keys(st, id, view_h, cw, drtl, &fc);
2023        }
2024        // ---- smooth scrolling (the wheel)
2025        if st.pending != 0 {
2026            let p0 = st.pending;
2027            if self.anim_off {
2028                st.pending = 0;
2029            } else {
2030                let steps = anim_steps(self.now_ms, &mut st.t0);
2031                st.pending = anim_decay(p0, 0, steps);
2032            }
2033            let p = st.rows.pos() + (p0 - st.pending) as i64;
2034            st.rows.set_pos(p);
2035            st.follow = false;
2036            if st.pending != 0 {
2037                self.animating = true;
2038            }
2039        }
2040        // ---- the finger's way (touch.rs)
2041        let (_, pan) = self.pan_take(id);
2042        if pan != 0 {
2043            st.pending = 0;
2044            st.reveal = None;
2045            st.follow = false;
2046            let p = st.rows.pos() + pan as i64;
2047            st.rows.set_pos(p);
2048        }
2049        // ---- the visible blocks (laid out; the anchor settles at the end, a range to reveal)
2050        let mut vis = self.md_walk(st, view_h, cw, drtl, &fc);
2051        // ---- the pointer
2052        let (mx, my) = (self.rtl_map(self.wp.ctx.mx, vx, vw), self.wp.ctx.my);
2053        let c = &self.wp.ctx;
2054        let press = c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id;
2055        let held = ev & EV_HELD != 0;
2056        if press && !disabled {
2057            let p = self.md_hit(st, &vis, mx - cx, my - vy, cw, drtl);
2058            st.press_link = self.md_link_at(st, &vis, mx, my, cx, vy, cw, drtl);
2059            st.dragged = false;
2060            if let Some(p) = p {
2061                if ev & EV_DOUBLE != 0 && st.press_link.is_none() {
2062                    let t = &st.doc.blocks()[p.line].text;
2063                    let (a, b) = word_range(t, p.byte);
2064                    st.word = (TextPos::new(p.line, a), TextPos::new(p.line, b));
2065                    st.sel = TextSel::new(st.word.0, st.word.1);
2066                    st.drag = 2;
2067                } else if self.mods & MOD_SHIFT != 0 {
2068                    st.sel = TextSel::new(st.sel.anchor, p);
2069                    st.drag = 1;
2070                } else {
2071                    st.sel = TextSel::at(p);
2072                    st.drag = 1;
2073                }
2074            }
2075        } else if held && st.drag != 0 {
2076            if ev & EV_DRAG != 0 {
2077                st.dragged = true;
2078            }
2079            // beyond the edges: scroll
2080            let edge = if my < vy {
2081                my - vy
2082            } else if my >= vy + view_h {
2083                my - (vy + view_h) + 1
2084            } else {
2085                0
2086            };
2087            if edge != 0 && st.dragged {
2088                let d = edge.signum() * (edge.abs() / 2).clamp(self.dp(4), view_h / 4);
2089                let p = st.rows.pos() + d as i64;
2090                st.rows.set_pos(p.max(0));
2091                st.rows.clamp(view_h);
2092                st.follow = false;
2093                self.animating = true;
2094                vis = self.md_walk(st, view_h, cw, drtl, &fc);
2095            }
2096            let yy = my.clamp(vy, vy + view_h - 1);
2097            if let Some(p) = self.md_hit(st, &vis, mx - cx, yy - vy, cw, drtl) {
2098                if st.drag == 2 {
2099                    let t = &st.doc.blocks()[p.line].text;
2100                    let (a, b) = word_range(t, p.byte);
2101                    let (wa, wb) = (TextPos::new(p.line, a), TextPos::new(p.line, b));
2102                    st.sel = if wa < st.word.0 { TextSel::new(st.word.1, wa) } else { TextSel::new(st.word.0, wb.max(st.word.1)) };
2103                } else {
2104                    st.sel = TextSel::new(st.sel.anchor, p);
2105                }
2106            }
2107        } else if !held {
2108            if ev & EV_ACT != 0 && !st.dragged {
2109                clicked = true;
2110                if let Some(k) = st.press_link {
2111                    if self.md_link_at(st, &vis, mx, my, cx, vy, cw, drtl) == Some(k) {
2112                        activate = Some(k);
2113                        st.sel = TextSel::at(st.sel.caret);
2114                    }
2115                }
2116            }
2117            st.drag = 0;
2118            st.press_link = None;
2119        }
2120        if let Some(k) = activate {
2121            if let Some(sp) = st.doc.link_spots().get(k).copied() {
2122                let url = String::from(st.doc.link(sp.link));
2123                if flags & MV_OPEN_LINKS != 0 {
2124                    self.open_url(&url);
2125                }
2126                out.link = Some(url);
2127                out.ev |= EV_ACT;
2128                out.click = (!clicked).then(|| st.doc.element_at(sp.block, sp.a as usize)).filter(|n| !n.is_none());
2129                self.repass();
2130            }
2131        }
2132        if clicked {
2133            let n = self.md_element_at(st, &vis, mx - cx, my - vy, cw, drtl);
2134            out.click = (!n.is_none()).then_some(n);
2135            self.repass();
2136        }
2137        if st.sel != sel0 {
2138            self.repass();
2139        }
2140        // ---- draw
2141        let st_ = self.style;
2142        if frame {
2143            let mut sp = Sp { flags: BOX_CARD, ..Sp::default() };
2144            sp.place(r);
2145            st_.box_deco(self, &sp);
2146        }
2147        self.clip_push(vx, vy, vw, vh);
2148        self.ltr_begin(vx, vw);
2149        // the blocks move with the position (scrollcopy.rs)
2150        let copy = st.rows.total() > view_h as i64;
2151        if copy {
2152            self.scroll_copy_begin(id, vx as i64, vy as i64 - st.rows.pos());
2153        }
2154        let bgc = self.md_bg(frame);
2155        let mut links: Vec<Link> = Vec::new();
2156        let mut fields: Vec<(usize, [i32; 4], u32)> = Vec::new();
2157        let mut hover: Option<usize> = None;
2158        for &(k, y) in &vis {
2159            let lay = st.lc.lays.get(&k).cloned();
2160            if let Some(lay) = lay {
2161                self.md_draw_block(&st.doc, &mut st.lc, st.sel, k, &lay, cx, vy + y, cw, drtl, focused, bgc, (vy, vy + view_h), &mut links, &mut fields);
2162            }
2163        }
2164        // the focused link: a frame around its pieces
2165        if let (Some(f), true) = (st.link, focused && self.wp.ctx.focus_visible) {
2166            for l in &links {
2167                if l.spot == f {
2168                    let st_ = self.style;
2169                    let lr = l.r;
2170                    st_.focus_ring(self, [lr[0] - self.dp(1), lr[1], lr[2] + self.dp(2), lr[3]], 2);
2171                }
2172            }
2173        }
2174        if copy {
2175            self.scroll_copy_end();
2176        }
2177        self.ltr_end();
2178        // links: a hand over them, a record each
2179        for l in &links {
2180            let lr = l.logical;
2181            self.wp.ctx.last_rect = lr;
2182            let before = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
2183            self.hit(id, hb & ((1 << HB_CURSOR_SHIFT) - 1) | CUR_HAND << HB_CURSOR_SHIFT);
2184            if let Some(h) = self.wp.ctx.hits[self.wp.ctx.hit_cur].get_mut(before) {
2185                h.continuation = true;
2186            }
2187            let (pmx, pmy) = (self.wp.ctx.mx, self.wp.ctx.my);
2188            if ev & EV_HOVER != 0 && pmx >= lr[0] && pmx < lr[0] + lr[2] && pmy >= lr[1] && pmy < lr[1] + lr[3] {
2189                hover = Some(l.spot);
2190            }
2191            self.wp.ctx.last_state = if st.link == Some(l.spot) && focused { SS_FOCUSED as u32 } else { 0 };
2192            self.sem(K_LINK, 0, Some(&l.label), 0);
2193        }
2194        // the fields in view: widgets over the text
2195        self.md_fields(st, id, &fields, disabled, view_h, cw, drtl, &fc, &mut out);
2196        self.clip_pop();
2197        if let Some(k) = hover {
2198            if let Some(sp) = st.doc.link_spots().get(k) {
2199                out.hover = Some(String::from(st.doc.link(sp.link)));
2200            }
2201        }
2202        // ---- the scroll bar
2203        let total = st.rows.total().min(i32::MAX as i64) as i32;
2204        if total > view_h {
2205            // a dragged thumb moves the view when the pointer moves, not when blocks laid out in
2206            // this pass change the total (that would chase the thumb pass after pass): while
2207            // the pointer stays, the bar compares with the position it reported last
2208            let sid = part_id(id, 1);
2209            let c = &self.wp.ctx;
2210            let (py, press) = (c.my, c.pressed & (1 << BTN_LEFT) != 0);
2211            let held = c.active == sid && c.mbtn & (1 << BTN_LEFT) != 0;
2212            let mut pos = if held && !press && py == st.sb_y { st.sb_pos } else { st.rows.pos().clamp(0, i32::MAX as i64) as i32 };
2213            self.next_rect_px(vx + vw - sbw, vy, sbw, vh);
2214            let sev = self.scrollbar(sid, &mut pos, total, view_h, true);
2215            if sev & EV_ACT != 0 && (press || py != st.sb_y) {
2216                st.rows.set_pos(pos as i64);
2217                st.rows.clamp(view_h);
2218                st.pending = 0;
2219                st.reveal = None;
2220                st.follow = false;
2221                self.repass();
2222            }
2223            st.sb_y = py;
2224            st.sb_pos = pos;
2225        }
2226        // ---- the wheel (over the text or one of its fields)
2227        let wy = self.wp.ctx.wheel_y;
2228        let over = ev & EV_HOVER != 0 || field_of(id, self.wp.ctx.hot, st.doc.fields().len()).is_some();
2229        if over && wy != 0 {
2230            let d = wy.wrapping_neg().wrapping_mul(self.dp(WHEEL_DIP)) / 120;
2231            let max = (st.rows.total() - view_h as i64).max(0);
2232            let p = st.rows.pos();
2233            if (d < 0 && p > 0) || (d > 0 && p < max) {
2234                if st.pending == 0 {
2235                    st.t0 = self.now_ms as i32;
2236                }
2237                st.pending += d;
2238                st.reveal = None;
2239                self.wp.ctx.wheel_y = 0;
2240                if self.anim_off {
2241                    self.repass();
2242                } else {
2243                    self.animating = true;
2244                }
2245            }
2246        }
2247        // a finger may roll it (touch.rs)
2248        let max = (st.rows.total() - view_h as i64).clamp(0, i32::MAX as i64) as i32;
2249        let pos = st.rows.pos().clamp(0, i32::MAX as i64) as i32;
2250        self.pan_view(id, hit0, super::touch::AX_V, [0, pos], [0, max]);
2251        // ---- the focus visual and the record
2252        self.wp.ctx.last_rect = r;
2253        if self.focus_ring(ev) && st.link.is_none() {
2254            self.focus_ring_draw(ev);
2255        }
2256        let mut s = 0;
2257        if ev & EV_HOVER != 0 {
2258            s |= SS_HOVER as u32;
2259        }
2260        if focused {
2261            s |= SS_FOCUSED as u32;
2262        }
2263        if disabled {
2264            s |= SS_DISABLED as u32;
2265        }
2266        self.wp.ctx.last_state = s;
2267        let label = if st.label.is_empty() { String::from(tr("Dokument")) } else { st.label.clone() };
2268        let pos = self.px2dip(st.rows.pos().min(i32::MAX as i64) as i32);
2269        self.sem(K_BOX, id, Some(&label), pos);
2270        // layouts far from the view go
2271        if st.lc.lays.len() > LAYS_KEEP {
2272            let top = st.rows.top;
2273            st.lc.lays.retain(|&k, _| k + LAYS_KEEP / 2 >= top && k <= top + LAYS_KEEP / 2);
2274        }
2275        self.wp.ctx.last_rect = r;
2276        out
2277    }
2278
2279    // the view's background colour (opaque)
2280    fn md_bg(&self, frame: bool) -> u32 {
2281        let bg = self.color(T_BG) | 0xFF00_0000;
2282        if !frame {
2283            return bg;
2284        }
2285        let c = self.color(T_CARD);
2286        let a = (c >> 24) as i32;
2287        color_mix(bg, c | 0xFF00_0000, a + (a >> 7))
2288    }
2289
2290    // the blocks in view, laid out: (block, top px from the view's top); the anchor is clamped
2291    // to the end and a range to reveal is brought into view (until the layouts settle)
2292    fn md_walk(&mut self, st: &mut MdViewState, view_h: i32, cw: i32, drtl: bool, fc: &Faces) -> Vec<(usize, i32)> {
2293        let n = st.doc.blocks().len();
2294        let mut vis = Vec::new();
2295        if n == 0 {
2296            return vis;
2297        }
2298        if st.reveal.is_some() {
2299            st.follow = false;
2300        }
2301        for _ in 0..6 {
2302            if st.follow {
2303                st.rows.set_pos(i64::MAX / 4);
2304            }
2305            if let Some((b, y0, y1)) = st.reveal {
2306                let b = b.min(n - 1);
2307                // (laid out: its height is known)
2308                self.md_lay(st, b, cw, drtl, fc);
2309                let m = self.dp(8);
2310                let top = st.rows.prefix(b) - st.rows.pos();
2311                if top + (y0 as i64) < 0 || (y1 - y0) >= view_h {
2312                    st.rows.set_pos(st.rows.prefix(b) + (y0 - m).max(0) as i64);
2313                } else if top + y1 as i64 > view_h as i64 {
2314                    st.rows.set_pos(st.rows.prefix(b) + (y1 + m - view_h) as i64);
2315                }
2316            }
2317            st.rows.clamp(view_h);
2318            vis.clear();
2319            let (t0, o0) = (st.rows.top, st.rows.off);
2320            let mut k = st.rows.top;
2321            let mut y = -st.rows.off;
2322            while k < n && y < view_h {
2323                let lay = self.md_lay(st, k, cw, drtl, fc);
2324                vis.push((k, y));
2325                y += lay.h;
2326                k += 1;
2327            }
2328            st.rows.clamp(view_h);
2329            let shown = match st.reveal {
2330                Some((b, y0, y1)) => vis.iter().find(|v| v.0 == b).is_some_and(|v| v.1 + y0 >= 0 && v.1 + y1 <= view_h),
2331                None => true,
2332            };
2333            if shown {
2334                st.reveal = None;
2335            }
2336            if (st.rows.top, st.rows.off) == (t0, o0) && shown {
2337                break;
2338            }
2339        }
2340        st.reveal = None;
2341        // at the end of a document longer than the view: stay there while it grows
2342        let total = st.rows.total();
2343        if total > view_h as i64 {
2344            st.follow = st.rows.pos() + view_h as i64 >= total - 1;
2345        }
2346        vis
2347    }
2348
2349    // the text position at (x, y): x from the column's start, y from the view's top
2350    fn md_hit(&self, st: &MdViewState, vis: &[(usize, i32)], x: i32, y: i32, cw: i32, drtl: bool) -> Option<TextPos> {
2351        let &(k, top) = vis.iter().rev().find(|v| v.1 <= y).or(vis.first())?;
2352        let lay = st.lc.lays.get(&k)?;
2353        let ly = y - top - lay.gap;
2354        if lay.boxes.is_empty() {
2355            return Some(TextPos::new(k, 0));
2356        }
2357        // the text area under x (else the nearest)
2358        let bx = |b: &FBox| box_x(0, cw, b.x, b.w, drtl);
2359        let fb = lay
2360            .boxes
2361            .iter()
2362            .min_by_key(|b| {
2363                let l = bx(b);
2364                if x < l { l - x } else if x >= l + b.w { x - (l + b.w) + 1 } else { 0 }
2365            })?;
2366        let fy = ly - fb.y;
2367        let row = fb.flow.rows.iter().rposition(|r| r.y <= fy).unwrap_or(0);
2368        let r = fb.flow.rows.get(row)?;
2369        if fy >= fb.flow.h && k + 1 < st.doc.blocks().len() && ly >= lay.ch {
2370            return Some(TextPos::new(k, r.b as usize));
2371        }
2372        Some(TextPos::new(k, r.hit(x - bx(fb)) as usize))
2373    }
2374
2375    // the element under the pointer (x, y as for md_hit): the one around the character there, else
2376    // that block's, beside the blocks the document's root
2377    fn md_element_at(&self, st: &MdViewState, vis: &[(usize, i32)], x: i32, y: i32, cw: i32, drtl: bool) -> MdNode {
2378        let root = st.doc.root();
2379        let Some(&(k, top)) = vis.iter().rev().find(|v| v.1 <= y) else { return root };
2380        let Some(lay) = st.lc.lays.get(&k) else { return root };
2381        let ly = y - top - lay.gap;
2382        if ly < 0 || ly >= lay.ch {
2383            return root;
2384        }
2385        let block = st.doc.blocks().get(k).map_or(root, |b| b.node);
2386        for fb in &lay.boxes {
2387            let x0 = box_x(0, cw, fb.x, fb.w, drtl);
2388            let px = x - x0;
2389            for r in &fb.flow.rows {
2390                let ry = fb.y + r.y;
2391                if ly < ry || ly >= ry + r.h {
2392                    continue;
2393                }
2394                for p in &r.pieces {
2395                    if px >= p.x && px < p.x + p.w {
2396                        // the stop that starts the character under the pointer (the stops of a
2397                        // piece that runs right to left stand at the characters' right edges)
2398                        let rtl = r.stops.iter().filter(|s| s.0 >= p.a && s.0 <= p.b).map(|s| (s.0, s.1)).min_by_key(|s| s.0).is_some_and(|s| s.1 > p.x);
2399                        let at = r.stops.iter().filter(|s| s.0 >= p.a && s.0 < p.b && if rtl { s.1 >= px } else { s.1 <= px });
2400                        let hit = if rtl { at.min_by_key(|s| s.1) } else { at.max_by_key(|s| s.1) };
2401                        return hit.map_or(block, |s| st.doc.element_at(k, s.0 as usize));
2402                    }
2403                }
2404            }
2405        }
2406        block
2407    }
2408
2409    // the link spot under the pointer (window px)
2410    #[allow(clippy::too_many_arguments)]
2411    fn md_link_at(&self, st: &MdViewState, vis: &[(usize, i32)], mx: i32, my: i32, cx: i32, vy: i32, cw: i32, drtl: bool) -> Option<usize> {
2412        let y = my - vy;
2413        let &(k, top) = vis.iter().rev().find(|v| v.1 <= y)?;
2414        let lay = st.lc.lays.get(&k)?;
2415        let ly = y - top - lay.gap;
2416        for fb in &lay.boxes {
2417            let x0 = box_x(cx, cw, fb.x, fb.w, drtl);
2418            for r in &fb.flow.rows {
2419                let ry = fb.y + r.y;
2420                if ly < ry || ly >= ry + r.h {
2421                    continue;
2422                }
2423                for p in &r.pieces {
2424                    if p.link != 0 && mx >= x0 + p.x && mx < x0 + p.x + p.w {
2425                        return spot_of(st.doc.link_spots(), k, |s| s.link == p.link && s.a <= p.a && p.b <= s.b);
2426                    }
2427                }
2428            }
2429        }
2430        None
2431    }
2432
2433    // the keys of a focused view -> a link to activate
2434    #[allow(clippy::too_many_arguments)]
2435    fn md_keys(&mut self, st: &mut MdViewState, id: u32, view_h: i32, cw: i32, drtl: bool, fc: &Faces) -> Option<usize> {
2436        let find = |ui: &Ui, vk: u32, need: u32, mask: u32| -> Option<usize> {
2437            ui.wp.ctx.keyq.iter().position(|&k| (k as i32) >= 0 && k & 0xFFFF == vk && (k >> 16) & mask == need)
2438        };
2439        let take = |ui: &mut Ui, i: usize| {
2440            if let Some(k) = ui.wp.ctx.keyq.get_mut(i) {
2441                *k |= 0x8000_0000;
2442            }
2443        };
2444        const M: u32 = MOD_SHIFT | MOD_CTRL | MOD_ALT;
2445        let line = self.dp(LINE_DIP);
2446        let page = (view_h - line).max(line);
2447        let mut d: i64 = 0;
2448        let mut act = None;
2449        if let Some(i) = find(self, VK_UP, 0, M) {
2450            take(self, i);
2451            d -= line as i64;
2452        }
2453        if let Some(i) = find(self, VK_DOWN, 0, M) {
2454            take(self, i);
2455            d += line as i64;
2456        }
2457        if let Some(i) = find(self, VK_PRIOR, 0, M & !MOD_SHIFT) {
2458            take(self, i);
2459            d -= page as i64;
2460        }
2461        if let Some(i) = find(self, VK_NEXT, 0, M & !MOD_SHIFT) {
2462            take(self, i);
2463            d += page as i64;
2464        }
2465        if let Some(i) = find(self, VK_SPACE, 0, MOD_CTRL | MOD_ALT) {
2466            let shift = (self.wp.ctx.keyq[i] >> 16) & MOD_SHIFT != 0;
2467            take(self, i);
2468            d += if shift { -(page as i64) } else { page as i64 };
2469        }
2470        let mut home = false;
2471        let mut end = false;
2472        if let Some(i) = find(self, VK_HOME, 0, MOD_SHIFT | MOD_ALT) {
2473            take(self, i);
2474            home = true;
2475        }
2476        if let Some(i) = find(self, VK_END, 0, MOD_SHIFT | MOD_ALT) {
2477            take(self, i);
2478            end = true;
2479        }
2480        if find(self, b'A' as u32, MOD_CTRL, M).map(|i| take(self, i)).is_some() {
2481            st.select_all();
2482        }
2483        if find(self, b'C' as u32, MOD_CTRL, M).or_else(|| find(self, VK_INSERT, MOD_CTRL, M)).map(|i| take(self, i)).is_some() && !st.sel.is_empty() {
2484            let (t, h) = (st.selected_text(), st.selected_html());
2485            self.clip_set_many(&[(CLIP_TEXT, t.as_bytes()), (CLIP_HTML, h.as_bytes())]);
2486        }
2487        // Tab / Shift+Tab: the next / previous link or field (past the ends the focus moves on)
2488        if let Some(i) = find(self, VK_TAB, 0, MOD_CTRL | MOD_ALT) {
2489            let back = (self.wp.ctx.keyq[i] >> 16) & MOD_SHIFT != 0;
2490            let cur = st.link.and_then(|k| st.stops.iter().position(|&s| s == Stop::Link(k)));
2491            match st.step(cur, back) {
2492                Some(Stop::Link(k)) => {
2493                    take(self, i);
2494                    st.link = Some(k);
2495                    self.wp.ctx.focus_visible = true;
2496                    self.md_reveal_link(st, k, cw, drtl, fc);
2497                    self.repass();
2498                }
2499                Some(Stop::Field(f)) => {
2500                    take(self, i);
2501                    st.link = None;
2502                    self.md_focus_field(st, id, f, cw, drtl, fc);
2503                }
2504                None if st.link.is_some() && back => {
2505                    take(self, i);
2506                    st.link = None;
2507                    self.repass();
2508                }
2509                None => st.link = None,
2510            }
2511        }
2512        if let Some(k) = st.link {
2513            if let Some(i) = find(self, VK_RETURN, 0, M) {
2514                take(self, i);
2515                act = Some(k);
2516            }
2517            if let Some(i) = find(self, VK_ESCAPE, 0, M) {
2518                take(self, i);
2519                st.link = None;
2520                self.repass();
2521            }
2522        }
2523        if home {
2524            st.rows.set_pos(0);
2525            st.pending = 0;
2526            st.follow = false;
2527        }
2528        if end {
2529            st.pending = 0;
2530            st.follow = true;
2531        }
2532        if d != 0 {
2533            st.rows.set_pos((st.rows.pos() + d).max(0));
2534            st.pending = 0;
2535            st.follow = false;
2536        }
2537        if home || end || d != 0 {
2538            st.reveal = None;
2539            self.wp.ctx.focus_visible = true;
2540            self.repass();
2541        }
2542        act
2543    }
2544
2545    // give field f the keyboard focus (its ring shown) and scroll it into view
2546    fn md_focus_field(&mut self, st: &mut MdViewState, id: u32, f: usize, cw: i32, drtl: bool, fc: &Faces) {
2547        self.wp.ctx.focus = md_field_id(id, f);
2548        self.wp.ctx.focus_visible = true;
2549        st.ffocus = Some(f);
2550        self.repass();
2551        let Some(sp) = st.doc.field_spots().iter().find(|s| s.field as usize == f + 1).copied() else { return };
2552        let lay = self.md_lay(st, sp.block, cw, drtl, fc);
2553        let (mut y0, mut y1) = (i32::MAX, 0);
2554        for fb in &lay.boxes {
2555            for r in &fb.flow.rows {
2556                if r.pieces.iter().any(|p| p.field == sp.field) {
2557                    y0 = y0.min(lay.gap + fb.y + r.y);
2558                    y1 = y1.max(lay.gap + fb.y + r.y + r.h);
2559                }
2560            }
2561        }
2562        if y0 <= y1 {
2563            st.reveal = Some((sp.block, y0, y1));
2564        }
2565    }
2566
2567    // an unconsumed key vk of this pass with none of the modifiers in `mask` -> its place in
2568    // the queue
2569    fn md_key_at(&self, vk: u32, mask: u32) -> Option<usize> {
2570        self.wp.ctx.keyq.iter().position(|&k| (k as i32) >= 0 && k & 0xFFFF == vk && (k >> 16) & mask == 0)
2571    }
2572
2573    // consume the key at place i of the queue
2574    fn md_key_take(&mut self, i: usize) {
2575        if let Some(k) = self.wp.ctx.keyq.get_mut(i) {
2576            *k |= 0x8000_0000;
2577        }
2578    }
2579
2580    // a check box, radio or button of a form at r (logical px): behave, the style's picture (a
2581    // box alone: its text is the document's), the focus ring, the record (label: the field's
2582    // name, a button's caption)
2583    fn md_ctl(&mut self, fid: u32, kind: u8, r: [i32; 4], label: &str, on: bool, disabled: bool) -> u32 {
2584        self.wp.ctx.last_rect = r;
2585        let button = !matches!(kind, MF_CHECKBOX | MF_RADIO);
2586        let mut hb = HB_FOCUS | if button { 0 } else { HB_SPACE };
2587        if disabled {
2588            hb |= HB_DISABLED;
2589        }
2590        let ev = self.behave(fid, hb);
2591        let st = self.style;
2592        let mut sp = Sp { flags: if disabled { ST_DISABLED } else { 0 }, label: button.then_some(label), ..Sp::default() };
2593        match kind {
2594            MF_CHECKBOX => st.checkbox_size(self, &mut sp),
2595            MF_RADIO => st.radio_size(self, &mut sp),
2596            _ => st.button_size(self, &mut sp),
2597        };
2598        self.sp_placed(&mut sp, ev);
2599        sp.value = on as i32;
2600        match kind {
2601            MF_CHECKBOX => st.checkbox(self, &sp),
2602            MF_RADIO => st.radio(self, &sp),
2603            _ => st.button(self, &sp),
2604        }
2605        self.focus_ring_draw(ev);
2606        if on {
2607            self.wp.ctx.last_state |= SS_CHECKED as u32;
2608        }
2609        let k = match kind {
2610            MF_CHECKBOX => K_CHECKBOX,
2611            MF_RADIO => K_RADIO,
2612            _ => K_BUTTON,
2613        };
2614        self.sem(k, fid, Some(label), on as i32);
2615        ev
2616    }
2617
2618    // the fields in view: the library's widgets at the rectangles of the layout (logical px;
2619    // off: the view is disabled, so are they), their values in the state; Enter sends the
2620    // form, the arrows choose a radio, Tab leaves a field for the next / previous link or field;
2621    // what the user did goes to `out`
2622    #[allow(clippy::too_many_arguments)]
2623    fn md_fields(&mut self, st: &mut MdViewState, id: u32, drawn: &[(usize, [i32; 4], u32)], off: bool, view_h: i32, cw: i32, drtl: bool, fc: &Faces, out: &mut MdEv) {
2624        let n = st.doc.fields().len();
2625        if n == 0 {
2626            return;
2627        }
2628        const M: u32 = MOD_SHIFT | MOD_CTRL | MOD_ALT;
2629        let mut changed: Option<usize> = None;
2630        let mut submit: Option<MdSubmit> = None;
2631        let mut reset: Option<usize> = None;
2632        let mut moved: Option<usize> = None;
2633        for &(f, r, under) in drawn {
2634            if under != 0 {
2635                self.draw_color(under);
2636                self.draw_rect(r[0], r[1], r[2], r[3]);
2637            }
2638            let fid = md_field_id(id, f);
2639            let focused = self.wp.ctx.focus == fid;
2640            let h0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
2641            let fd = &st.doc.fields()[f];
2642            let (kind, form, disabled, maxlength) = (fd.kind, fd.form as usize, fd.disabled || off, fd.maxlength as usize);
2643            match kind {
2644                MF_TEXT | MF_PASSWORD | MF_TEXTAREA | MF_SELECT => {
2645                    // the name: a record without area along the top edge (text boxes and lists
2646                    // take the text record right above them as their name)
2647                    let name = field_name(fd);
2648                    if !name.is_empty() {
2649                        self.wp.ctx.last_rect = [r[0], r[1], r[2], 0];
2650                        self.wp.ctx.last_state = 0;
2651                        self.sem(K_TEXT, 0, Some(name), 0);
2652                    }
2653                    let enter = if focused && kind != MF_TEXTAREA && kind != MF_SELECT { self.md_key_at(VK_RETURN, 0) } else { None };
2654                    let esc = focused && kind != MF_SELECT && self.md_key_at(VK_ESCAPE, M).is_some();
2655                    if disabled {
2656                        self.next_flags(ST_DISABLED);
2657                    }
2658                    self.next_rect_px(r[0], r[1], r[2], r[3]);
2659                    let ev = if kind == MF_SELECT {
2660                        let opts: Vec<&str> = fd.options.iter().map(|o| if o.label.is_empty() { " " } else { o.label.as_str() }).collect();
2661                        self.combobox(fid, &mut st.vals[f].sel, &opts)
2662                    } else {
2663                        let mut flags = match kind {
2664                            MF_PASSWORD => TB_PASSWORD,
2665                            MF_TEXTAREA => TB_MULTI | TB_WRAP,
2666                            _ => 0,
2667                        };
2668                        if fd.readonly {
2669                            flags |= TB_READONLY;
2670                        }
2671                        let cap = if maxlength > 0 { (maxlength as u32 * 4 + 1).min(FIELD_CAP) } else { FIELD_CAP };
2672                        let ph = (!fd.placeholder.is_empty()).then_some(fd.placeholder.as_str());
2673                        self.textbox(fid, ph, &mut st.vals[f].text, cap | flags)
2674                    };
2675                    if ev & EV_ACT != 0 {
2676                        st.vals[f].dirty = true;
2677                        // (maxlength counts characters)
2678                        let t = &mut st.vals[f].text;
2679                        if maxlength > 0 && t.chars().count() > maxlength {
2680                            let cut = t.char_indices().nth(maxlength).map_or(t.len(), |c| c.0);
2681                            t.truncate(cut);
2682                        }
2683                        changed = Some(f);
2684                    }
2685                    if enter.is_some() && ev & EV_COMMIT != 0 {
2686                        submit = st.implicit(f);
2687                    }
2688                    if esc && self.wp.ctx.focus == 0 {
2689                        // Esc in a text field: back to the view
2690                        self.wp.ctx.focus = id;
2691                        self.wp.ctx.focus_visible = true;
2692                    }
2693                }
2694                _ => {
2695                    let label = if matches!(kind, MF_CHECKBOX | MF_RADIO) { field_name(fd) } else { fd.caption() };
2696                    let on = st.vals[f].on;
2697                    let ev = self.md_ctl(fid, kind, r, label, on, disabled);
2698                    match kind {
2699                        MF_CHECKBOX | MF_RADIO => {
2700                            if focused {
2701                                if let Some(i) = self.md_key_at(VK_RETURN, M) {
2702                                    self.md_key_take(i);
2703                                    submit = st.implicit(f);
2704                                }
2705                            }
2706                            if focused && kind == MF_RADIO {
2707                                // Up / Down: the previous / next radio; Left / Right as they lie
2708                                // in the paragraph (its direction; the window's mirroring undone:
2709                                // the text keeps its order)
2710                                let rtl = st.doc.field_spots().iter().find(|s| s.field as usize == f + 1).and_then(|s| st.doc.blocks().get(s.block)).and_then(|b| text_rtl(&b.text)).unwrap_or(drtl);
2711                                for vk in [VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN] {
2712                                    if let Some(i) = self.md_key_at(vk, M) {
2713                                        let k = self.rtl_text_key(self.wp.ctx.keyq[i]) & 0xFFFF;
2714                                        self.md_key_take(i);
2715                                        let back = k == VK_UP || (k != VK_DOWN && ((k == VK_LEFT) != rtl));
2716                                        moved = st.radio_step(f, back);
2717                                        break;
2718                                    }
2719                                }
2720                            }
2721                            if ev & EV_ACT != 0 && (kind == MF_CHECKBOX || !on) {
2722                                st.set_field_checked(f, !on);
2723                                changed = Some(f);
2724                            }
2725                        }
2726                        MF_SUBMIT if ev & EV_ACT != 0 => match form.checked_sub(1) {
2727                            Some(fm) => submit = Some(st.submit(fm, Some(f))),
2728                            None => changed = Some(f),
2729                        },
2730                        MF_RESET if ev & EV_ACT != 0 => {
2731                            reset = form.checked_sub(1);
2732                            changed = Some(f);
2733                        }
2734                        _ if ev & EV_ACT != 0 => changed = Some(f),
2735                        _ => {}
2736                    }
2737                }
2738            }
2739            // (no Tab stop of the window: the view steps through its links and fields)
2740            let c = &mut self.wp.ctx;
2741            for h in c.hits[c.hit_cur][h0..].iter_mut() {
2742                if h.id == fid {
2743                    h.continuation = true;
2744                }
2745            }
2746        }
2747        if let Some(fm) = reset {
2748            st.reset_form(fm);
2749            self.repass();
2750        }
2751        if let Some(g) = moved {
2752            st.set_field_checked(g, true);
2753            self.md_focus_field(st, id, g, cw, drtl, fc);
2754            changed = Some(g);
2755        }
2756        // Tab / Shift+Tab from a field: the next / previous link or field; past the last one the
2757        // focus moves on from the view, before the first one it stays on the view
2758        if let Some(f) = field_of(id, self.wp.ctx.focus, n) {
2759            if let Some(i) = self.md_key_at(VK_TAB, MOD_CTRL | MOD_ALT) {
2760                let back = (self.wp.ctx.keyq[i] >> 16) & MOD_SHIFT != 0;
2761                let cur = st.stops.iter().position(|&s| s == Stop::Field(f));
2762                match st.step(cur, back) {
2763                    Some(Stop::Field(g)) => {
2764                        self.md_key_take(i);
2765                        self.md_focus_field(st, id, g, cw, drtl, fc);
2766                    }
2767                    s => {
2768                        st.link = None;
2769                        if let Some(Stop::Link(k)) = s {
2770                            st.link = Some(k);
2771                            self.md_reveal_link(st, k, cw, drtl, fc);
2772                        }
2773                        if s.is_some() || back {
2774                            self.md_key_take(i);
2775                        }
2776                        self.wp.ctx.focus = id;
2777                        self.wp.ctx.focus_visible = true;
2778                        self.repass();
2779                    }
2780                }
2781            }
2782        }
2783        // PgUp / PgDn on a field the field does not take: the view scrolls by a page (the field
2784        // keeps the focus)
2785        if field_of(id, self.wp.ctx.focus, n).is_some() {
2786            for (vk, dir) in [(VK_PRIOR, -1i64), (VK_NEXT, 1)] {
2787                if let Some(i) = self.md_key_at(vk, MOD_CTRL | MOD_ALT) {
2788                    self.md_key_take(i);
2789                    let line = self.dp(LINE_DIP);
2790                    st.rows.set_pos((st.rows.pos() + dir * (view_h - line).max(line) as i64).max(0));
2791                    st.pending = 0;
2792                    st.reveal = None;
2793                    st.follow = false;
2794                    self.repass();
2795                }
2796            }
2797        }
2798        st.ffocus = field_of(id, self.wp.ctx.focus, n);
2799        if changed.is_some() || submit.is_some() {
2800            self.repass();
2801        }
2802        out.field = changed;
2803        out.submit = submit;
2804    }
2805
2806    // scroll link spot k into view
2807    fn md_reveal_link(&mut self, st: &mut MdViewState, k: usize, cw: i32, drtl: bool, fc: &Faces) {
2808        let Some(sp) = st.doc.link_spots().get(k).copied() else { return };
2809        let lay = self.md_lay(st, sp.block, cw, drtl, fc);
2810        let (mut y0, mut y1) = (i32::MAX, 0);
2811        for fb in &lay.boxes {
2812            for r in &fb.flow.rows {
2813                if r.pieces.iter().any(|p| p.link == sp.link && p.a < sp.b && p.b > sp.a) {
2814                    y0 = y0.min(lay.gap + fb.y + r.y);
2815                    y1 = y1.max(lay.gap + fb.y + r.y + r.h);
2816                }
2817            }
2818        }
2819        if y0 <= y1 {
2820            st.reveal = Some((sp.block, y0, y1));
2821        }
2822    }
2823
2824    // ------------------------------------------------------------------ drawing
2825
2826    #[allow(clippy::too_many_arguments)]
2827    // (rows outside vis: y0 .. y1 are skipped -- a block may be far taller than the view; the
2828    // fields go to `fields` with their logical rectangles: md_fields builds their widgets)
2829    fn md_draw_block(&mut self, doc: &MdDoc, lc: &mut Cache, sel: TextSel, k: usize, lay: &BLay, cx: i32, top: i32, cw: i32, drtl: bool, focused: bool, bgc: u32, vis: (i32, i32), links: &mut Vec<Link>, fields: &mut Vec<(usize, [i32; 4], u32)>) {
2830        let hc = self.high_contrast;
2831        let bl = &doc.blocks()[k];
2832        let ctop = top + lay.gap;
2833        let mut tcol = self.color(T_TEXT) | 0xFF00_0000;
2834        let mut t2 = self.color(T_TEXT2) | 0xFF00_0000;
2835        let mut lcol = self.color(T_ACCENT_TEXT) | 0xFF00_0000;
2836        let bgc0 = bgc;
2837        let mut bgc = bgc;
2838        // the boxes of CSS: backgrounds (not in high contrast), borders (high contrast: in the
2839        // text's colour); on a background the text takes a colour that reads on it
2840        for g in &lay.css {
2841            let Some(b) = doc.boxes().get(g.b as usize) else { continue };
2842            let (x, y0, hh) = (box_x(cx, cw, g.x, g.w, drtl), top + g.y0, g.y1 - g.y0);
2843            if hh <= 0 {
2844                continue;
2845            }
2846            if b.bg != 0 && !hc {
2847                self.draw_color(b.bg);
2848                self.draw_rect(x, y0, g.w, hh);
2849                bgc = color_mix(bgc, b.bg | 0xFF00_0000, ((b.bg >> 24) as i32) + ((b.bg >> 31) as i32));
2850            }
2851            let bw = b.border.map(|v| self.dp(v as i32));
2852            let col = |s: usize| if hc || b.border_color[s] == 0 { tcol } else { b.border_color[s] };
2853            let edge = |ui: &mut Ui, s: usize, r: [i32; 4]| {
2854                if bw[s] > 0 && r[3] > 0 {
2855                    ui.draw_color(col(s));
2856                    ui.draw_rect(r[0], r[1], r[2], r[3]);
2857                }
2858            };
2859            edge(self, 3, [x, y0, bw[3], hh]);
2860            edge(self, 1, [x + g.w - bw[1], y0, bw[1], hh]);
2861            if g.first {
2862                edge(self, 0, [x, y0, g.w, bw[0]]);
2863            }
2864            if g.last {
2865                edge(self, 2, [x, top + g.y1 - bw[2], g.w, bw[2]]);
2866            }
2867        }
2868        // (fields on a background lighter or darker than the theme's: the theme's below them)
2869        let mut under = 0;
2870        if bgc != bgc0 && !hc {
2871            let theme_light = (tcol & 0xFF) < 0x80;
2872            (tcol, t2, lcol) = on_bg(bgc);
2873            if ((tcol & 0xFF) < 0x80) != theme_light {
2874                under = bgc0;
2875            }
2876        }
2877        let div = if hc { tcol } else { color_mix(bgc, tcol, 48) };
2878        let tint = color_mix(bgc, tcol, if self.theme_dark { 22 } else { 14 });
2879        let hair = self.hair().max(1);
2880        // quote bars (through the gap when the block before is quoted as deep)
2881        let prev_q = if k > 0 { doc.blocks()[k - 1].quote } else { 0 };
2882        let mut qn = 0;
2883        for &bx in &lay.bars {
2884            qn += 1;
2885            let bw = self.dp(3);
2886            let y0 = if prev_q >= qn && k > 0 { top } else { ctop };
2887            let x = box_x(cx, cw, bx, bw, drtl);
2888            self.draw_color(if hc { tcol } else { color_mix(bgc, tcol, 70) });
2889            self.draw_rect(x, y0, bw, ctop + lay.ch - y0);
2890        }
2891        let colx = box_x(cx, cw, lay.x, lay.w, drtl);
2892        match bl.kind {
2893            MB_CODE => {
2894                if hc {
2895                    self.draw_color(tcol);
2896                    self.draw_frame(colx, ctop, lay.w, lay.ch, self.dp(4), hair);
2897                } else {
2898                    let th = self.code_theme();
2899                    let fill = if th.bg != 0 { color_mix(bgc, tcol, if self.theme_dark { 18 } else { 10 }) } else { tint };
2900                    self.cmd_rrect(colx, ctop, lay.w, lay.ch, self.dp(6), 0, fill, 0, 0);
2901                }
2902            }
2903            MB_RULE => {
2904                let t = self.dp(2).max(1);
2905                self.draw_color(div);
2906                self.draw_rect(colx, ctop + lay.ch / 2 - t / 2, lay.w, t);
2907            }
2908            MB_ROW => {
2909                // cell frames, the header's fill
2910                let px = self.dp_text(CELL_PX);
2911                for fb in &lay.boxes {
2912                    let x = box_x(cx, cw, fb.x - px, fb.w + px * 2, drtl);
2913                    if bl.head && !hc {
2914                        self.draw_color(tint);
2915                        self.draw_rect(x, ctop, fb.w + px * 2, lay.ch);
2916                    }
2917                    self.draw_color(div);
2918                    self.draw_frame(x, ctop, fb.w + px * 2 + hair, lay.ch + hair, 0, hair);
2919                }
2920            }
2921            // (a heading whose CSS gives it a border below has no line of its own)
2922            MB_HEADING if bl.level <= 2 && !lay.css.iter().any(|g| g.last && doc.boxes().get(g.b as usize).is_some_and(|b| b.node == bl.node && b.border[2] > 0)) => {
2923                let fh = lay.boxes.first().map_or(0, |b| b.flow.h);
2924                self.draw_color(div);
2925                self.draw_rect(colx, ctop + fh + self.dp(6), lay.w, hair);
2926            }
2927            _ => {}
2928        }
2929        // the selection under the text
2930        let (sa, sb) = sel.range();
2931        let selc = if focused { (self.color(T_ACCENT) & 0x00FF_FFFF) | 0x60 << 24 } else { (self.color(T_TEXT3) & 0x00FF_FFFF) | 0x38 << 24 };
2932        if sa < sb && sa.line <= k && sb.line >= k {
2933            let lo = if sa.line == k { sa.byte as u32 } else { 0 };
2934            let hi = if sb.line == k { sb.byte as u32 } else { u32::MAX };
2935            if bl.kind == MB_RULE {
2936                self.draw_color(selc);
2937                self.draw_rect(colx, ctop, lay.w, lay.ch);
2938            }
2939            for fb in &lay.boxes {
2940                let x0 = box_x(cx, cw, fb.x, fb.w, drtl);
2941                for r in &fb.flow.rows {
2942                    let ry = ctop + fb.y + r.y;
2943                    if ry + r.h <= vis.0 || ry >= vis.1 {
2944                        continue;
2945                    }
2946                    for p in &r.pieces {
2947                        let (a, b) = (p.a.max(lo), p.b.min(hi));
2948                        if a >= b {
2949                            continue;
2950                        }
2951                        let (mut xa, mut xb) = (i32::MAX, i32::MIN);
2952                        for s in &r.stops {
2953                            if s.0 >= a && s.0 <= b {
2954                                xa = xa.min(s.1);
2955                                xb = xb.max(s.1);
2956                            }
2957                        }
2958                        if xa < xb {
2959                            self.draw_color(selc);
2960                            self.draw_rect(x0 + xa, ctop + fb.y + r.y, xb - xa, r.h);
2961                        }
2962                    }
2963                }
2964            }
2965        }
2966        // the text
2967        let quote = bl.quote > 0;
2968        let theme = if bl.kind == MB_CODE { Some(self.code_theme()) } else { None };
2969        // the first row: the list marker's baseline and font
2970        let first_base: Option<(i32, i32, u32, i32, i32)> = lay.boxes.first().and_then(|fb| {
2971            let r = fb.flow.rows.first()?;
2972            let (f, s, a) = r.pieces.iter().find(|p| p.image == 0 && p.field == 0).map_or((0, 0, 0), |p| (p.font, p.size, p.asc));
2973            Some((ctop + fb.y + r.y + r.base, box_x(cx, cw, fb.x, fb.w, drtl), f, s, a))
2974        });
2975        for fb in &lay.boxes {
2976            let x0 = box_x(cx, cw, fb.x, fb.w, drtl);
2977            for r in &fb.flow.rows {
2978                let ry = ctop + fb.y + r.y;
2979                if ry + r.h <= vis.0 || ry >= vis.1 {
2980                    continue;
2981                }
2982                let base = ry + r.base;
2983                let mut prev_link: Option<([i32; 4], u32)> = None;
2984                for p in &r.pieces {
2985                    let x = x0 + p.x;
2986                    if p.image != 0 {
2987                        self.md_draw_image(lc, doc, p.image as usize - 1, x, base - p.ih, p.w, p.ih, tcol, bgc);
2988                        let alt = doc.images().get(p.image as usize - 1).map_or("", |m| m.alt.as_str());
2989                        self.wp.ctx.last_rect = self.ltr_rect([x, base - p.ih, p.w, p.ih]);
2990                        self.wp.ctx.last_state = 0;
2991                        let label = if alt.is_empty() { tr("Bild") } else { alt };
2992                        self.sem(K_IMAGE, 0, Some(label), 0);
2993                    } else if p.field != 0 {
2994                        // a field: its widget after the blocks, here (its row's gap above it)
2995                        let y = base - p.asc + self.dp(FIELD_GAP);
2996                        fields.push((p.field as usize - 1, self.ltr_rect([x, y, p.w, p.ih]), under));
2997                    } else {
2998                        let link = p.link != 0;
2999                        let col = if let Some(t) = &theme {
3000                            t.tokens.get(p.syn.saturating_sub(1) as usize).copied().unwrap_or(tcol)
3001                        } else if link && (p.color == 0 || hc) {
3002                            lcol
3003                        } else if p.color != 0 && !hc {
3004                            opaque(p.color)
3005                        } else if quote || (bl.kind == MB_HEADING && bl.level >= 6) {
3006                            t2
3007                        } else {
3008                            tcol
3009                        };
3010                        if p.bg != 0 && !hc {
3011                            self.draw_color(opaque(p.bg));
3012                            self.draw_rect(x, ry, p.w, r.h);
3013                        }
3014                        if p.style & MS_CODE != 0 && bl.kind != MB_CODE {
3015                            let (h, yy, cp) = (p.asc + self.dp(6), base - p.asc - self.dp(2), self.dp_text(CODE_PAD));
3016                            if hc {
3017                                self.draw_color(tcol);
3018                                self.draw_frame(x - cp, yy, p.w + cp * 2, h, self.dp(3), hair);
3019                            } else {
3020                                self.cmd_rrect(x - cp, yy, p.w + cp * 2, h, self.dp(3), 0, tint, 0, 0);
3021                            }
3022                        }
3023                        let s = &bl.text.as_bytes()[p.a as usize..p.b as usize];
3024                        let s = if s.last() == Some(&b'\n') { &s[..s.len() - 1] } else { s };
3025                        if p.shaped {
3026                            let d: DrawRef = Rc::new(Shaped { s: s.to_vec(), font: p.font, size: p.size, color: col, dx: self.dp(2), base: base - ry });
3027                            let ver = (text_hash(core::str::from_utf8(s).unwrap_or("")) as u32) ^ col ^ (p.size as u32).rotate_left(7) ^ p.font;
3028                            self.cmd_custom(x - self.dp(2), ry, p.w + self.dp(4), r.h, Some(d), 0, ver);
3029                        } else {
3030                            self.cmd_text(x, base, s, p.font, p.size, col, 0);
3031                        }
3032                        let px = p.size >> 8;
3033                        let tl = (px / 14).max(1);
3034                        if link || p.style & MS_UNDER != 0 {
3035                            self.draw_color(col);
3036                            self.draw_rect(x, base + (px / 9).max(1), p.w, tl);
3037                        }
3038                        if p.style & MS_STRIKE != 0 {
3039                            self.draw_color(col);
3040                            self.draw_rect(x, base - p.asc * 3 / 10, p.w, tl);
3041                        }
3042                    }
3043                    // links: one rectangle per row and link
3044                    if p.link != 0 {
3045                        let pr = [x, ry, p.w, r.h];
3046                        match &mut prev_link {
3047                            Some((lr, l)) if *l == p.link && (lr[0] + lr[2] == x || x + p.w == lr[0]) => {
3048                                let x0 = lr[0].min(x);
3049                                lr[2] = (lr[0] + lr[2]).max(x + p.w) - x0;
3050                                lr[0] = x0;
3051                            }
3052                            _ => {
3053                                if let Some((lr, l)) = prev_link.take() {
3054                                    self.md_link_rec(doc, k, l, lr, r, links);
3055                                }
3056                                prev_link = Some((pr, p.link));
3057                            }
3058                        }
3059                    } else if let Some((lr, l)) = prev_link.take() {
3060                        self.md_link_rec(doc, k, l, lr, r, links);
3061                    }
3062                }
3063                if let Some((lr, l)) = prev_link.take() {
3064                    self.md_link_rec(doc, k, l, lr, r, links);
3065                }
3066            }
3067        }
3068        // the list marker in front of the first row
3069        if bl.marker != MK_NONE {
3070            let (base, _, mf, ms, masc) = first_base.unwrap_or((ctop + self.dp(14), 0, 0, 0, self.dp(12)));
3071            let (mf, ms, masc) = if mf == 0 {
3072                let f = self.md_faces();
3073                let fnt = f.get(false, false, false);
3074                let (a, _, _) = self.md_metrics(fnt, f.size);
3075                (fnt, f.size, a)
3076            } else {
3077                (mf, ms, masc)
3078            };
3079            let gut = self.dp_text(LIST_W);
3080            let gx = box_x(cx, cw, lay.mx - gut, gut, drtl);
3081            let mcol = if quote { t2 } else { tcol };
3082            let label = bl.text.chars().take(LABEL_MAX).collect::<String>();
3083            if bl.task != 0 {
3084                let s = ((masc as f32 * 0.9) as i32).max(self.dp(10));
3085                let bx = if drtl { gx + self.dp_text(6) } else { gx + gut - s - self.dp_text(8) };
3086                let by = base - s + self.dp(1);
3087                let done = bl.task == 2;
3088                if done {
3089                    self.cmd_rrect(bx, by, s, s, self.dp(3), 0, self.color(T_ACCENT) | 0xFF00_0000, 0, 0);
3090                    let c = self.color(T_ON_ACCENT) | 0xFF00_0000;
3091                    self.draw_color(c);
3092                    let (fx, fy) = ((bx << 8) as i64, (by << 8) as i64);
3093                    let sz = (s << 8) as i64;
3094                    let w = (self.dp(2).max(1) << 8) * 3 / 4;
3095                    let p = |a: i64, b: i64| ((fx + sz * a / 100) as i32, (fy + sz * b / 100) as i32);
3096                    let (a0, a1) = p(24, 52);
3097                    let (b0, b1) = p(43, 70);
3098                    let (c0, c1) = p(76, 32);
3099                    self.draw_line(a0, a1, b0, b1, w);
3100                    self.draw_line(b0, b1, c0, c1, w);
3101                } else {
3102                    self.cmd_rrect(bx, by, s, s, self.dp(3), hair, 0, self.color(T_STRONG_STROKE) | 0xFF00_0000, self.color(T_STRONG_STROKE) | 0xFF00_0000);
3103                }
3104                self.wp.ctx.last_rect = self.ltr_rect([bx, by, s, s]);
3105                self.wp.ctx.last_state = if done { SS_CHECKED as u32 } else { 0 };
3106                self.sem(K_CHECKBOX, 0, Some(&label), done as i32);
3107            } else if bl.marker == MK_BULLET {
3108                let d = ((masc as f32 * 0.36) as i32).max(self.dp(4)) & !1;
3109                let mx = if drtl { gx + self.dp_text(12) } else { gx + gut - self.dp_text(12) };
3110                let my = base - (masc as f32 * 0.34) as i32;
3111                self.draw_color(mcol);
3112                match (bl.list.max(1) - 1) % 3 {
3113                    0 => self.draw_circle(mx << 8, my << 8, (d << 8) / 2, 0),
3114                    1 => self.draw_circle(mx << 8, my << 8, (d << 8) / 2 - 128, self.hair().max(1) << 8),
3115                    _ => self.draw_rect(mx - d / 2, my - d / 2, d, d),
3116                }
3117            } else {
3118                // (right to left the delimiter reads after the number: it stands on its left)
3119                let d = if bl.delim == b')' { '(' } else { '.' };
3120                let m = if drtl { format!("{}{}", d, bl.number) } else { format!("{}{}", bl.number, if bl.delim == b')' { ')' } else { '.' }) };
3121                self.text.set_font(mf, ms);
3122                let mw = self.text.width(&m);
3123                let x = if drtl { gx + self.dp_text(6) } else { gx + gut - self.dp_text(6) - mw };
3124                self.cmd_text(x, base, m.as_bytes(), mf, ms, mcol, 0);
3125            }
3126            // the list item's record
3127            let (lx, lw) = (lay.mx - gut, lay.x + lay.w - (lay.mx - gut));
3128            self.wp.ctx.last_rect = self.ltr_rect([box_x(cx, cw, lx, lw, drtl), ctop, lw, lay.ch]);
3129            self.wp.ctx.last_state = 0;
3130            self.sem(K_LISTITEM, 0, Some(&label), bl.number as i32);
3131        }
3132        // the block's record
3133        self.wp.ctx.last_rect = self.ltr_rect([colx, ctop, lay.w, lay.ch]);
3134        self.wp.ctx.last_state = 0;
3135        match bl.kind {
3136            MB_HEADING => {
3137                let label = doc.plain_range(TextPos::new(k, 0), TextPos::new(k, bl.text.len()));
3138                self.sem(K_HEADING, 0, Some(&label), bl.level as i32);
3139            }
3140            MB_ROW => {
3141                for (c, fb) in lay.boxes.iter().enumerate() {
3142                    let Some(&(a, b)) = bl.cells.get(c) else { continue };
3143                    let t: String = doc.plain_range(TextPos::new(k, a as usize), TextPos::new(k, b as usize));
3144                    let px = self.dp_text(CELL_PX);
3145                    self.wp.ctx.last_rect = self.ltr_rect([box_x(cx, cw, fb.x - px, fb.w + px * 2, drtl), ctop, fb.w + px * 2, lay.ch]);
3146                    self.sem(K_TEXT, 0, Some(&t), 0);
3147                }
3148            }
3149            MB_RULE => {}
3150            _ if bl.marker == MK_NONE || bl.kind == MB_CODE => {
3151                let t: String = if bl.kind == MB_CODE {
3152                    bl.text.chars().take(LABEL_MAX).collect()
3153                } else {
3154                    doc.plain_range(TextPos::new(k, 0), TextPos::new(k, bl.text.len())).chars().take(LABEL_MAX).collect()
3155                };
3156                if !t.is_empty() {
3157                    self.sem(K_TEXT, 0, Some(&t), 0);
3158                }
3159            }
3160            _ => {}
3161        }
3162    }
3163
3164    // a link's rectangle in a row: merged with the link's other pieces of the row
3165    fn md_link_rec(&self, doc: &MdDoc, k: usize, link: u32, lr: [i32; 4], r: &LRow, links: &mut Vec<Link>) {
3166        let Some(spot) = spot_of(doc.link_spots(), k, |s| s.link == link && r.pieces.iter().any(|p| p.link == link && p.a >= s.a && p.b <= s.b)) else {
3167            return;
3168        };
3169        let sp = doc.link_spots()[spot];
3170        // (its own text: a list's marker in front of the block is no part of the link's name)
3171        let t = doc.block_text(k, sp.a as usize, sp.b as usize);
3172        links.push(Link { r: lr, logical: self.ltr_rect(lr), label: t, spot }); // (visible rows only)
3173    }
3174
3175    // an image at x, y (w x h px): the picture, else a frame with its alt text
3176    #[allow(clippy::too_many_arguments)]
3177    fn md_draw_image(&mut self, lc: &mut Cache, doc: &MdDoc, i: usize, x: i32, y: i32, w: i32, h: i32, tcol: u32, bgc: u32) {
3178        let img = lc.imgs.get(i).cloned().unwrap_or(Img::Failed);
3179        match img {
3180            Img::Bitmap(sf) => {
3181                let key = (i, w, h);
3182                let s = if sf.w == w && sf.h == h {
3183                    sf
3184                } else if let Some(s) = lc.scaled.get(&key) {
3185                    s.clone()
3186                } else {
3187                    let s = img_core::ops::scale(&sf, w, h, true).map(Rc::new).unwrap_or(sf);
3188                    lc.scaled.insert(key, s.clone());
3189                    s
3190                };
3191                self.draw_image(&s, i as u32, x, y, w, h);
3192            }
3193            Img::Vector(svg) => {
3194                let color = tcol;
3195                let fonts = self.svg_fonts();
3196                let d = lc.svgs.entry((i, color)).or_insert_with(|| Rc::new(SvgDraw { doc: svg.clone(), color, fonts })).clone();
3197                let obj: DrawRef = d;
3198                self.cmd_custom(x, y, w, h, Some(obj), 0, (i as u32).wrapping_mul(0x9E37_79B1) ^ color);
3199            }
3200            _ => {
3201                let hair = self.hair().max(1);
3202                self.draw_color(color_mix(bgc, tcol, 90));
3203                self.draw_frame(x, y, w, h, self.dp(3), hair);
3204                let alt = doc.images().get(i).map_or("", |m| m.alt.as_str());
3205                let alt = if alt.is_empty() { tr("Bild") } else { alt };
3206                let f = self.md_faces();
3207                let font = f.get(false, false, true);
3208                let (asc, desc, _) = self.md_metrics(font, f.size);
3209                let by = y + (h - asc - desc) / 2 + asc;
3210                self.draw_clip(x, y, w, h);
3211                self.cmd_text(x + self.dp(8), by, alt.as_bytes(), font, f.size, self.color(T_TEXT2) | 0xFF00_0000, 0);
3212                self.draw_unclip();
3213            }
3214        }
3215    }
3216}
3217
3218// a link's rectangle in a row: drawn (island coordinates) and logical (records, hit boxes)
3219struct Link {
3220    r: [i32; 4],
3221    logical: [i32; 4],
3222    label: String,
3223    spot: usize,
3224}
3225
3226// a run drawn with OpenType shaping and bidirectional reordering (text_shape): Hebrew and
3227// Arabic in their visual order, joined
3228struct Shaped {
3229    s: Vec<u8>,
3230    font: u32,
3231    size: i32,
3232    color: u32,
3233    // the pen and the baseline from the rectangle's corner
3234    dx: i32,
3235    base: i32,
3236}
3237
3238impl Draw for Shaped {
3239    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
3240        let old = g.text.set_shaping(true);
3241        let prev = g.text.get_font();
3242        g.dc.set_color(self.color);
3243        g.text.set_font(self.font, self.size);
3244        g.text.draw_n(g.dc, g.sf, r[0] + self.dx, r[1] + self.base, &self.s, self.s.len());
3245        g.text.set_font(prev.0, prev.1);
3246        g.text.set_shaping(old);
3247    }
3248}