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