Skip to main content

ui/
edit.rs

1// edit -- text input: single-line and multi-line text boxes, number boxes, the clipboard for
2// widgets                                                             [src/ui/ui_edit.s]
3//
4//   textbox(id, placeholder, &mut buf, cap_flags) -> EV
5//        WinUI TextBox (32 DIP high).  buf: the app's UTF-8 text; cap_flags = the size of
6//        the asm's buffer in bytes incl. the NUL (bits 0..22: the text holds at most cap - 1
7//        bytes) | TB_PASSWORD | TB_READONLY | TB_DIGITS | TB_SIGNED.  placeholder (or None)
8//        TB_PASSWORD: while it has the keyboard the input method's purpose is IP_PASSWORD (set
9//        by the box itself, the purpose of before put back after; no plat::ime_purpose needed).
10//        shows while the text is empty (cut with "…" where the box is narrower).
11//        ST_DISABLED via next_flags.
12//        EV_ACT: the text changed this frame; EV_COMMIT: Enter, Esc or focus lost; EV_FOCUS
13//        while it has the keyboard focus.
14//        Semantic record K_TEXTBOX: label = the text (one dot per character for passwords),
15//        value = caret, max = selection anchor (byte indices; 0 while not focused), min = the
16//        kind of box: (TB_MULTI | TB_PASSWORD | TB_READONLY) >> 24.
17//        TB_MULTI (U1): several lines (natural height 5 lines): Enter inserts a line break,
18//        Ctrl+Enter commits, Up / Down keep the preferred x, PgUp / PgDn move a view, Home /
19//        End work on the line (Ctrl: the text), the wheel and the caret scroll it vertically,
20//        a thin bar shows the position; TB_WRAP wraps at the width (else it scrolls
21//        horizontally; soft hyphens: a hyphen where a line breaks after one), TB_HYPHENATE
22//        with it hyphenates words that do not fit (hyph.rs; the hyphen is drawn, not part of
23//        the text).  Pasted text keeps its lines.
24//        TB_SPELL: spell checking -- misspelled words get a red wavy line, a right click on
25//        one offers suggestions, "add to dictionary" and "ignore" (edit_spell.rs: spell_enable,
26//        spell_language, spell_sources, spell_set, speller, spell_state, spell_errors).
27//   number_box(id, &mut v, &range) -> EV
28//        an integer in a text box: digits only ('-' too when the range minimum is negative);
29//        Up / Down add -/+ step; typed text is taken on Enter or focus loss; the value is
30//        clamped to the range.  EV_ACT when v changed, EV_COMMIT on Enter / focus loss.
31//        Semantic value = v, max = range maximum.  PgUp / PgDn: 10 steps.
32//   number_box_f(id, &mut v, &range_f) -> EV   (U1)
33//        an f64 in a text box: shown with the range's decimals and decimal_sep; ',' and '.'
34//        are both accepted; Up / Down = one step (step 0: 1 % of the range), PgUp / PgDn =
35//        10 steps; clamped, snapped and rounded like the slider.  Semantic value =
36//        v * 10^decimals.
37//   (U2) both number boxes follow v while they have the focus as long as the user has not
38//        edited their text; Enter / focus loss only store the text when it was edited.
39//   nb_live(id, &mut v) / nb_live_f(id, &mut v) -> bool   the number typed so far into box
40//        id (being edited) is stored when it differs from v (clamped / fixed to the range),
41//        without reformatting the text -- property rows preview while the user types
42//   open_folder(path) / open_url(url) -> 0 / -1   (U2) headless runs only print them
43//   clip_set(fmt, data) -> 0 / -1, clip_get(fmt) -> data   the clipboard (headless runs:
44//        an in-process clipboard, so scripts are deterministic)
45//   text_key_filter(k) (crate)  the keys the focused text box consumes are hidden from key()
46//
47// Keys: Left/Right (+Ctrl words, +Shift select), Home/End, Backspace/Delete (+Ctrl words),
48// Ctrl+A/C/X/V, Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo, Shift+Delete cut, Ctrl+Insert copy,
49// Shift+Insert paste, Enter commits, Esc drops the focus.  Plain character keys are consumed
50// (their text arrives as typed text).  Tab, Up/Down/PgUp/PgDn, F-keys, Alt chords and other
51// Ctrl chords stay for the app.
52// Mouse: click places the caret (Shift+click extends), drag selects, double click selects a
53// word.  The caret blinks every 530 ms (timer UIT_CARET; always visible in headless runs).
54//
55// Only one text box has the keyboard focus, so the edit state (caret, anchor, undo history)
56// is one block that belongs to the focused box (ed_*); the horizontal / vertical scroll
57// offsets are kept per box in its state slot.  The asm edits the app's NUL-terminated buffer
58// in place; here the box's text is moved into S::buf for the call (byte for byte the asm's
59// buffer up to its NUL) and handed back as a String.
60//
61// Deviation (Rust ownership): when another number box takes the edit buffer, the asm stores
62// the previous owner's edited text into that owner's value right away (through the `&v` it
63// kept); here that store waits for the owner's next number_box call (S::pend_text), which
64// runs before the owner reads its value.  The events it reports and the settled frame are the
65// same; an owner that is never drawn again keeps its old value.
66
67use alloc::format;
68use alloc::string::String;
69use alloc::vec::Vec;
70
71use crate::consts::*;
72use crate::keys::*;
73use crate::value::{fmt_f64_sep, parse_f64_sep, rf_fix, rf_scaled, RangeF};
74use crate::{Sp, Ui};
75use text::segment;
76#[path = "text_surface.rs"]
77mod surface;
78pub use surface::TextSurfaceLine;
79#[path = "edit_spell.rs"]
80mod spellbox;
81
82const TB_NAT_W: i32 = 200; // natural width of a text box (DIP)
83const NB_NAT_W: i32 = 120; // ... of a number box
84const TB_FLAG_MASK: u32 = 0xFF80_0000;
85const ML_MAX: usize = 4096; // visual lines of a multi-line box (more: the rest is one line)
86const PW_MAX: usize = 1024; // characters a password box shows
87const UD_N: usize = 16; // undo states (power of two)
88const UD_SEG: usize = 2048; // bytes per undo state (longer texts are not recorded)
89const TXQ_MAX: usize = super::ctx::TEXTQ_MAX; // typed codepoints taken per pass (all of them)
90const NB_CAP: u32 = 32; // number box text buffer
91
92/// Opt-in presentation for a multiline editor embedded in document content. Geometry is in
93/// physical pixels; callers scale it with the document zoom and DPI. Editing, clipboard,
94/// selection, accessibility and IME behavior remain the ordinary textbox implementation.
95#[derive(Clone, Copy)]
96pub struct TextboxPresentation {
97    /// Font identifier registered in the UI text engine.
98    pub font: u32,
99    /// Font size in 24.8 physical pixels, clamped to 1..512 pixels.
100    pub size: i32,
101    /// Line advance in physical pixels, clamped to 1..2048.
102    pub line_height: i32,
103    /// Baseline offset from a line's top, clamped to the line height.
104    pub baseline: i32,
105    /// Text and caret color, straight ARGB.
106    pub color: u32,
107    /// Solid editor background, straight ARGB.
108    pub background: u32,
109    /// Draw an underline beneath every text run.
110    pub underline: bool,
111}
112// Ctrl+key shortcuts a text box takes: A C X V Z Y and the editing keys
113const CTRL_KEYS: [u32; 13] = [65, 67, 88, 86, 90, 89, VK_LEFT, VK_RIGHT, VK_HOME, VK_END, VK_BACK, VK_DELETE, VK_INSERT];
114
115/// state shared by all windows
116pub(crate) struct S {
117    // the headless text clipboard (g_hclip, g_hclip_len, g_hclip_fmt)
118    pub(crate) hclip: Option<Vec<u8>>,
119    pub(crate) hclip_fmt: u32,
120    // clipboard_platform: alternative representations of the same selection.
121    pub(crate) hclip_rich: Vec<(u32, Vec<u8>)>,
122    // ---- the text box being built (g_tb_*)
123    buf: Vec<u8>, // g_tb_buf: its text (moved in for the call)
124    disp: Vec<u8>, // g_ed_disp: the dots of a password box (g_tb_disp = buf or disp)
125    tb_id: u32,
126    tb_max: usize, // capacity - 1
127    tb_flags: u32,
128    tb_chg: bool,
129    tb_ev: u32,
130    tb_natw: i32,
131    tb_base: i32,
132    tb_font: u32,
133    tb_size: i32,
134    tb_sem: Option<usize>, // its semantic record (None: the table was full)
135    presentation: Option<TextboxPresentation>,
136    surface: Option<Vec<TextSurfaceLine>>,
137    surface_start: Option<usize>,
138    surface_change: Option<(usize, usize, String)>,
139    ime_frame: Option<u32>, // the frame of the last caret told to the platform (ime_pos)
140    pw_purpose: Option<u32>, // a TB_PASSWORD box set IP_PASSWORD: the purpose before it (put back after)
141    pw_frame: u32,           // the frame it last had the keyboard in
142    // ---- the focused editor (g_ed_*): box ed_id of window ed_win (two windows may have boxes
143    // of the same id; the edit state is the one box's of the window with the keyboard)
144    ed_id: u32, // 0 = none
145    ed_win: u32,
146    ed_blur: u32, // box that lost the focus before it was drawn (of window ed_blur_win)
147    ed_blur_win: u32,
148    ed_caret: usize,
149    ed_anchor: usize,
150    ed_t0: u32, // blink start
151    ed_merge: bool, // typing continues the last undo step
152    ed_drag: bool, // drag selection running
153    ed_seen: u32, // pass the editor was last drawn in
154    ed_self: bool, // the text box reads keys itself
155    ed_multi: bool, // (U1) the focused editor is a multi-line box
156    ed_prefx: i32, // preferred x of Up / Down (-1 = none)
157    // ---- undo history: logical entries 0..count-1 at ring position base
158    ud_base: usize,
159    ud_pos: usize,
160    ud_count: usize,
161    ud_hdr: [[usize; 3]; UD_N], // len, caret, anchor
162    ud_data: [Vec<u8>; UD_N],
163    // ---- number boxes
164    nb_id: u32, // box owning nb_buf (of window nb_win)
165    nb_win: u32,
166    nb_seen: u32,
167    nb_pend: u32, // (of window nb_pend_win)
168    nb_pend_win: u32,
169    nb_pend_ev: u32,
170    nb_kind: u32, // owner of nb_buf: 0 i32 (Range), 1 f64 (RangeF)
171    nb_rg: Range, // the owner's range (g_nb_rg)
172    nb_rf: RangeF,
173    nb_shown: u64, // (U2) value (i32 / f64 bits) nb_orig shows
174    nb_buf: Vec<u8>,
175    nb_view: Vec<u8>,
176    nb_orig: Vec<u8>, // (U2) text as formatted from the value (unedited when equal)
177    pend_text: Option<(u32, u32, u32, Vec<u8>)>, // (port) window, id, kind, text of a released owner
178    // ---- the multi-line layout of the box being built (g_ml_*)
179    ml_start: Vec<usize>, // start of every visual line
180    ml_hyph: Vec<bool>, // the visual line ends hyphenated (a hyphen drawn after it)
181    hyph: Option<alloc::sync::Arc<spell::Hyphenator>>, // TB_HYPHENATE: the box's patterns
182    ml_w: i32, // wrap width (px)
183    ml_lh: i32, // line height (px)
184    ml_page: i32, // lines per view (PgUp / PgDn)
185    // ---- spell checking (TB_SPELL, edit_spell.rs)
186    spell: spellbox::S,
187}
188
189impl Default for S {
190    fn default() -> S {
191        S {
192            hclip: None,
193            hclip_fmt: 0,
194            hclip_rich: Vec::new(),
195            buf: Vec::new(),
196            disp: Vec::new(),
197            tb_id: 0,
198            tb_max: 0,
199            tb_flags: 0,
200            tb_chg: false,
201            tb_ev: 0,
202            tb_natw: 0,
203            tb_base: 0,
204            tb_font: 0,
205            tb_size: 0,
206            tb_sem: None,
207            presentation: None,
208            surface: None,
209            surface_start: None,
210            surface_change: None,
211            ime_frame: None,
212            pw_purpose: None,
213            pw_frame: 0,
214            ed_id: 0,
215            ed_win: 0,
216            ed_blur: 0,
217            ed_blur_win: 0,
218            ed_caret: 0,
219            ed_anchor: 0,
220            ed_t0: 0,
221            ed_merge: false,
222            ed_drag: false,
223            ed_seen: 0,
224            ed_self: false,
225            ed_multi: false,
226            ed_prefx: -1,
227            ud_base: 0,
228            ud_pos: 0,
229            ud_count: 0,
230            ud_hdr: [[0; 3]; UD_N],
231            ud_data: core::array::from_fn(|_| Vec::new()),
232            nb_id: 0,
233            nb_win: 0,
234            nb_seen: 0,
235            nb_pend: 0,
236            nb_pend_win: 0,
237            nb_pend_ev: 0,
238            nb_kind: 0,
239            nb_rg: Range::default(),
240            nb_rf: RangeF::default(),
241            nb_shown: 0,
242            nb_buf: Vec::new(),
243            nb_view: Vec::new(),
244            nb_orig: Vec::new(),
245            pend_text: None,
246            ml_start: Vec::new(),
247            ml_hyph: Vec::new(),
248            hyph: None,
249            ml_w: 0,
250            ml_lh: 0,
251            ml_page: 0,
252            spell: Default::default(),
253        }
254    }
255}
256
257// the display text: the text, or a dot per character (g_tb_disp)
258fn disp_of(e: &S) -> &[u8] {
259    if e.tb_flags & TB_PASSWORD != 0 { &e.disp } else { &e.buf }
260}
261
262// the byte at i, 0 past the end (the asm's NUL)
263fn at(s: &[u8], i: usize) -> u8 {
264    if i < s.len() { s[i] } else { 0 }
265}
266
267// utf8_decode(s, i) -> (codepoint, next) of str.s: 1..4 byte sequences; invalid bytes,
268// overlong forms, surrogates and values above U+10FFFF decode as U+FFFD and advance one
269// byte; NUL (and the end) is 0
270fn u8_decode(s: &[u8], i: usize) -> (u32, usize) {
271    let b0 = at(s, i) as u32;
272    if b0 < 0x80 {
273        return (b0, i + 1);
274    }
275    let (mut cp, n, min) = if b0 < 0xC2 {
276        return (0xFFFD, i + 1);
277    } else if b0 < 0xE0 {
278        (b0 & 0x1F, 1, 0x80)
279    } else if b0 < 0xF0 {
280        (b0 & 0x0F, 2, 0x800)
281    } else if b0 < 0xF5 {
282        (b0 & 7, 3, 0x10000)
283    } else {
284        return (0xFFFD, i + 1);
285    };
286    let mut j = i + 1;
287    for _ in 0..n {
288        let c = at(s, j) as u32;
289        if c & 0xC0 != 0x80 {
290            return (0xFFFD, i + 1);
291        }
292        cp = cp << 6 | (c & 0x3F);
293        j += 1;
294    }
295    if cp < min || cp > 0x10FFFF || cp & 0xFFFF_F800 == 0xD800 {
296        return (0xFFFD, i + 1);
297    }
298    (cp, j)
299}
300
301// utf8_next(s, i) -> start of the next character (i itself at the NUL / end)
302fn u8_next(s: &[u8], i: usize) -> usize {
303    if at(s, i) == 0 {
304        return i;
305    }
306    u8_decode(s, i).1
307}
308
309// utf8_prev(s, i) -> start of the character before i (0 if i == 0)
310fn u8_prev(s: &[u8], i: usize) -> usize {
311    let mut p = i;
312    if p == 0 {
313        return 0;
314    }
315    loop {
316        p -= 1;
317        if p == 0 {
318            return 0;
319        }
320        if s[p] & 0xC0 != 0x80 {
321            return p;
322        }
323    }
324}
325
326// utf8_clip(s, n) -> largest length <= n that ends on a character boundary
327fn u8_clip(s: &[u8], n: usize) -> usize {
328    let mut k = n;
329    while k != 0 && at(s, k) & 0xC0 == 0x80 {
330        k -= 1;
331    }
332    k
333}
334
335// utf8_encode(cp) appended (1..4 bytes; above U+10FFFF: U+FFFD).  (port) A surrogate code
336// point becomes U+FFFD too (the asm writes its 3-byte form, which a String cannot hold; the
337// platforms never deliver one: Win32 pairs WM_CHAR halves, xkb gives scalar values)
338fn u8_push(out: &mut Vec<u8>, cp: u32) {
339    let c = if cp > 0x10FFFF || cp & 0xFFFF_F800 == 0xD800 { 0xFFFD } else { cp };
340    if c < 0x80 {
341        out.push(c as u8);
342    } else if c < 0x800 {
343        out.push(0xC0 | (c >> 6) as u8);
344        out.push(0x80 | (c & 0x3F) as u8);
345    } else if c < 0x10000 {
346        out.push(0xE0 | (c >> 12) as u8);
347        out.push(0x80 | ((c >> 6) & 0x3F) as u8);
348        out.push(0x80 | (c & 0x3F) as u8);
349    } else {
350        out.push(0xF0 | (c >> 18) as u8);
351        out.push(0x80 | ((c >> 12) & 0x3F) as u8);
352        out.push(0x80 | ((c >> 6) & 0x3F) as u8);
353        out.push(0x80 | (c & 0x3F) as u8);
354    }
355}
356
357// the text up to its first NUL as a String (invalid sequences replaced; the boxes only
358// ever insert whole characters)
359fn to_string(mut b: Vec<u8>) -> String {
360    if let Some(p) = b.iter().position(|&c| c == 0) {
361        b.truncate(p);
362    }
363    match String::from_utf8(b) {
364        Ok(s) => s,
365        Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
366    }
367}
368
369// tb_owns(entry, multi) -> a focused text box takes this key (entry = VK | mods << 16)
370fn tb_owns(entry: u32, multi: bool) -> bool {
371    let vk = entry & 0xFF;
372    let m = entry >> 16;
373    if vk == VK_TAB {
374        return false;
375    }
376    if multi {
377        // (U1) multi-line: vertical keys, Ctrl+Enter
378        if vk == VK_UP || vk == VK_DOWN || vk.wrapping_sub(VK_PRIOR) <= VK_NEXT - VK_PRIOR {
379            return m & (MOD_ALT | MOD_CTRL) == 0;
380        }
381        if vk == VK_RETURN {
382            return m & MOD_ALT == 0;
383        }
384    }
385    // single line: Up / Down / PgUp / PgDn stay for the app (the number box reads Up / Down
386    // itself)
387    if vk == VK_UP || vk == VK_DOWN || vk.wrapping_sub(VK_PRIOR) <= VK_NEXT - VK_PRIOR {
388        return false;
389    }
390    if vk.wrapping_sub(VK_SHIFT) <= VK_CAPITAL - VK_SHIFT // Shift, Ctrl, Alt, Pause, Caps
391        || vk.wrapping_sub(VK_LWIN) <= VK_APPS - VK_LWIN // Windows keys, menu key
392        || vk.wrapping_sub(VK_F1) <= 23 // F1..F24
393        || vk.wrapping_sub(VK_NUMLOCK) <= 1
394        || vk.wrapping_sub(VK_LSHIFT) <= VK_RMENU - VK_LSHIFT
395    {
396        return false;
397    }
398    // AltGr (Ctrl+Alt) types characters
399    if m & MOD_ALTGR != 0 || m & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT {
400        return true;
401    }
402    if m & MOD_ALT != 0 {
403        return false;
404    }
405    if m & MOD_CTRL == 0 {
406        return true;
407    }
408    // Ctrl: only the editing shortcuts
409    CTRL_KEYS.contains(&vk)
410}
411
412// tb_accept(cp, flags) -> the box accepts the character
413fn tb_accept(cp: u32, flags: u32) -> bool {
414    if cp < 32 || cp == 127 {
415        return false;
416    }
417    if flags & TB_DIGITS == 0 || cp.wrapping_sub(48) <= 9 {
418        return true;
419    }
420    if flags & TB_DECIMAL != 0 && (cp == 44 || cp == 46) {
421        return true; // (U1) decimal separators
422    }
423    cp == 45 && flags & TB_SIGNED != 0
424}
425
426// nb_format(value): decimal text of a number box
427fn nb_format(v: i32) -> Vec<u8> {
428    format!("{}", v).into_bytes()
429}
430
431// nb_parse(text, stored) -> the value typed (64-bit, saturated), or the stored value when
432// there are no digits
433fn nb_parse_text(t: &[u8], stored: i32) -> i64 {
434    let mut i = 0;
435    let neg = at(t, 0) == b'-';
436    if neg {
437        i = 1;
438    }
439    let mut v: i64 = 0;
440    let mut digits = 0;
441    while i < NB_CAP as usize {
442        let d = at(t, i).wrapping_sub(48);
443        if d > 9 {
444            break;
445        }
446        digits += 1;
447        v = v * 10 + d as i64;
448        if v as u64 > 0x8000_0000 {
449            v = 0x8000_0000;
450        }
451        i += 1;
452    }
453    if digits == 0 {
454        return stored as i64;
455    }
456    if neg { -v } else { v }
457}
458
459// clamp a 64-bit value to a range (min first, then max)
460fn rg_clamp(v: i64, rg: &Range) -> i32 {
461    let mut c = v;
462    if c < rg.min as i64 {
463        c = rg.min as i64;
464    }
465    if c > rg.max as i64 {
466        c = rg.max as i64;
467    }
468    c as i32
469}
470
471impl Ui {
472    // ------------------------------------------------------------------ text box
473
474    // box id of the live window holds the edit state (two windows may have boxes of one id)
475    pub(crate) fn ed_is(&self, id: u32) -> bool {
476        let e = &self.m.edit;
477        e.ed_id == id && e.ed_win == self.uiw_cur
478    }
479
480    // box id of the live window owns the number box text nb_buf
481    fn nb_is(&self, id: u32) -> bool {
482        let e = &self.m.edit;
483        e.nb_id == id && e.nb_win == self.uiw_cur
484    }
485
486    /// Read the focused textbox's UTF-8 byte selection (anchor, caret), after drawing it.
487    /// Opt-in API for document editors; ordinary textbox behavior is unchanged.
488    pub fn textbox_selection(&self, id: u32) -> Option<(usize, usize)> {
489        if self.ed_is(id) && self.focused() == id {
490            Some((self.m.edit.ed_anchor, self.m.edit.ed_caret))
491        } else { None }
492    }
493
494    /// Select valid UTF-8 byte boundaries in an already focused/drawn textbox. Pass the
495    /// same current text used by textbox(); returns false without changes for invalid ranges.
496    pub fn textbox_select(&mut self, id: u32, text: &str, anchor: usize, caret: usize) -> bool {
497        if !self.ed_is(id) || self.focused() != id || !text.is_char_boundary(anchor) || !text.is_char_boundary(caret) {
498            return false;
499        }
500        self.m.edit.ed_anchor = anchor;
501        self.m.edit.ed_caret = caret;
502        self.m.edit.ed_prefx = -1;
503        self.tb_blink_reset();
504        self.repass();
505        true
506    }
507
508    /// ui_textbox(id, placeholder, buf, cap_flags) -> EV_* (see the module header)
509    #[cfg_attr(jfgui_dev, track_caller)]
510    pub fn textbox(&mut self, id: u32, placeholder: Option<&str>, buf: &mut String, cap_flags: u32) -> u32 {
511        let _src = self.src_scope();
512        self.textbox_icon(id, 0, placeholder, buf, cap_flags)
513    }
514
515    /// Single-line text input with a leading semantic icon. The icon reserves its own
516    /// inset, so pointer selection, caret scrolling and IME share the visible text origin.
517    /// Multiline inputs ignore the icon and retain their ordinary presentation.
518    #[cfg_attr(jfgui_dev, track_caller)]
519    pub fn textbox_icon(&mut self, id: u32, icon: u32, placeholder: Option<&str>, buf: &mut String, cap_flags: u32) -> u32 {
520        let _src = self.src_scope();
521        self.m.edit.tb_natw = TB_NAT_W;
522        let mut b = core::mem::take(buf).into_bytes();
523        let ev = self.tb_core(id, placeholder, &mut b, cap_flags, icon);
524        *buf = to_string(b);
525        ev
526    }
527
528    /// Edit multiline text on a caller-provided surface without the style's textbox frame.
529    /// Uses zero text insets and the supplied font, line metrics and colors. The supplied
530    /// presentation applies only to this call; ordinary textboxes keep their default style.
531    /// `TB_MULTI` is implicit; other capacity and textbox flags retain their usual meaning.
532    #[cfg_attr(jfgui_dev, track_caller)]
533    pub fn textbox_presented(&mut self, id: u32, placeholder: Option<&str>, buf: &mut String, cap_flags: u32, mut presentation: TextboxPresentation) -> u32 {
534        let _src = self.src_scope();
535        presentation.size = presentation.size.clamp(256, 512 * 256);
536        presentation.line_height = presentation.line_height.clamp(1, 2048);
537        presentation.baseline = presentation.baseline.clamp(0, presentation.line_height);
538        let previous = self.m.edit.presentation.replace(presentation);
539        let old_font = self.text.get_font();
540        let ev = self.textbox(id, placeholder, buf, cap_flags | TB_MULTI);
541        self.m.edit.presentation = previous;
542        self.text.set_font(old_font.0, old_font.1);
543        ev
544    }
545
546    // tb_core(id, placeholder, buf, cap_flags) -> EV (natural width in tb_natw); the text
547    // lives in S::buf for the call
548    fn tb_core(&mut self, id: u32, placeholder: Option<&str>, buf: &mut Vec<u8>, cap_flags: u32, icon: u32) -> u32 {
549        if let Some(p) = buf.iter().position(|&c| c == 0) {
550            buf.truncate(p); // the asm's text ends at its NUL
551        }
552        core::mem::swap(buf, &mut self.m.edit.buf);
553        // a text a screen reader set (a11y.rs): replaces the content, the caret at its end
554        let set = self.a11y_text(id).map(|t| {
555            let max = ((cap_flags & TB_CAP_MASK) as usize).saturating_sub(1);
556            let mut n = if max > 0 { t.len().min(max) } else { t.len() };
557            while !t.is_char_boundary(n) {
558                n -= 1;
559            }
560            let held = self.ed_is(id);
561            let e = &mut self.m.edit;
562            e.buf = t.as_bytes()[..n].to_vec();
563            if held {
564                e.ed_caret = n;
565                e.ed_anchor = n;
566            }
567            self.repass();
568        });
569        let ph = placeholder.map(|p| p.split('\0').next().unwrap_or(""));
570        let ev = if cap_flags & TB_MULTI != 0 {
571            self.tbm_core(id, ph, cap_flags)
572        } else {
573            self.tb_core1(id, ph, cap_flags, icon)
574        };
575        core::mem::swap(buf, &mut self.m.edit.buf);
576        if set.is_some() { ev | EV_ACT | EV_COMMIT } else { ev }
577    }
578
579    // tb_start(id, cap_flags, flag_mask) -> style flags: the per-call state of the box
580    fn tb_start(&mut self, id: u32, cap_flags: u32, mask: u32) -> u32 {
581        let e = &mut self.m.edit;
582        e.tb_id = id;
583        e.tb_max = ((cap_flags & TB_CAP_MASK) as usize).saturating_sub(1);
584        e.tb_flags = cap_flags & mask;
585        e.tb_chg = false;
586        e.tb_ev = 0;
587        self.take_flags()
588    }
589
590    // the single-line box (tb_core after the TB_MULTI test)
591    fn tb_core1(&mut self, id: u32, placeholder: Option<&str>, cap_flags: u32, icon: u32) -> u32 {
592        let stflags = self.tb_start(id, cap_flags, TB_FLAG_MASK);
593        // place and behave (style: size and text insets)
594        let st = self.style;
595        let mut sp = Sp { value: self.m.edit.tb_natw, icon, flags: 0, ..Sp::default() };
596        let (w, h) = st.textbox_metrics(self, &mut sp);
597        let (w, h) = self.ctrl_size(w, h, stflags, false); // (theme) control height
598        if icon != 0 { sp.s[0] += self.dp(23); }
599        self.place(w, h);
600        let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
601        if stflags & ST_DISABLED != 0 {
602            hb |= HB_DISABLED;
603        }
604        let ev = self.behave(id, hb);
605        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
606        let mut editing = self.tb_focus();
607        if editing {
608            self.m.edit.ed_multi = false; // (U1)
609        }
610        // geometry
611        let hair = self.hair();
612        let r = self.wp.ctx.last_rect;
613        let x0 = sp.s[0].wrapping_add(r[0]);
614        let tw = r[2].wrapping_sub(sp.s[0].wrapping_add(sp.s[1])).max(1);
615        let mut scroll = self.slot(id).scrollx;
616        st.textbox_font(self, &sp);
617        self.tb_build_disp();
618        // editing: pointer, keys, typed text (not while the box's spell menu is open)
619        if editing && !self.spell_menu_open(id) {
620            let x = self.rtl_map(self.wp.ctx.mx, x0, tw).wrapping_sub(x0).wrapping_add(scroll);
621            self.tb_mouse(ev, x);
622            self.tb_keys();
623            self.tb_typed();
624            #[cfg(target_os = "android")]
625            self.tb_android_ime();
626            if self.m.edit.tb_chg {
627                self.m.edit.tb_ev |= EV_ACT;
628                self.repass();
629                self.tb_build_disp();
630            }
631            if !self.ed_is(id) {
632                editing = false; // Esc dropped the focus
633                self.ime_off();
634            }
635        }
636        // The same face is used for hit-testing, selection, caret and display.
637        let (lh, base) = st.textbox_font(self, &sp);
638        let top = (r[3].wrapping_sub(lh) >> 1).wrapping_add(r[1]);
639        self.m.edit.tb_base = top.wrapping_add(base);
640        let (font, size) = self.text.get_font();
641        self.m.edit.tb_font = font;
642        self.m.edit.tb_size = size;
643        // scroll so that the caret stays visible
644        let last = tw.wrapping_sub(hair); // last column the caret may use
645        let mut s = scroll;
646        let mut cx = 0;
647        if editing {
648            let d = self.tb_to_disp(self.m.edit.ed_caret);
649            cx = self.text.caret_x(disp_of(&self.m.edit), d);
650            if cx.wrapping_sub(s) > last {
651                s = cx.wrapping_sub(last);
652            }
653            if cx < s {
654                s = cx;
655            }
656        }
657        // never further than needed to show the end of the text
658        let dlen = disp_of(&self.m.edit).len();
659        let ex = self.text.caret_x(disp_of(&self.m.edit), dlen).wrapping_sub(last);
660        if s > ex {
661            s = ex;
662        }
663        if s < 0 {
664            s = 0;
665        }
666        scroll = s;
667        self.slot(id).scrollx = s;
668        // box (style): fill, border, focus edge
669        sp.place(r);
670        sp.ev = ev;
671        sp.value = editing as i32;
672        sp.value2 = disabled as i32;
673        st.textbox_frame(self, &sp);
674        if icon != 0 {
675            let size = self.dp(16);
676            self.draw_color(self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT2 }));
677            self.draw_icon(icon, r[0] + self.dp(8), r[1] + (r[3] - size) / 2, size << 8);
678        }
679        // text area (clipped to the padding box); right-to-left it keeps the text's order
680        self.draw_clip(x0, r[1], tw, r[3]);
681        self.ltr_begin(x0, tw);
682        let bx = x0.wrapping_sub(scroll); // x of the first character
683        let tcol = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT });
684        if dlen == 0 {
685            // empty: placeholder
686            if let Some(p) = placeholder {
687                let c = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT2 });
688                self.tb_emit_hint(x0, p, c, tw);
689            }
690        } else {
691            let (a, b) = self.tb_sel();
692            if editing && a != b {
693                // selection: highlight, then the text in three clipped runs
694                let da = self.tb_to_disp(a);
695                let sx0 = self.text.caret_x(disp_of(&self.m.edit), da);
696                let db = self.tb_to_disp(b);
697                let sx1 = self.text.caret_x(disp_of(&self.m.edit), db);
698                let c = self.color(T_ACCENT);
699                self.draw_color(c);
700                self.draw_rect(bx.wrapping_add(sx0), top, sx1.wrapping_sub(sx0), lh);
701                self.tb_clip_x(r[0], bx.wrapping_add(sx0)); // before the selection
702                self.tb_emit_disp(bx, tcol);
703                self.draw_unclip();
704                self.tb_clip_x(bx.wrapping_add(sx0), bx.wrapping_add(sx1)); // inside: on the accent
705                let c = self.color(T_ON_ACCENT);
706                self.tb_emit_disp(bx, c);
707                self.draw_unclip();
708                self.tb_clip_x(bx.wrapping_add(sx1), r[0].wrapping_add(r[2])); // after
709                self.tb_emit_disp(bx, tcol);
710                self.draw_unclip();
711            } else {
712                self.tb_emit_disp(bx, tcol);
713            }
714        }
715        // spell checking (TB_SPELL): marks under the misspelled words
716        let spell = self.spell_scan(id);
717        if spell && dlen != 0 {
718            self.spell_draw(id, bx, 0, dlen, editing);
719        }
720        if editing {
721            let x = bx.wrapping_add(cx);
722            if self.tb_caret_on() {
723                let c = self.color(T_TEXT);
724                self.draw_color(c);
725                self.draw_rect(x, top, hair, lh);
726            }
727            plat::ime_inline(false); // (E4) the input method's window shows the composition
728            let pw = self.m.edit.tb_flags & TB_PASSWORD != 0;
729            self.tb_purpose(pw);
730            self.ime_pos(x, top, lh);
731        }
732        self.ltr_end();
733        self.draw_unclip();
734        if spell {
735            let hit = (ev & EV_RCLICK != 0).then(|| {
736                self.text.set_font(self.m.edit.tb_font, self.m.edit.tb_size);
737                let x = self.rtl_map(self.wp.ctx.mx, x0, tw).wrapping_sub(x0).wrapping_add(scroll);
738                self.tb_hit(x)
739            });
740            self.spell_input(id, ev, hit, editing);
741        }
742        self.tb_finish(ev, editing)
743    }
744
745    // the semantic record and the events of a text box (the end of tb_core / tbm_core)
746    fn tb_finish(&mut self, ev: u32, editing: bool) -> u32 {
747        #[cfg(target_os = "android")]
748        if editing {
749            let e = &self.m.edit;
750            plat::android_ime_publish(e.tb_id, &e.buf, e.ed_anchor, e.ed_caret, e.tb_flags);
751        }
752        if !editing {
753            self.wp.ctx.last_state &= !(SS_FOCUSED as u32);
754        }
755        let (v, mx) = if editing { (self.m.edit.ed_caret as i32, self.m.edit.ed_anchor as i32) } else { (0, 0) };
756        let pw = self.m.edit.tb_flags & TB_PASSWORD != 0;
757        let d = core::mem::take(if pw { &mut self.m.edit.disp } else { &mut self.m.edit.buf });
758        let n0 = self.sems.len();
759        {
760            let l = String::from_utf8_lossy(&d);
761            let id = self.m.edit.tb_id;
762            let kind = self.m.edit.tb_flags & (TB_MULTI | TB_PASSWORD | TB_READONLY);
763            let s = self.sem(K_TEXTBOX, id, Some(l.as_ref()), v);
764            s.max = mx;
765            s.min = (kind >> 24) as i32; // the kind of box for the accessibility tree (a11y.rs)
766        }
767        self.m.edit.tb_sem = if self.sems.len() > n0 { Some(n0) } else { None };
768        *(if pw { &mut self.m.edit.disp } else { &mut self.m.edit.buf }) = d;
769        // events: the widget's own EV_ACT / EV_COMMIT replace the click ones
770        let mut out = ev & (EV_HOVER | EV_HELD | EV_DOUBLE | EV_RCLICK | EV_DRAG);
771        if editing {
772            out |= EV_FOCUS;
773        }
774        out | self.m.edit.tb_ev
775    }
776
777    // the semantic record of the box just built: value and max (number boxes)
778    fn tb_sem_set(&mut self, value: i32, max: i32) {
779        let r = match self.m.edit.tb_sem {
780            Some(i) if i < self.sems.len() => &mut self.sems[i],
781            _ => &mut self.sem_dummy,
782        };
783        r.value = value;
784        r.max = max;
785    }
786
787    // ------------------------------------------------------------------ multi-line box (U1)
788
789    // tbm_core(id, placeholder, cap_flags) -> EV: the TB_MULTI text box
790    fn tbm_core(&mut self, id: u32, placeholder: Option<&str>, cap_flags: u32) -> u32 {
791        let stflags = self.tb_start(id, cap_flags, TB_FLAG_MASK & !TB_PASSWORD);
792        let hy = cap_flags & (TB_HYPHENATE | TB_WRAP) == TB_HYPHENATE | TB_WRAP;
793        self.m.edit.hyph = self.hyph_take(hy); // (hyph.rs)
794        // place: natural width tb_natw, height from the style (lines + insets)
795        let presentation = self.m.edit.presentation;
796        let lh = presentation.map_or_else(|| self.ts_line(TS_BODY), |p| p.line_height);
797        let st = self.style;
798        let mut sp = Sp { value: self.m.edit.tb_natw, value2: lh, flags: 1, ..Sp::default() };
799        let (w, h) = if presentation.is_some() {
800            (self.dp(self.m.edit.tb_natw), lh.saturating_mul(5))
801        } else {
802            st.textbox_metrics(self, &mut sp)
803        };
804        self.place(w, h);
805        let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
806        if stflags & ST_DISABLED != 0 {
807            hb |= HB_DISABLED;
808        }
809        let ev = self.behave(id, hb);
810        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
811        let mut editing = self.tb_focus();
812        if editing {
813            self.m.edit.ed_multi = true;
814        }
815        let hair = self.hair();
816        let r = self.wp.ctx.last_rect;
817        let x0 = sp.s[0].wrapping_add(r[0]);
818        let tw = r[2].wrapping_sub(sp.s[0].wrapping_add(sp.s[1])).max(1);
819        self.m.edit.ml_w = tw.wrapping_sub(hair); // wrap width (room for the caret)
820        let top = r[1].wrapping_add(sp.s[2]);
821        let vh = r[3].wrapping_sub(sp.s[2].wrapping_add(sp.s[2])).max(lh); // view height
822        self.m.edit.ml_page = (if lh != 0 { vh / lh } else { 0 }).max(1);
823        let boff = if let Some(p) = presentation {
824            self.text.set_font(p.font, p.size);
825            p.baseline
826        } else {
827            self.ts_base(TS_BODY) // also selects the body font
828        };
829        let (font, size) = self.text.get_font();
830        self.m.edit.tb_font = font;
831        self.m.edit.tb_size = size;
832        let (mut sx, mut sy) = {
833            let s = self.slot(id);
834            (s.scrollx, s.scrolly)
835        };
836        self.tb_build_disp();
837        self.tbm_layout();
838        // editing: pointer, keys, typed text (not while the box's spell menu is open)
839        if editing && !self.spell_menu_open(id) {
840            let x = self.rtl_map(self.wp.ctx.mx, x0, tw).wrapping_sub(x0).wrapping_add(sx);
841            let y = self.wp.ctx.my.wrapping_sub(top).wrapping_add(sy);
842            self.tbm_mouse(ev, x, y, lh);
843            self.m.edit.ml_lh = lh;
844            self.tb_keys();
845            self.tb_typed();
846            #[cfg(target_os = "android")]
847            self.tb_android_ime();
848            if self.m.edit.tb_chg {
849                self.m.edit.tb_ev |= EV_ACT;
850                self.repass();
851                self.tb_build_disp();
852            }
853            self.tbm_layout();
854            if !self.ed_is(id) {
855                editing = false; // Esc dropped the focus
856                self.ime_off();
857            }
858        }
859        // content height; the wheel scrolls while there is room
860        let ch = (self.m.edit.ml_start.len() as i32).wrapping_mul(lh);
861        let clamp_y = |y: i32| -> i32 {
862            let m = ch.wrapping_sub(vh).max(0);
863            y.min(m).max(0)
864        };
865        let wy = self.wp.ctx.wheel_y;
866        if ev & EV_HOVER != 0 && wy != 0 {
867            let d = wy.wrapping_mul(lh.wrapping_mul(3)) / 120;
868            let c = clamp_y(sy.wrapping_sub(d));
869            if c != sy {
870                sy = c;
871                self.wp.ctx.wheel_y = 0;
872            } // at the end: the page may scroll
873        }
874        // a finger may roll it; its way (touch.rs)
875        let (_, pan) = self.pan_take(id);
876        sy = clamp_y(sy.wrapping_add(pan));
877        let hit_end = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
878        self.pan_view(id, hit_end.saturating_sub(1), super::touch::AX_V, [0, sy], [0, ch.wrapping_sub(vh).max(0)]);
879        // the caret line and x
880        let mut cl = 0;
881        let mut cx = 0;
882        if editing {
883            let caret = self.m.edit.ed_caret;
884            cl = self.tbm_line_of(caret);
885            let ls = self.m.edit.ml_start[cl];
886            cx = self.text.caret_x(&self.m.edit.buf[ls..], caret - ls);
887            // scroll the caret into view when it moved or the text changed
888            let len = self.m.edit.buf.len() as i32;
889            let s = self.slot(id);
890            if caret as i32 != s.aux || len != s.fixed {
891                s.aux = caret as i32;
892                s.fixed = len;
893                let ct = (cl as i32).wrapping_mul(lh); // caret line top (content px)
894                if ct < sy {
895                    sy = ct;
896                }
897                let cb = ct.wrapping_add(lh).wrapping_sub(vh);
898                if cb > sy {
899                    sy = cb;
900                }
901                if self.m.edit.tb_flags & TB_WRAP == 0 {
902                    let last = tw.wrapping_sub(hair); // last column the caret may use
903                    if cx.wrapping_sub(sx) > last {
904                        sx = cx.wrapping_sub(last);
905                    }
906                    if cx < sx {
907                        sx = cx;
908                    }
909                }
910            }
911        }
912        sy = clamp_y(sy);
913        if self.m.edit.tb_flags & TB_WRAP != 0 {
914            sx = 0;
915        }
916        if sx < 0 {
917            sx = 0;
918        }
919        {
920            let s = self.slot(id);
921            s.scrollx = sx;
922            s.scrolly = sy;
923        }
924        // box (style): fill, border, focus edge
925        sp.place(r);
926        sp.ev = ev;
927        sp.value = editing as i32;
928        sp.value2 = disabled as i32;
929        if let Some(p) = presentation {
930            self.draw_color(p.background);
931            self.draw_rect(r[0], r[1], r[2], r[3]);
932        } else {
933            st.textbox_frame(self, &sp);
934        }
935        // text area clip (inside the border)
936        let inset = if presentation.is_some() { 0 } else { hair };
937        self.draw_clip(x0, r[1].wrapping_add(inset), tw, r[3].wrapping_sub(inset).wrapping_sub(inset));
938        self.ltr_begin(x0, tw); // right-to-left: the text keeps its order
939        // the lines move with the position (scrollcopy.rs)
940        let copy = ch > vh || self.m.edit.tb_flags & TB_WRAP == 0;
941        if copy {
942            self.scroll_copy_begin(id, x0.wrapping_sub(sx) as i64, top.wrapping_sub(sy) as i64);
943        }
944        let tcol = presentation.map_or_else(|| self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT }), |p| p.color);
945        let scol = self.color(T_ON_ACCENT); // selected text colour
946        let bx = x0.wrapping_sub(sx); // x of a line's first character
947        let spell = self.spell_scan(id); // TB_SPELL
948        if self.m.edit.buf.is_empty() {
949            // empty: placeholder on the first line
950            if let Some(p) = placeholder {
951                let c = if presentation.is_some() { 0xff87918f } else { self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT2 }) };
952                self.m.edit.tb_base = top.wrapping_sub(sy).wrapping_add(boff);
953                self.tb_emit_hint(x0, p, c, tw);
954            }
955        } else {
956            // selection range (empty unless focused)
957            let (sa, sb) = if editing { self.tb_sel() } else { (0, 0) };
958            // visible lines
959            let n = self.m.edit.ml_start.len();
960            let first = if lh != 0 { sy / lh } else { 0 };
961            let mut last = (if lh != 0 { sy.wrapping_add(vh) / lh } else { 0 }).wrapping_add(1);
962            if last > n as i32 {
963                last = n as i32;
964            }
965            let mut l = first;
966            while l < last && (l as usize) < ML_MAX {
967                let ls = self.m.edit.ml_start[l as usize];
968                let le = self.tbm_line_end(l as usize);
969                let hy = self.m.edit.ml_hyph.get(l as usize).copied().unwrap_or(false);
970                let ly = l.wrapping_mul(lh).wrapping_add(top).wrapping_sub(sy); // line top
971                self.m.edit.tb_base = ly.wrapping_add(boff);
972                // selection part of this line: [max(a, s), min(b, e)] (+ the line break)
973                let s0 = sa.max(ls);
974                let s1 = sb.min(le);
975                let sel = sa != sb && (s0 < s1 || (s0 == s1 && sb > le));
976                if sel {
977                    let x0r = self.text.caret_x(&self.m.edit.buf[ls..], s0 - ls);
978                    let mut x1r = self.text.caret_x(&self.m.edit.buf[ls..], s1 - ls);
979                    if sb > le {
980                        x1r = x1r.wrapping_add(self.dp(4)); // the line break is selected
981                    }
982                    let c = self.color(T_ACCENT);
983                    self.draw_color(c);
984                    self.draw_rect(bx.wrapping_add(x0r), ly, x1r.wrapping_sub(x0r), lh);
985                    // three clipped runs: before, inside (on the accent), after
986                    self.tb_clip_x(r[0], bx.wrapping_add(x0r));
987                    self.tbm_run(bx, ls, le, tcol, hy);
988                    self.draw_unclip();
989                    self.tb_clip_x(bx.wrapping_add(x0r), bx.wrapping_add(x1r));
990                    self.tbm_run(bx, ls, le, scol, hy);
991                    self.draw_unclip();
992                    self.tb_clip_x(bx.wrapping_add(x1r), r[0].wrapping_add(r[2]));
993                    self.tbm_run(bx, ls, le, tcol, hy);
994                    self.draw_unclip();
995                } else {
996                    self.tbm_run(bx, ls, le, tcol, hy);
997                }
998                if spell {
999                    self.spell_draw(id, bx, ls, le, editing);
1000                }
1001                l += 1;
1002            }
1003        }
1004        if editing {
1005            let ly = (cl as i32).wrapping_mul(lh).wrapping_add(top).wrapping_sub(sy);
1006            let x = bx.wrapping_add(cx);
1007            if self.tb_caret_on() {
1008                let c = presentation.map_or_else(|| self.color(T_TEXT), |p| p.color);
1009                self.draw_color(c);
1010                self.draw_rect(x, ly, hair, lh);
1011            }
1012            plat::ime_inline(false); // (E4) the input method's window shows the composition
1013            self.tb_purpose(false);
1014            self.ime_pos(x, ly, lh);
1015        }
1016        if copy {
1017            self.scroll_copy_end();
1018        }
1019        self.ltr_end();
1020        self.draw_unclip();
1021        // scroll position bar (2 DIP) when the text is taller than the view
1022        if ch > vh {
1023            sp.place(r);
1024            sp.value = vh;
1025            sp.value2 = ch;
1026            sp.s[0] = sy;
1027            sp.s[1] = top;
1028            st.textbox_vbar(self, &sp);
1029        }
1030        if spell {
1031            let hit = (ev & EV_RCLICK != 0).then(|| {
1032                let x = self.rtl_map(self.wp.ctx.mx, x0, tw).wrapping_sub(x0).wrapping_add(sx);
1033                let y = self.wp.ctx.my.wrapping_sub(top).wrapping_add(sy);
1034                self.m.edit.ml_lh = lh;
1035                self.tbm_hit(x, y)
1036            });
1037            self.spell_input(id, ev, hit, editing);
1038        }
1039        self.tb_finish(ev, editing)
1040    }
1041
1042    // .tm_run: the text of the line ls .. le at x, baseline tb_base (hy: and a hyphen)
1043    fn tbm_run(&mut self, x: i32, ls: usize, le: usize, c: u32, hy: bool) {
1044        let b = core::mem::take(&mut self.m.edit.buf);
1045        if hy {
1046            let mut t = b[ls..le].to_vec();
1047            t.push(b'-');
1048            self.tb_emit(x, &t, c);
1049        } else {
1050            self.tb_emit(x, &b[ls..le], c);
1051        }
1052        self.m.edit.buf = b;
1053    }
1054
1055    // tbm_layout: the visual lines of the text (ml_start, ml_hyph); lines break at '\n' and,
1056    // with TB_WRAP, where the text reaches ml_w px (after the last blank that fits, after a
1057    // soft hyphen, at a hyphenation point with TB_HYPHENATE, else inside the word).  Uses the
1058    // current font.
1059    fn tbm_layout(&mut self) {
1060        if let Some(lines) = &self.m.edit.surface {
1061            self.m.edit.ml_start = lines.iter().map(|l| Self::tb_fix_index(&self.m.edit.buf, l.start)).collect();
1062            self.m.edit.ml_hyph.clear();
1063            return;
1064        }
1065        let len = self.m.edit.buf.len();
1066        let wrap = self.m.edit.tb_flags & TB_WRAP != 0;
1067        let mut starts = core::mem::take(&mut self.m.edit.ml_start);
1068        let mut hyph = core::mem::take(&mut self.m.edit.ml_hyph);
1069        starts.clear();
1070        hyph.clear();
1071        let mut pos = 0;
1072        'para: loop {
1073            // paragraph end: the next '\n' or the end
1074            let mut pe = pos;
1075            while pe < len && self.m.edit.buf[pe] != b'\n' {
1076                pe += 1;
1077            }
1078            loop {
1079                if starts.len() >= ML_MAX {
1080                    break 'para; // too many lines: the rest stays on the last
1081                }
1082                starts.push(pos);
1083                hyph.push(false);
1084                if !wrap {
1085                    break;
1086                }
1087                let (e, hy) = self.tbm_wrap(pos, pe);
1088                if e >= pe {
1089                    break;
1090                }
1091                if let Some(h) = hyph.last_mut() {
1092                    *h = hy;
1093                }
1094                pos = e; // soft break: next visual line
1095            }
1096            if pe >= len {
1097                break;
1098            }
1099            pos = pe + 1; // after the '\n'
1100        }
1101        self.m.edit.ml_start = starts;
1102        self.m.edit.ml_hyph = hyph;
1103    }
1104
1105    // tbm_wrap(pos, pe) -> (where the visual line starting at pos ends (pos < end <= pe),
1106    // hyphenated): the last line break opportunity (UAX #14, a soft hyphen) whose text fits, a
1107    // hyphenation point (TB_HYPHENATE), else grapheme clusters
1108    fn tbm_wrap(&mut self, pos: usize, pe: usize) -> (usize, bool) {
1109        if self.tw_fits(pos, pe - pos) {
1110            return (pe, false);
1111        }
1112        let w = self.m.edit.ml_w;
1113        let hw = self.text.width("-");
1114        let hyph = self.m.edit.hyph.clone();
1115        let text = &mut self.text;
1116        match core::str::from_utf8(&self.m.edit.buf) {
1117            Ok(s) => {
1118                let mut fits = |a: usize, b: usize, h: bool| text.width_n(&s.as_bytes()[a..], b - a) + if h { hw } else { 0 } <= w;
1119                match hyph.as_deref() {
1120                    Some(hy) => segment::wrap_end_hyph(s, pos, pe, &mut fits, Some(&mut |wd: &str, out: &mut Vec<usize>| *out = hy.hyphenate(wd))),
1121                    None => segment::wrap_end_hyph(s, pos, pe, &mut fits, None),
1122                }
1123            }
1124            Err(_) => {
1125                // not UTF-8: code points while they fit (at least one)
1126                let buf = &self.m.edit.buf;
1127                let mut best = u8_next(buf, pos);
1128                while best < pe {
1129                    let n = u8_next(buf, best);
1130                    if text.width_n(&buf[pos..], n - pos) > w {
1131                        break;
1132                    }
1133                    best = n;
1134                }
1135                (best, false)
1136            }
1137        }
1138    }
1139
1140    // .tw_fits(pos, n): n bytes from pos fit into ml_w
1141    fn tw_fits(&mut self, pos: usize, n: usize) -> bool {
1142        self.text.width_n(&self.m.edit.buf[pos..], n) <= self.m.edit.ml_w
1143    }
1144
1145    // tbm_line_of(index) -> visual line that shows the index (a soft break index belongs
1146    // to the next line)
1147    fn tbm_line_of(&self, i: usize) -> usize {
1148        let st = &self.m.edit.ml_start;
1149        let mut l = st.len() as i32;
1150        loop {
1151            l -= 1;
1152            if l <= 0 {
1153                return 0;
1154            }
1155            if st[l as usize] <= i {
1156                return l as usize;
1157            }
1158        }
1159    }
1160
1161    // tbm_line_end(line) -> index after the last character of the line (before its '\n')
1162    fn tbm_line_end(&self, line: usize) -> usize {
1163        if let Some(lines) = &self.m.edit.surface { return Self::tb_fix_index(&self.m.edit.buf, lines[line].end); }
1164        let e = &self.m.edit;
1165        if line + 1 >= e.ml_start.len() {
1166            return e.buf.len();
1167        }
1168        let s = e.ml_start[line + 1];
1169        if s != 0 && e.buf[s - 1] == b'\n' { s - 1 } else { s }
1170    }
1171
1172    // tbm_hit(x, y) -> text index nearest to (x, y) px relative to the text's top-left
1173    // (content coordinates, scrolling included); line height ml_lh
1174    fn tbm_hit(&mut self, x: i32, y: i32) -> usize {
1175        let y = y.max(0);
1176        let lh = self.m.edit.ml_lh.max(1);
1177        let n = self.m.edit.ml_start.len() as i32;
1178        let l = (y / lh).min(n - 1).max(0) as usize;
1179        let ls = self.m.edit.ml_start[l];
1180        let le = self.tbm_line_end(l);
1181        let i = ls + self.text.hit(&self.m.edit.buf[ls..], Some(le - ls), x);
1182        self.tb_snap(i)
1183    }
1184
1185    // tbm_mouse(events, x, y, line_h): click / drag / double click in content coordinates
1186    fn tbm_mouse(&mut self, ev: u32, x: i32, y: i32, lh: i32) {
1187        self.m.edit.ml_lh = lh;
1188        let c = &self.wp.ctx;
1189        if c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == self.m.edit.tb_id {
1190            self.m.edit.ed_prefx = -1;
1191            let i = self.tbm_hit(x, y);
1192            self.tb_press(ev, i);
1193        } else if ev & EV_HELD != 0 {
1194            if self.m.edit.ed_drag {
1195                let i = self.tbm_hit(x, y);
1196                self.tb_drag_to(i);
1197            }
1198        } else {
1199            self.m.edit.ed_drag = false;
1200        }
1201    }
1202
1203    // tbm_vmove(lines, shift): the caret moves up / down by `lines` visual lines at the
1204    // preferred x (kept over several moves); at the first / last line it goes to its start /
1205    // end
1206    fn tbm_vmove(&mut self, lines: i32, shift: bool) {
1207        if self.m.edit.surface.is_some() { self.surface_vmove(lines, shift); return; }
1208        self.tbm_layout();
1209        let caret = self.m.edit.ed_caret;
1210        let cur = self.tbm_line_of(caret);
1211        // preferred x: kept, or the caret's x now
1212        let mut px = self.m.edit.ed_prefx;
1213        if px < 0 {
1214            let ls = self.m.edit.ml_start[cur];
1215            px = self.text.caret_x(&self.m.edit.buf[ls..], caret - ls);
1216        }
1217        let t = (cur as i32).wrapping_add(lines);
1218        let pos = if t < 0 {
1219            0 // above the first line: its start
1220        } else if t >= self.m.edit.ml_start.len() as i32 {
1221            self.m.edit.buf.len() // below the last line: the end
1222        } else {
1223            let ls = self.m.edit.ml_start[t as usize];
1224            let le = self.tbm_line_end(t as usize);
1225            let i = ls + self.text.hit(&self.m.edit.buf[ls..], Some(le - ls), px);
1226            self.tb_snap(i)
1227        };
1228        self.tb_move(pos, shift);
1229        self.m.edit.ed_prefx = px;
1230    }
1231
1232    // ------------------------------------------------------------------ drawing helpers
1233
1234    // tb_emit_hint(x, placeholder, colour, width): the placeholder, cut with "…" where it is
1235    // wider than the text area
1236    fn tb_emit_hint(&mut self, x: i32, p: &str, c: u32, w: i32) {
1237        let e = &self.m.edit;
1238        self.text.set_font(e.tb_font, e.tb_size);
1239        if w > 0 && self.text.width(p) > w {
1240            let (cut, _) = self.ellipsize(p.as_bytes(), w);
1241            self.tb_emit(x, cut.as_bytes(), c);
1242        } else {
1243            self.tb_emit(x, p.as_bytes(), c);
1244        }
1245    }
1246
1247    // tb_emit(x, s, colour): a text run at the text baseline
1248    fn tb_emit(&mut self, x: i32, s: &[u8], c: u32) {
1249        let e = &self.m.edit;
1250        let (y, font, size) = (e.tb_base, e.tb_font, e.tb_size);
1251        self.cmd_text(x, y, s, font, size, c, 0);
1252        if self.m.edit.presentation.is_some_and(|p| p.underline) {
1253            self.text.set_font(font, size);
1254            let width = (self.text.measure(core::str::from_utf8(s).unwrap_or("")) + 128) / 256;
1255            self.draw_color(c);
1256            self.draw_rect(x, y + (size / 256 / 12).max(1), width, (size / 256 / 16).max(1));
1257        }
1258    }
1259
1260    // tb_emit(x, disp, dlen, colour): the display text
1261    fn tb_emit_disp(&mut self, x: i32, c: u32) {
1262        let pw = self.m.edit.tb_flags & TB_PASSWORD != 0;
1263        let d = core::mem::take(if pw { &mut self.m.edit.disp } else { &mut self.m.edit.buf });
1264        self.tb_emit(x, &d, c);
1265        *(if pw { &mut self.m.edit.disp } else { &mut self.m.edit.buf }) = d;
1266    }
1267
1268    // tb_clip_x(x0, x1): clip drawing to the columns x0..x1 of the placed box
1269    fn tb_clip_x(&mut self, x0: i32, x1: i32) {
1270        let r = self.wp.ctx.last_rect;
1271        self.draw_clip(x0, r[1], x1.wrapping_sub(x0), r[3]);
1272    }
1273
1274    // tb_caret_on() -> the caret is drawn now (arms the blink timer)
1275    fn tb_caret_on(&mut self) -> bool {
1276        let (a, b) = self.tb_sel();
1277        if a != b {
1278            return false; // a selection hides the caret
1279        }
1280        if self.headless {
1281            return true; // headless: always visible
1282        }
1283        let blink = self.blink_ms; // the system's caret blink time (0: steady)
1284        if blink == 0 {
1285            return true;
1286        }
1287        let t = self.now_ms.wrapping_sub(self.m.edit.ed_t0);
1288        let phase = t / blink;
1289        self.timer(UIT_CARET, blink - t % blink);
1290        phase & 1 == 0
1291    }
1292
1293    /// (E4) the input method's composition in the live window (Preedit: its text and the caret
1294    /// as a byte offset in it), empty when none is going on: a canvas that draws an editable
1295    /// field of its own (ime_enable) draws it at its caret, underlined; the committed text comes
1296    /// through text_take
1297    pub fn ime_preedit(&self) -> plat::Preedit {
1298        self.ed_preedit()
1299    }
1300
1301    /// (E4) an editable field the app draws itself (a web page's input in a canvas) got the focus
1302    /// (on, its purpose IP_*: IP_TEXT, IP_URL, IP_EMAIL, IP_NUMBER, IP_PHONE, IP_PASSWORD,
1303    /// IP_SEARCH) or lost it (off): with it the input method comes while the app tells its caret
1304    /// every frame (ime_pos), and the app draws the composition itself (ime_preedit: Windows
1305    /// hides the method's composition window, its candidates stay at the caret); IP_PASSWORD
1306    /// takes the input method away (Windows) or says so (Wayland).  Off: the method goes
1307    pub fn ime_enable(&mut self, on: bool, purpose: u32) {
1308        self.m.edit.pw_purpose = None; // the app's own field says its purpose itself
1309        plat::ime_purpose(if on { purpose } else { plat::IP_TEXT });
1310        plat::ime_inline(on);
1311        if !on {
1312            self.ime_pos(0, 0, 0);
1313        }
1314    }
1315
1316    // ime_pos(x, y, h): IME windows follow the caret (Windows, window mode only); so do screen
1317    // magnifiers (the hidden system caret, the accessibility tree: a11y_caret)
1318    pub fn ime_pos(&mut self, x: i32, y: i32, h: i32) {
1319        let px = self.phys_x(x);
1320        self.a11y_caret(px, y, h);
1321        if !self.headless {
1322            plat::ime_pos(px, y, h);
1323        }
1324        self.m.edit.ime_frame = (h > 0).then_some(self.frame);
1325    }
1326
1327    // (frame_end, Android) no text box told its caret this frame after one did: it went away with
1328    // its window (a dialog closed) and cannot tell; the keyboard must not come with the next tap
1329    #[cfg(target_os = "android")]
1330    pub(crate) fn ime_frame_end(&mut self) {
1331        if self.m.edit.ime_frame.is_some_and(|f| f != self.frame) {
1332            self.ime_off();
1333        }
1334    }
1335
1336    // ime_off: the text box lost the focus
1337    fn ime_off(&mut self) {
1338        #[cfg(target_os = "android")]
1339        plat::android_ime_clear();
1340        self.tb_purpose(false);
1341        self.ime_pos(0, 0, 0);
1342    }
1343
1344    // tb_purpose(pw): a TB_PASSWORD box with the keyboard sets the input method's purpose to
1345    // IP_PASSWORD itself (Windows takes the input method away, X11 keeps the keys from IBus,
1346    // Wayland tells the compositor; the app calls no plat::ime_purpose); any other text box with
1347    // the keyboard, and the password box losing it, put back the purpose of before
1348    fn tb_purpose(&mut self, pw: bool) {
1349        if pw {
1350            if self.m.edit.pw_purpose.is_none() {
1351                self.m.edit.pw_purpose = Some(plat::ime_purpose_of());
1352            }
1353            self.m.edit.pw_frame = self.frame;
1354            plat::ime_purpose(plat::IP_PASSWORD);
1355        } else if let Some(p) = self.m.edit.pw_purpose.take() {
1356            plat::ime_purpose(p);
1357        }
1358    }
1359
1360    // (frame_end) the password box that set IP_PASSWORD did not have the keyboard in this frame
1361    // (it went away with its window, or another editor took the keyboard): the purpose of
1362    // before comes back
1363    pub(crate) fn pw_frame_end(&mut self) {
1364        if self.m.edit.pw_purpose.is_some() && self.m.edit.pw_frame != self.frame {
1365            self.tb_purpose(false);
1366        }
1367    }
1368
1369    // ------------------------------------------------------------------ focus and edit state
1370
1371    // (md_view.rs) a text box got another widget id (a field of a changed document): its edit
1372    // state (caret, selection) goes with it
1373    pub(crate) fn tb_rekey(&mut self, old: u32, new: u32) {
1374        let w = self.uiw_cur;
1375        let e = &mut self.m.edit;
1376        if e.ed_id == old && e.ed_win == w {
1377            e.ed_id = new;
1378        }
1379        if e.ed_blur == old && e.ed_blur_win == w {
1380            e.ed_blur = new;
1381        }
1382    }
1383
1384    // tb_focus() -> this box (tb_id) is the focused editor.  Loads the edit state when the
1385    // focus arrives, reports EV_COMMIT when it leaves.  The state is keyed by window and id:
1386    // a box of the same id in another window neither takes nor drops it.
1387    fn tb_focus(&mut self) -> bool {
1388        let id = self.m.edit.tb_id;
1389        let w = self.uiw_cur;
1390        if self.m.edit.ed_blur == id && self.m.edit.ed_blur_win == w {
1391            self.m.edit.ed_blur = 0;
1392            self.m.edit.tb_ev |= EV_COMMIT;
1393        }
1394        // (multi-window) only the window with the keyboard edits; the others show their
1395        // focused text box unfocused
1396        if self.wp.ctx.focus == id && self.wp.ctx.last_state != SS_DISABLED as u32 && w == self.m.win.active {
1397            let fv = self.wp.ctx.focus_visible;
1398            let e = &mut self.m.edit;
1399            if e.ed_id != id || e.ed_win != w {
1400                // a box that still holds the edit state commits when it is drawn
1401                if e.ed_id != 0 {
1402                    e.ed_blur = e.ed_id;
1403                    e.ed_blur_win = e.ed_win;
1404                }
1405                e.ed_id = id;
1406                e.ed_win = w;
1407                e.ed_caret = e.buf.len();
1408                e.ed_anchor = e.buf.len();
1409                if fv {
1410                    e.ed_anchor = 0; // keyboard focus selects everything
1411                }
1412                e.ed_drag = false;
1413                self.ud_reset();
1414                self.tb_blink_reset();
1415            }
1416            self.m.edit.ed_seen = self.frame;
1417            // the app may have changed the text: keep caret and anchor inside it
1418            let e = &mut self.m.edit;
1419            e.ed_caret = Self::tb_fix_index(&e.buf, e.ed_caret);
1420            e.ed_anchor = Self::tb_fix_index(&e.buf, e.ed_anchor);
1421            return true;
1422        }
1423        if self.ed_is(id) {
1424            self.m.edit.ed_id = 0;
1425            self.m.edit.tb_ev |= EV_COMMIT;
1426            self.ime_off();
1427        }
1428        false
1429    }
1430
1431    // tb_fix_index(i) -> i clamped to the text and moved to a character start
1432    fn tb_fix_index(buf: &[u8], i: usize) -> usize {
1433        u8_clip(buf, i.min(buf.len()))
1434    }
1435
1436    // tb_blink_reset: restart the blink cycle (caret visible now)
1437    fn tb_blink_reset(&mut self) {
1438        self.m.edit.ed_t0 = self.now_ms;
1439    }
1440
1441    // tb_move(pos, shift): move the caret; without shift the selection collapses
1442    fn tb_move(&mut self, pos: usize, shift: bool) {
1443        let e = &mut self.m.edit;
1444        e.ed_caret = pos;
1445        if !shift {
1446            e.ed_anchor = pos;
1447        }
1448        e.ed_merge = false;
1449        self.tb_blink_reset();
1450    }
1451
1452    // tb_sel() -> (selection start, end)
1453    fn tb_sel(&self) -> (usize, usize) {
1454        let e = &self.m.edit;
1455        if e.ed_caret <= e.ed_anchor { (e.ed_caret, e.ed_anchor) } else { (e.ed_anchor, e.ed_caret) }
1456    }
1457
1458    // ------------------------------------------------------------------ pointer
1459
1460    // a press on the box this frame at text index i (tb_mouse / tbm_mouse)
1461    fn tb_press(&mut self, ev: u32, i: usize) {
1462        self.m.edit.ed_drag = true;
1463        self.m.edit.ed_merge = false;
1464        self.tb_blink_reset();
1465        if ev & EV_DOUBLE != 0 {
1466            self.tb_word_at(i);
1467            self.m.edit.ed_drag = false;
1468            return;
1469        }
1470        self.m.edit.ed_caret = i;
1471        if self.mods & MOD_SHIFT == 0 {
1472            self.m.edit.ed_anchor = i; // Shift+click extends the selection
1473        }
1474    }
1475
1476    // a drag to text index i
1477    fn tb_drag_to(&mut self, i: usize) {
1478        if i != self.m.edit.ed_caret {
1479            self.m.edit.ed_caret = i;
1480            self.tb_blink_reset();
1481        }
1482    }
1483
1484    // tb_mouse(events, x): x = pointer relative to the first character (px)
1485    fn tb_mouse(&mut self, ev: u32, x: i32) {
1486        let c = &self.wp.ctx;
1487        if c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == self.m.edit.tb_id {
1488            let i = self.tb_hit(x);
1489            self.tb_press(ev, i);
1490        } else if ev & EV_HELD != 0 {
1491            if self.m.edit.ed_drag {
1492                let i = self.tb_hit(x);
1493                self.tb_drag_to(i);
1494            }
1495        } else {
1496            self.m.edit.ed_drag = false;
1497        }
1498    }
1499
1500    // tb_hit(x) -> text index closest to x px (relative to the first character)
1501    fn tb_hit(&mut self, x: i32) -> usize {
1502        let e = &self.m.edit;
1503        let d = disp_of(e);
1504        let i = self.text.hit(d, Some(d.len()), x);
1505        let i = self.tb_from_disp(i);
1506        self.tb_snap(i)
1507    }
1508
1509    // ------------------------------------------------------------------ display text
1510
1511    // tb_build_disp: the display text is the text, or a dot per character
1512    fn tb_build_disp(&mut self) {
1513        let e = &mut self.m.edit;
1514        if e.tb_flags & TB_PASSWORD == 0 {
1515            return;
1516        }
1517        e.disp.clear();
1518        let mut i = 0;
1519        let mut n = 0;
1520        while at(&e.buf, i) != 0 && n < PW_MAX {
1521            e.disp.extend_from_slice(&[0xE2, 0x97, 0x8F]); // U+25CF BLACK CIRCLE
1522            n += 1;
1523            i = u8_next(&e.buf, i);
1524        }
1525    }
1526
1527    // tb_to_disp(i) -> index in the display text of text index i
1528    fn tb_to_disp(&self, i: usize) -> usize {
1529        let e = &self.m.edit;
1530        if e.tb_flags & TB_PASSWORD == 0 {
1531            return i;
1532        }
1533        let mut p = 0;
1534        let mut n = 0;
1535        while p < i && at(&e.buf, p) != 0 && n < PW_MAX {
1536            p = u8_next(&e.buf, p);
1537            n += 1;
1538        }
1539        n * 3
1540    }
1541
1542    // tb_from_disp(d) -> text index of display index d
1543    fn tb_from_disp(&self, d: usize) -> usize {
1544        let e = &self.m.edit;
1545        if e.tb_flags & TB_PASSWORD == 0 {
1546            return d;
1547        }
1548        let mut n = d / 3;
1549        let mut p = 0;
1550        while n != 0 && at(&e.buf, p) != 0 {
1551            p = u8_next(&e.buf, p);
1552            n -= 1;
1553        }
1554        p
1555    }
1556
1557    // ------------------------------------------------------------------ characters and words
1558    // (Unicode segmentation: text::segment, UAX #29; text that is not UTF-8 by code points)
1559
1560    // the text as str (None: not UTF-8)
1561    fn tb_str(&self) -> Option<&str> {
1562        core::str::from_utf8(&self.m.edit.buf).ok()
1563    }
1564
1565    // tb_prev(i) / tb_next(i) -> start of the grapheme cluster before / after index i
1566    fn tb_prev(&self, i: usize) -> usize {
1567        match self.tb_str() {
1568            Some(s) => segment::grapheme_prev(s, i),
1569            None => u8_prev(&self.m.edit.buf, i),
1570        }
1571    }
1572
1573    fn tb_next(&self, i: usize) -> usize {
1574        match self.tb_str() {
1575            Some(s) => segment::grapheme_next(s, i),
1576            None => u8_next(&self.m.edit.buf, i),
1577        }
1578    }
1579
1580    // tb_back(i) -> what Backspace removes before i: an emoji sequence whole, of other
1581    // clusters the last code point (an accent can be typed again)
1582    fn tb_back(&self, i: usize) -> usize {
1583        let p = self.tb_prev(i);
1584        match self.tb_str() {
1585            Some(s) if !segment::pictographic_cluster(s, p, i) => u8_prev(&self.m.edit.buf, i).max(p),
1586            _ => p,
1587        }
1588    }
1589
1590    // tb_snap(i) -> i moved to the start of its grapheme cluster (hit tests)
1591    fn tb_snap(&self, i: usize) -> usize {
1592        match self.tb_str() {
1593            Some(s) if i < s.len() && !segment::is_grapheme_boundary(s, i) => segment::grapheme_prev(s, i),
1594            _ => i,
1595        }
1596    }
1597
1598    // tb_word_left(i) -> start of the word before i (Ctrl+Left)
1599    fn tb_word_left(&self, i: usize) -> usize {
1600        if self.m.edit.tb_flags & TB_PASSWORD != 0 {
1601            return 0;
1602        }
1603        match self.tb_str() {
1604            Some(s) => segment::word_prev(s, i),
1605            None => 0,
1606        }
1607    }
1608
1609    // tb_word_right(i) -> start of the next word after i (Ctrl+Right)
1610    fn tb_word_right(&self, i: usize) -> usize {
1611        let len = self.m.edit.buf.len();
1612        if self.m.edit.tb_flags & TB_PASSWORD != 0 || i >= len {
1613            return len;
1614        }
1615        match self.tb_str() {
1616            Some(s) => segment::word_next(s, i),
1617            None => len,
1618        }
1619    }
1620
1621    // tb_word_at(i): select the word at index i (a run of blanks or of punctuation as one)
1622    fn tb_word_at(&mut self, i: usize) {
1623        let len = self.m.edit.buf.len();
1624        if len == 0 {
1625            self.m.edit.ed_anchor = 0;
1626            self.m.edit.ed_caret = 0;
1627            return;
1628        }
1629        if self.m.edit.tb_flags & TB_PASSWORD != 0 {
1630            self.m.edit.ed_anchor = 0;
1631            self.m.edit.ed_caret = len;
1632            return;
1633        }
1634        let (a, b) = match self.tb_str() {
1635            Some(s) => {
1636                let (mut a, mut b) = segment::word_at(s, i);
1637                let kind = segment::word_kind(s, a, b);
1638                if kind != segment::WK_WORD {
1639                    // neighbours of the same kind join (".." , "  "), not across a line
1640                    while a > 0 {
1641                        let (pa, pb) = segment::word_at(s, a - 1);
1642                        if segment::word_kind(s, pa, pb) != kind || s[pa..pb].contains('\n') {
1643                            break;
1644                        }
1645                        a = pa;
1646                    }
1647                    while b < s.len() {
1648                        let (na, nb) = segment::word_at(s, b);
1649                        if segment::word_kind(s, na, nb) != kind || s[na..nb].contains('\n') {
1650                            break;
1651                        }
1652                        b = nb;
1653                    }
1654                }
1655                (a, b)
1656            }
1657            None => (0, len),
1658        };
1659        self.m.edit.ed_anchor = a;
1660        self.m.edit.ed_caret = b;
1661    }
1662
1663    // ------------------------------------------------------------------ editing primitives
1664
1665    // tb_delete(a, b): remove the bytes a..b-1
1666    fn tb_delete(&mut self, a: usize, b: usize) {
1667        let e = &mut self.m.edit;
1668        let b = b.min(e.buf.len());
1669        if b <= a {
1670            return;
1671        }
1672        e.buf.drain(a..b);
1673        if e.surface.is_some() { e.surface_start = Some(e.surface_start.map_or(a, |v| v.min(a))); }
1674        e.tb_chg = true;
1675    }
1676
1677    // tb_insert(pos, src) -> bytes inserted (cut at the capacity on a character boundary)
1678    fn tb_insert(&mut self, pos: usize, src: &[u8]) -> usize {
1679        let e = &mut self.m.edit;
1680        if e.tb_max <= e.buf.len() {
1681            return 0;
1682        }
1683        let room = e.tb_max - e.buf.len();
1684        let n = if src.len() > room { u8_clip(src, room) } else { src.len() };
1685        if n == 0 {
1686            return 0;
1687        }
1688        e.buf.splice(pos..pos, src[..n].iter().copied());
1689        if e.surface.is_some() { e.surface_start = Some(e.surface_start.map_or(pos, |v| v.min(pos))); }
1690        e.tb_chg = true;
1691        n
1692    }
1693
1694    // tb_replace(src): replace the selection with src; caret after it
1695    fn tb_replace(&mut self, src: &[u8]) {
1696        let (a, b) = self.tb_sel();
1697        // Structured text callers must receive the entire replacement or no edit. A
1698        // truncated paste can otherwise delete a selection before the model can reject it.
1699        if self.m.edit.surface.is_some()
1700            && self.m.edit.buf.len().saturating_sub(b - a).saturating_add(src.len()) > self.m.edit.tb_max
1701        {
1702            return;
1703        }
1704        if a != b {
1705            self.tb_delete(a, b);
1706        }
1707        let mut p = a;
1708        self.m.edit.ed_caret = p;
1709        self.m.edit.ed_anchor = p;
1710        if src.is_empty() {
1711            return;
1712        }
1713        p += self.tb_insert(p, src);
1714        self.m.edit.ed_caret = p;
1715        self.m.edit.ed_anchor = p;
1716    }
1717
1718    // tb_typed: insert this frame's typed text at the caret
1719    fn tb_typed(&mut self) {
1720        let cps = self.text_take(TXQ_MAX);
1721        let flags = self.m.edit.tb_flags;
1722        if flags & TB_READONLY != 0 {
1723            return;
1724        }
1725        let mut stage = Vec::new();
1726        for &c in cps.iter() {
1727            if tb_accept(c, flags) {
1728                u8_push(&mut stage, c);
1729            }
1730        }
1731        if stage.is_empty() {
1732            return;
1733        }
1734        if !self.m.edit.ed_merge {
1735            self.ud_push(); // typing continues one undo step
1736        }
1737        self.tb_replace(&stage);
1738        self.m.edit.ed_merge = true;
1739        self.tb_blink_reset();
1740    }
1741
1742    // ------------------------------------------------------------------ keys
1743
1744    // Android sends each composing revision immediately, with selection, through the
1745    // same capacity/filter/undo rules as normal typing. No wait for a word to commit.
1746    #[cfg(target_os = "android")]
1747    fn tb_android_ime(&mut self) {
1748        let Some((bytes, anchor, caret)) = plat::android_ime_take(self.m.edit.tb_id) else { return; };
1749        if self.m.edit.tb_flags & TB_READONLY != 0 { return; }
1750        let mut filtered = Vec::new();
1751        let mut positions = [0; 2];
1752        for (i, c) in core::str::from_utf8(&bytes).unwrap_or("").char_indices() {
1753            if tb_accept(c as u32, self.m.edit.tb_flags) { u8_push(&mut filtered, c as u32); }
1754            if i < anchor { positions[0] = filtered.len(); }
1755            if i < caret { positions[1] = filtered.len(); }
1756        }
1757        if filtered != self.m.edit.buf {
1758            if !self.m.edit.ed_merge { self.ud_push(); }
1759            self.m.edit.ed_anchor = 0;
1760            self.m.edit.ed_caret = self.m.edit.buf.len();
1761            self.tb_replace(&filtered);
1762            self.m.edit.ed_merge = true;
1763        }
1764        self.m.edit.ed_anchor = Self::tb_fix_index(&self.m.edit.buf, positions[0]);
1765        self.m.edit.ed_caret = Self::tb_fix_index(&self.m.edit.buf, positions[1]);
1766        self.m.edit.ed_prefx = -1;
1767        self.tb_blink_reset();
1768        self.repass();
1769    }
1770
1771    /// ui_text_key_filter(k) -> k, or None when the focused text box consumes that key (so
1772    /// app-level key() / key_peek() do not see it); k = index of a key queue entry
1773    pub(crate) fn text_key_filter(&mut self, k: Option<usize>) -> Option<usize> {
1774        let i = k?;
1775        let e = &self.m.edit;
1776        if e.ed_self {
1777            return k; // the text box itself asks
1778        }
1779        if e.ed_id == 0 || e.ed_id != self.wp.ctx.focus || e.ed_win != self.uiw_cur {
1780            return k; // (the box of the same id in another window holds it)
1781        }
1782        if self.frame.wrapping_sub(e.ed_seen) > 2 {
1783            return k; // the box is no longer drawn
1784        }
1785        if self.spell_menu_open(e.ed_id) {
1786            return k; // its spell menu takes the keys (edit_spell.rs)
1787        }
1788        let q = self.wp.ctx.keyq[i];
1789        if tb_owns(q, e.ed_multi) && !self.tb_esc_passes(q) { None } else { k }
1790    }
1791
1792    // tb_esc_passes(k): Esc is not the text box's while it holds the focus a dialog window gave
1793    // its first control (kbnav.rs): Esc closes the dialog at once, as on every system
1794    fn tb_esc_passes(&self, k: u32) -> bool {
1795        k & 0xFFFF == VK_ESCAPE && self.wp.ctx.kb.soft != 0 && self.wp.ctx.kb.soft == self.wp.ctx.focus
1796    }
1797
1798    // tb_keys: apply this frame's editing keys (stops at the first key it does not own)
1799    fn tb_keys(&mut self) {
1800        self.m.edit.ed_self = true;
1801        loop {
1802            if self.m.edit.ed_id != self.m.edit.tb_id {
1803                break;
1804            }
1805            let k = self.key_first();
1806            if k == 0 || !tb_owns(k, self.m.edit.ed_multi) || self.tb_esc_passes(k) {
1807                break;
1808            }
1809            self.key(k & 0xFFFF); // consume it
1810            let k = self.rtl_text_key(k); // right-to-left: arrows as the text reads
1811            let vk = k & 0xFFFF;
1812            let m = k >> 16;
1813            let shift = m & MOD_SHIFT != 0;
1814            let ctrl = m & (MOD_ALT | MOD_ALTGR) == 0 && m & MOD_CTRL != 0; // not AltGr
1815            let flags = self.m.edit.tb_flags;
1816            if flags & TB_MULTI != 0 {
1817                // (U1) line keys of a multi-line box
1818                let page = self.m.edit.ml_page;
1819                match vk {
1820                    VK_UP => {
1821                        self.tbm_vmove(-1, shift);
1822                        continue;
1823                    }
1824                    VK_DOWN => {
1825                        self.tbm_vmove(1, shift);
1826                        continue;
1827                    }
1828                    VK_PRIOR => {
1829                        self.tbm_vmove(page.wrapping_neg(), shift);
1830                        continue;
1831                    }
1832                    VK_NEXT => {
1833                        self.tbm_vmove(page, shift);
1834                        continue;
1835                    }
1836                    VK_RETURN => {
1837                        if ctrl {
1838                            self.m.edit.tb_ev |= EV_COMMIT; // Ctrl+Enter commits
1839                        } else if flags & TB_READONLY == 0 {
1840                            self.m.edit.ed_prefx = -1;
1841                            self.ud_push();
1842                            self.tb_replace(b"\n");
1843                            self.tb_blink_reset();
1844                        }
1845                        continue;
1846                    }
1847                    VK_HOME if !ctrl => {
1848                        self.m.edit.ed_prefx = -1;
1849                        self.tbm_layout();
1850                        let l = self.tbm_line_of(self.m.edit.ed_caret);
1851                        let p = self.m.edit.ml_start[l];
1852                        self.tb_move(p, shift);
1853                        continue;
1854                    }
1855                    VK_END if !ctrl => {
1856                        self.m.edit.ed_prefx = -1;
1857                        self.tbm_layout();
1858                        let l = self.tbm_line_of(self.m.edit.ed_caret);
1859                        let p = self.tbm_line_end(l);
1860                        self.tb_move(p, shift);
1861                        continue;
1862                    }
1863                    _ => {} // Ctrl+Home / End: the whole text
1864                }
1865            }
1866            self.m.edit.ed_prefx = -1;
1867            let len = self.m.edit.buf.len();
1868            let caret = self.m.edit.ed_caret;
1869            match vk {
1870                VK_LEFT => {
1871                    let (a, b) = self.tb_sel();
1872                    let p = if ctrl {
1873                        self.tb_word_left(caret)
1874                    } else if !shift && a != b {
1875                        a // collapse to the selection start
1876                    } else {
1877                        self.tb_prev(caret)
1878                    };
1879                    self.tb_move(p, shift);
1880                }
1881                VK_RIGHT => {
1882                    let (a, b) = self.tb_sel();
1883                    let p = if ctrl {
1884                        self.tb_word_right(caret)
1885                    } else if !shift && a != b {
1886                        b // collapse to the selection end
1887                    } else {
1888                        self.tb_next(caret)
1889                    };
1890                    self.tb_move(p, shift);
1891                }
1892                VK_HOME => self.tb_move(0, shift),
1893                VK_END => self.tb_move(len, shift),
1894                VK_BACK => {
1895                    if flags & TB_READONLY != 0 {
1896                        continue;
1897                    }
1898                    let (a, b) = self.tb_sel();
1899                    if a != b {
1900                        self.tb_delrange(a, b);
1901                    } else {
1902                        let s = if ctrl { self.tb_word_left(a) } else { self.tb_back(a) };
1903                        self.tb_delrange(s, a);
1904                    }
1905                }
1906                VK_DELETE => {
1907                    if shift && !ctrl {
1908                        self.tb_cut(); // Shift+Delete = cut
1909                        continue;
1910                    }
1911                    if flags & TB_READONLY != 0 {
1912                        continue;
1913                    }
1914                    let (a, b) = self.tb_sel();
1915                    if a != b {
1916                        self.tb_delrange(a, b);
1917                    } else {
1918                        let e = if ctrl { self.tb_word_right(a) } else { self.tb_next(a) };
1919                        self.tb_delrange(a, e);
1920                    }
1921                }
1922                VK_INSERT => {
1923                    if ctrl {
1924                        self.tb_copy(); // Ctrl+Insert = copy
1925                    } else if shift && flags & TB_READONLY == 0 {
1926                        self.tb_paste(); // Shift+Insert = paste
1927                    }
1928                }
1929                VK_RETURN => self.m.edit.tb_ev |= EV_COMMIT,
1930                VK_ESCAPE => {
1931                    self.wp.ctx.focus = 0;
1932                    self.m.edit.ed_id = 0;
1933                    self.m.edit.tb_ev |= EV_COMMIT;
1934                    self.repass();
1935                    break;
1936                }
1937                _ if !ctrl => {} // character keys: the text comes as typed text
1938                65 => {
1939                    // Ctrl+A
1940                    let e = &mut self.m.edit;
1941                    e.ed_anchor = 0;
1942                    e.ed_caret = len;
1943                    e.ed_merge = false;
1944                }
1945                67 => self.tb_copy(),
1946                88 => self.tb_cut(),
1947                86 => {
1948                    if flags & TB_READONLY == 0 {
1949                        self.tb_paste();
1950                    }
1951                }
1952                90 | 89 => {
1953                    // Ctrl+Z (Ctrl+Shift+Z = redo), Ctrl+Y
1954                    if flags & TB_READONLY == 0 {
1955                        if vk == 90 && !shift {
1956                            self.ud_undo();
1957                        } else {
1958                            self.ud_redo();
1959                        }
1960                    }
1961                }
1962                _ => {}
1963            }
1964        }
1965        self.m.edit.ed_self = false;
1966    }
1967
1968    // .tk_delrange(a, b): a..b is removed, the caret goes to a
1969    fn tb_delrange(&mut self, a: usize, b: usize) {
1970        if a >= b {
1971            return;
1972        }
1973        self.ud_push();
1974        self.tb_delete(a, b);
1975        self.tb_move(a, false);
1976    }
1977
1978    // .tk_cut: copy and delete the selection (not for read-only / password boxes)
1979    fn tb_cut(&mut self) {
1980        if self.m.edit.tb_flags & (TB_READONLY | TB_PASSWORD) != 0 {
1981            return;
1982        }
1983        self.tb_copy();
1984        let (a, b) = self.tb_sel();
1985        if a != b {
1986            self.tb_delrange(a, b);
1987        }
1988    }
1989
1990    // ------------------------------------------------------------------ clipboard operations
1991
1992    // tb_copy: the selection to the clipboard (not for passwords)
1993    fn tb_copy(&mut self) {
1994        if self.m.edit.tb_flags & TB_PASSWORD != 0 {
1995            return;
1996        }
1997        let (a, b) = self.tb_sel();
1998        if a == b {
1999            return;
2000        }
2001        let s = self.m.edit.buf[a..b].to_vec();
2002        self.clip_set(CLIP_TEXT, &s);
2003    }
2004
2005    // tb_paste: the first line of the clipboard text (filtered) replaces the selection
2006    // (multi-line boxes: every line)
2007    fn tb_paste(&mut self) {
2008        let Some(clip) = self.clip_get(CLIP_TEXT) else { return };
2009        let flags = self.m.edit.tb_flags;
2010        let cap = clip.len() * 3; // re-encoded: at most 3 bytes per input byte
2011        let mut out = Vec::with_capacity(cap + 4);
2012        let mut i = 0;
2013        loop {
2014            let (c, n) = u8_decode(&clip, i);
2015            if c == 0 {
2016                break;
2017            }
2018            i = n;
2019            if c == 10 {
2020                if flags & TB_MULTI == 0 || out.len() >= cap {
2021                    break;
2022                }
2023                out.push(10); // (U1) multi-line: keep the break
2024                continue;
2025            }
2026            if c == 13 {
2027                if flags & TB_MULTI != 0 {
2028                    if self.m.edit.surface.is_some() && clip.get(i) != Some(&10) {
2029                        out.push(10); // Rich document paste: a lone CR is a paragraph mark.
2030                    }
2031                    continue; // CR of CR LF: dropped
2032                }
2033                break;
2034            }
2035            if tb_accept(c, flags) {
2036                u8_push(&mut out, c);
2037            }
2038        }
2039        if out.is_empty() {
2040            return;
2041        }
2042        self.ud_push();
2043        self.tb_replace(&out);
2044        self.m.edit.ed_merge = false;
2045        self.tb_blink_reset();
2046    }
2047
2048    /// Publish distinct TEXT/HTML/RTF/DOCX representations of one selection (64 MiB total).
2049    /// Validation precedes replacement; 0 means every representation was published.
2050    pub fn clip_set_many(&mut self, entries: &[(u32, &[u8])]) -> i32 {
2051        if !plat::clip_many_valid(entries) { return -1; }
2052        // (hot reload's replay leaves the system's clipboard alone: hotreload.rs)
2053        if !self.headless && !self.hr_replaying() { return if plat::clip_set_many(entries) { 0 } else { -1 }; }
2054        let rich: Vec<_> = entries.iter().filter(|e| e.0 != CLIP_TEXT).map(|e| (e.0, e.1.to_vec())).collect();
2055        let text = entries.iter().find(|e| e.0 == CLIP_TEXT).map(|e| e.1.to_vec());
2056        self.hclip_img_clear();
2057        self.m.edit.hclip = text;
2058        self.m.edit.hclip_fmt = CLIP_TEXT;
2059        self.m.edit.hclip_rich = rich;
2060        0
2061    }
2062
2063    /// Replace the clipboard with one text, image or rich representation.
2064    pub fn clip_set(&mut self, fmt: u32, data: &[u8]) -> i32 {
2065        if matches!(fmt, CLIP_HTML | CLIP_RTF | CLIP_DOCX) { return self.clip_set_many(&[(fmt, data)]); }
2066        if !self.headless && !self.hr_replaying() {
2067            return if plat::clip_set(fmt, data) { 0 } else { -1 };
2068        }
2069        if fmt != CLIP_TEXT {
2070            return self.hclip_img_set(fmt, data); // K1: images go to the image store
2071        }
2072        self.hclip_img_clear(); // K1: one content at a time
2073        self.m.edit.hclip_rich.clear();
2074        self.m.edit.hclip = Some(data.to_vec());
2075        self.m.edit.hclip_fmt = fmt;
2076        0
2077    }
2078
2079    /// ui_clip_get(fmt) -> the clipboard data (text: UTF-8) or None
2080    pub fn clip_get(&mut self, fmt: u32) -> Option<Vec<u8>> {
2081        if self.hr_on() {
2082            return self.hr_clip_get(fmt); // hot reload: the answer recorded / replayed (hotreload.rs)
2083        }
2084        self.clip_read(fmt)
2085    }
2086
2087    /// clip_get without hot reload's record (the scripts' own checks of the clipboard)
2088    pub(crate) fn clip_read(&mut self, fmt: u32) -> Option<Vec<u8>> {
2089        if !self.headless {
2090            return plat::clip_get(fmt);
2091        }
2092        if fmt != CLIP_TEXT {
2093            if matches!(fmt, CLIP_HTML | CLIP_RTF | CLIP_DOCX) {
2094                return self.m.edit.hclip_rich.iter().find(|e| e.0 == fmt).map(|e| e.1.clone());
2095            }
2096            return self.hclip_img_get(fmt); // K1: images come from the image store
2097        }
2098        match &self.m.edit.hclip {
2099            Some(d) if self.m.edit.hclip_fmt == fmt => Some(d.clone()),
2100            _ => None,
2101        }
2102    }
2103
2104    /// ui_open_folder(path) -> 0 / -1: (U2) show a folder (or a file selected in its folder)
2105    /// in the system file manager; headless runs start nothing: they print
2106    /// "ui_open_folder: PATH" and return 0
2107    pub fn open_folder(&mut self, path: &str) -> i32 {
2108        if !self.headless && !self.hr_replaying() {
2109            return plat::open_folder(path);
2110        }
2111        plat::write_stdout(format!("ui_open_folder: {}\n", path).as_bytes());
2112        0
2113    }
2114
2115    /// ui_open_url(url) -> 0 / -1: a web page / mailto link in the default application;
2116    /// headless runs print "ui_open_url: URL" and return 0
2117    pub fn open_url(&mut self, url: &str) -> i32 {
2118        if !self.headless && !self.hr_replaying() {
2119            return plat::open_url(url);
2120        }
2121        plat::write_stdout(format!("ui_open_url: {}\n", url).as_bytes());
2122        0
2123    }
2124
2125    /// (MV_OPEN_LINKS, RT_OPEN_LINKS) a link of a document the library opens by itself: a web page
2126    /// or a mail address only (http, https, mailto) -- a document's file:, ms-msdt: or search-ms:
2127    /// link would start a program; the app gets every link and decides about the others
2128    pub(crate) fn open_doc_link(&mut self, url: &str) {
2129        let scheme = url.split_once(':').map_or("", |(s, _)| s);
2130        if ["http", "https", "mailto"].iter().any(|w| scheme.eq_ignore_ascii_case(w)) {
2131            self.open_url(url);
2132        }
2133    }
2134
2135    /// hclip_text_has: the headless clipboard holds text
2136    pub(crate) fn hclip_text_has(&self) -> bool {
2137        self.m.edit.hclip.is_some()
2138    }
2139
2140    /// hclip_text_clear: empty the headless text clipboard (an image was set)
2141    pub(crate) fn hclip_text_clear(&mut self) {
2142        self.m.edit.hclip = None;
2143        self.m.edit.hclip_rich.clear();
2144    }
2145
2146    // ------------------------------------------------------------------ undo history
2147    // Logical entries 0..count-1 (ring position ud_base); entries before `pos` are older
2148    // states, entry `pos` (when it exists) is the current one (after an undo).
2149
2150    fn ud_reset(&mut self) {
2151        let e = &mut self.m.edit;
2152        e.ud_base = 0;
2153        e.ud_pos = 0;
2154        e.ud_count = 0;
2155        e.ed_merge = false;
2156    }
2157
2158    // ud_slot(i) -> ring index of logical entry i
2159    fn ud_slot(&self, i: usize) -> usize {
2160        (self.m.edit.ud_base + i) & (UD_N - 1)
2161    }
2162
2163    // ud_room: make room for one more entry (the oldest is dropped when full)
2164    fn ud_room(&mut self) {
2165        let e = &mut self.m.edit;
2166        if e.ud_count < UD_N {
2167            return;
2168        }
2169        e.ud_base = (e.ud_base + 1) & (UD_N - 1);
2170        e.ud_count -= 1;
2171        if e.ud_pos != 0 {
2172            e.ud_pos -= 1;
2173        }
2174    }
2175
2176    // ud_store(i): snapshot the text, caret and anchor into logical entry i
2177    fn ud_store(&mut self, i: usize) {
2178        let k = self.ud_slot(i);
2179        let e = &mut self.m.edit;
2180        e.ud_hdr[k] = [e.buf.len(), e.ed_caret, e.ed_anchor];
2181        e.ud_data[k].clear();
2182        e.ud_data[k].extend_from_slice(&e.buf);
2183    }
2184
2185    // ud_restore(i): make logical entry i the current text
2186    fn ud_restore(&mut self, i: usize) {
2187        let k = self.ud_slot(i);
2188        let e = &mut self.m.edit;
2189        let [len, caret, anchor] = e.ud_hdr[k];
2190        let n = len.min(e.tb_max);
2191        e.buf.clear();
2192        e.buf.extend_from_slice(&e.ud_data[k][..n]);
2193        if e.surface.is_some() { e.surface_start = Some(0); }
2194        e.tb_chg = true;
2195        e.ed_merge = false;
2196        e.ed_anchor = Self::tb_fix_index(&e.buf, anchor);
2197        e.ed_caret = Self::tb_fix_index(&e.buf, caret);
2198        self.tb_blink_reset();
2199    }
2200
2201    // ud_push: remember the state before an edit (drops the redo states)
2202    fn ud_push(&mut self) {
2203        self.m.edit.ed_merge = false;
2204        if self.m.edit.buf.len() >= UD_SEG {
2205            self.ud_reset(); // too long to record
2206            return;
2207        }
2208        self.m.edit.ud_count = self.m.edit.ud_pos;
2209        self.ud_room();
2210        self.ud_store(self.m.edit.ud_pos);
2211        self.m.edit.ud_pos += 1;
2212        self.m.edit.ud_count = self.m.edit.ud_pos;
2213    }
2214
2215    // ud_undo -> a state was restored
2216    fn ud_undo(&mut self) -> bool {
2217        if self.m.edit.ud_pos == 0 {
2218            return false;
2219        }
2220        if self.m.edit.ud_pos == self.m.edit.ud_count && self.m.edit.buf.len() < UD_SEG {
2221            // at the newest state: keep it for redo
2222            self.ud_room();
2223            self.ud_store(self.m.edit.ud_count);
2224            self.m.edit.ud_count += 1;
2225        }
2226        self.m.edit.ud_pos -= 1;
2227        self.ud_restore(self.m.edit.ud_pos);
2228        true
2229    }
2230
2231    // ud_redo -> a state was restored
2232    fn ud_redo(&mut self) -> bool {
2233        let p = self.m.edit.ud_pos + 1;
2234        if p >= self.m.edit.ud_count {
2235            return false;
2236        }
2237        self.m.edit.ud_pos = p;
2238        self.ud_restore(p);
2239        true
2240    }
2241
2242    // ------------------------------------------------------------------ number box
2243
2244    /// ui_number_box(id, &mut v, &range) -> EV_* (see the module header)
2245    #[cfg_attr(jfgui_dev, track_caller)]
2246    pub fn number_box(&mut self, id: u32, v: &mut i32, rg: &Range) -> u32 {
2247        let _src = self.src_scope();
2248        self.nb_pend_store(id, Some(&mut *v), None, Some(rg), None);
2249        let entry = *v;
2250        let mut deferred = 0;
2251        let step = if rg.step >= 1 { rg.step } else { 1 };
2252        let mut flags = NB_CAP | TB_DIGITS;
2253        if rg.min < 0 {
2254            flags |= TB_SIGNED;
2255        }
2256        // committed earlier in this frame by a box that took over the edit buffer
2257        if self.nb_pend_is(id) {
2258            self.m.edit.nb_pend = 0;
2259            deferred = self.m.edit.nb_pend_ev;
2260        }
2261        let view;
2262        // (multi-window) only the keyboard window edits
2263        if self.wp.ctx.focus == id && self.uiw_cur == self.m.win.active {
2264            if !self.nb_is(id) {
2265                self.nb_release(); // the previous owner commits its text
2266                let w = self.uiw_cur;
2267                let e = &mut self.m.edit;
2268                e.nb_id = id;
2269                e.nb_win = w;
2270                e.nb_kind = 0;
2271                e.nb_buf = nb_format(*v);
2272                self.nb_sync(*v as u32 as u64); // (U2) the text as shown for v
2273            }
2274            self.m.edit.nb_rg = *rg; // (g_nb_rg: the caller's record)
2275            self.m.edit.nb_seen = self.frame;
2276            self.nb_follow(*v as u32 as u64, 0); // (U2) v changed elsewhere, text not edited
2277            // Up / Down step the value while the editor is active
2278            if self.ed_is(id) {
2279                let n = self.nb_step_keys(); // +1 / -1 (arrows), +10 / -10 (pages)
2280                if n != 0 {
2281                    let d = n.wrapping_mul(step);
2282                    let p = nb_parse_text(&self.m.edit.nb_buf, *v);
2283                    self.nb_store(p.wrapping_add(d as i64), v);
2284                    self.repass();
2285                }
2286            }
2287            view = false;
2288        } else if self.nb_is(id) {
2289            self.m.edit.nb_rg = *rg;
2290            view = false; // losing the focus: committed below
2291        } else {
2292            self.m.edit.nb_view = nb_format(*v);
2293            view = true;
2294        }
2295        let ev = self.nb_core(id, flags, view);
2296        // Enter / focus loss: take the typed number (U2: only when the user edited the text)
2297        if ev & EV_COMMIT != 0 && self.nb_is(id) {
2298            if self.nb_edited() {
2299                let p = nb_parse_text(&self.m.edit.nb_buf, *v);
2300                self.nb_store(p, v);
2301                self.repass();
2302            }
2303            if self.wp.ctx.focus != id {
2304                self.m.edit.nb_id = 0;
2305            }
2306        }
2307        let mut out = (ev & !EV_ACT) | deferred; // EV_ACT = the value changed
2308        if *v != entry {
2309            out |= EV_ACT;
2310        }
2311        self.tb_sem_set(*v, rg.max);
2312        out
2313    }
2314
2315    // the text box of a number box (tb_core on nb_buf or nb_view)
2316    fn nb_core(&mut self, id: u32, flags: u32, view: bool) -> u32 {
2317        self.m.edit.tb_natw = NB_NAT_W;
2318        let mut b = core::mem::take(if view { &mut self.m.edit.nb_view } else { &mut self.m.edit.nb_buf });
2319        let ev = self.tb_core(id, None, &mut b, flags, 0);
2320        *(if view { &mut self.m.edit.nb_view } else { &mut self.m.edit.nb_buf }) = b;
2321        ev
2322    }
2323
2324    // nb_step_keys() -> steps typed into the focused number box: Up / Down 1, PgUp / PgDn
2325    // 10 (consumed)
2326    fn nb_step_keys(&mut self) -> i32 {
2327        self.m.edit.ed_self = true;
2328        let mut n = self.key(VK_UP) as i32;
2329        n -= self.key(VK_DOWN) as i32;
2330        n += self.key(VK_PRIOR) as i32 * 10;
2331        n -= self.key(VK_NEXT) as i32 * 10;
2332        self.m.edit.ed_self = false;
2333        n
2334    }
2335
2336    /// ui_number_box_f(id, &mut v, &range_f) -> EV_* (U1; see the module header)
2337    #[cfg_attr(jfgui_dev, track_caller)]
2338    pub fn number_box_f(&mut self, id: u32, v: &mut f64, rf: &RangeF) -> u32 {
2339        let _src = self.src_scope();
2340        self.nb_pend_store(id, None, Some(&mut *v), None, Some(rf));
2341        let entry = v.to_bits();
2342        let mut deferred = 0;
2343        let mut flags = NB_CAP | TB_DIGITS | TB_DECIMAL;
2344        if 0.0 > rf.min {
2345            flags |= TB_SIGNED;
2346        }
2347        if self.nb_pend_is(id) {
2348            self.m.edit.nb_pend = 0;
2349            deferred = self.m.edit.nb_pend_ev;
2350        }
2351        let view;
2352        if self.wp.ctx.focus == id && self.uiw_cur == self.m.win.active {
2353            // only the keyboard window edits
2354            if !self.nb_is(id) {
2355                self.nb_release();
2356                let t = self.nbf_format(*v, rf.dec);
2357                let w = self.uiw_cur;
2358                let e = &mut self.m.edit;
2359                e.nb_id = id;
2360                e.nb_win = w;
2361                e.nb_kind = 1;
2362                e.nb_buf = t;
2363                self.nb_sync(v.to_bits()); // (U2)
2364            }
2365            self.m.edit.nb_rf = *rf;
2366            self.m.edit.nb_seen = self.frame;
2367            self.nb_follow(v.to_bits(), rf.dec); // (U2)
2368            if self.ed_is(id) {
2369                let n = self.nb_step_keys();
2370                if n != 0 {
2371                    let x = self.nbf_parse(*v);
2372                    let mut step = rf.step;
2373                    if !(step > 0.0) {
2374                        step = (rf.max - rf.min) * 0.01;
2375                    }
2376                    let d = n as f64 * step;
2377                    self.nbf_store(x + d, v);
2378                    self.repass();
2379                }
2380            }
2381            view = false;
2382        } else if self.nb_is(id) {
2383            self.m.edit.nb_rf = *rf;
2384            view = false; // losing the focus: committed below
2385        } else {
2386            let t = self.nbf_format(*v, rf.dec);
2387            self.m.edit.nb_view = t;
2388            view = true;
2389        }
2390        let ev = self.nb_core(id, flags, view);
2391        if ev & EV_COMMIT != 0 && self.nb_is(id) {
2392            if self.nb_edited() {
2393                // (U2) only an edited text is taken
2394                let x = self.nbf_parse(*v);
2395                self.nbf_store(x, v);
2396                self.repass();
2397            }
2398            if self.wp.ctx.focus != id {
2399                self.m.edit.nb_id = 0;
2400            }
2401        }
2402        let mut out = (ev & !EV_ACT) | deferred;
2403        if v.to_bits() != entry {
2404            out |= EV_ACT;
2405        }
2406        let sv = rf_scaled(rf, *v);
2407        let mx = rf_scaled(rf, rf.max);
2408        self.tb_sem_set(sv, mx);
2409        out
2410    }
2411
2412    /// nb_live(id, &mut v) -> true when number box `id` is being edited and the text typed
2413    /// so far is a number different from v: the value (clamped to the range) is stored
2414    /// without reformatting the text -- property rows preview while the user types
2415    #[cfg_attr(jfgui_dev, track_caller)]
2416    pub fn nb_live(&mut self, id: u32, v: &mut i32) -> bool {
2417        let _src = self.src_scope();
2418        if id == 0 || !self.nb_is(id) || !self.ed_is(id) || self.m.edit.nb_kind != 0 {
2419            return false;
2420        }
2421        let e = &self.m.edit;
2422        let x = rg_clamp(nb_parse_text(&e.nb_buf, *v), &e.nb_rg); // (no digits: the value)
2423        if x == *v {
2424            return false;
2425        }
2426        *v = x;
2427        true
2428    }
2429
2430    /// nb_live_f(id, &mut v) -> nb_live of an f64 number box (fixed to its RangeF)
2431    #[cfg_attr(jfgui_dev, track_caller)]
2432    pub fn nb_live_f(&mut self, id: u32, v: &mut f64) -> bool {
2433        let _src = self.src_scope();
2434        if id == 0 || !self.nb_is(id) || !self.ed_is(id) || self.m.edit.nb_kind != 1 {
2435            return false;
2436        }
2437        let e = &self.m.edit;
2438        let Some(x) = parse_f64_sep(&e.nb_buf, self.m.value.decimal_sep) else { return false };
2439        let x = rf_fix(&e.nb_rf, x);
2440        if x.to_bits() == v.to_bits() {
2441            return false;
2442        }
2443        *v = x;
2444        true
2445    }
2446
2447    // nbf_format(v, decimals): the f64 with the range's decimals and the decimal separator
2448    fn nbf_format(&self, v: f64, dec: u32) -> Vec<u8> {
2449        fmt_f64_sep(v, dec, self.m.value.decimal_sep).into_bytes()
2450    }
2451
2452    // nbf_parse(stored) -> the number typed into nb_buf, or the stored value without digits
2453    fn nbf_parse(&self, stored: f64) -> f64 {
2454        parse_f64_sep(&self.m.edit.nb_buf, self.m.value.decimal_sep).unwrap_or(stored)
2455    }
2456
2457    // nbf_store(x, &mut v): fix the value to the owner's RangeF, store it, show it
2458    fn nbf_store(&mut self, x: f64, v: &mut f64) {
2459        let rf = self.m.edit.nb_rf;
2460        *v = rf_fix(&rf, x);
2461        let t = self.nbf_format(*v, rf.dec);
2462        self.m.edit.nb_buf = t;
2463        self.nb_sync(v.to_bits()); // (U2)
2464        self.nb_caret_end();
2465    }
2466
2467    // nb_store(x, &mut v): clamp a 64-bit value to the owner's range, store it, show it
2468    fn nb_store(&mut self, x: i64, v: &mut i32) {
2469        *v = rg_clamp(x, &self.m.edit.nb_rg);
2470        self.m.edit.nb_buf = nb_format(*v);
2471        self.nb_sync(*v as u32 as u64); // (U2)
2472        self.nb_caret_end();
2473    }
2474
2475    // the caret after the new text (when the owner is the editor)
2476    fn nb_caret_end(&mut self) {
2477        let e = &mut self.m.edit;
2478        if e.nb_id != 0 && e.ed_id == e.nb_id && e.ed_win == e.nb_win {
2479            e.ed_caret = e.nb_buf.len();
2480            e.ed_anchor = e.nb_buf.len();
2481        }
2482    }
2483
2484    // (U2) nb_sync(shown): nb_buf now shows the value with these bits: remember that text
2485    // (nb_orig) and value (nb_shown); the box counts as edited as soon as its text differs
2486    fn nb_sync(&mut self, shown: u64) {
2487        let e = &mut self.m.edit;
2488        e.nb_orig.clear();
2489        e.nb_orig.extend_from_slice(&e.nb_buf[..e.nb_buf.len().min(NB_CAP as usize - 1)]);
2490        e.nb_shown = shown;
2491    }
2492
2493    // (U2) nb_edited() -> the user changed the text since it was formatted
2494    fn nb_edited(&self) -> bool {
2495        self.m.edit.nb_buf != self.m.edit.nb_orig
2496    }
2497
2498    // (U2) nb_follow(value bits, decimals): the value of the focused number box changed
2499    // elsewhere (another widget, a command, a tool) while its text is unedited: show the new
2500    // value; the caret goes to the end, a selection of the whole text stays a selection of
2501    // the whole text
2502    fn nb_follow(&mut self, cur: u64, dec: u32) {
2503        if cur == self.m.edit.nb_shown || self.nb_edited() {
2504            return; // the typed text wins
2505        }
2506        let e = &self.m.edit;
2507        let old = e.nb_buf.len();
2508        let (lo, hi) = if e.ed_caret >= e.ed_anchor { (e.ed_anchor, e.ed_caret) } else { (e.ed_caret, e.ed_anchor) };
2509        let all = lo == 0 && hi == old && old != 0;
2510        let t = if e.nb_kind == 0 { nb_format(cur as u32 as i32) } else { self.nbf_format(f64::from_bits(cur), dec) };
2511        self.m.edit.nb_buf = t;
2512        self.nb_sync(cur);
2513        let e = &mut self.m.edit;
2514        if e.ed_id == e.nb_id && e.ed_win == e.nb_win {
2515            e.ed_caret = e.nb_buf.len();
2516            e.ed_anchor = e.nb_buf.len();
2517            if all {
2518                e.ed_anchor = 0;
2519            }
2520        }
2521    }
2522
2523    // nb_release: the box owning nb_buf commits its text (another box takes it).  The asm
2524    // stores the text into the owner's value here (through its saved &v); see pend_text.
2525    fn nb_release(&mut self) {
2526        let old = self.m.edit.nb_id;
2527        if old == 0 {
2528            return;
2529        }
2530        self.m.edit.nb_id = 0;
2531        if self.frame.wrapping_sub(self.m.edit.nb_seen) > 2 {
2532            return; // not drawn lately: drop the text
2533        }
2534        if self.nb_edited() {
2535            // (U2) not edited: nothing is stored
2536            let e = &mut self.m.edit;
2537            e.pend_text = Some((e.nb_win, old, e.nb_kind, e.nb_buf.clone()));
2538            self.repass();
2539        }
2540        self.m.edit.nb_pend = old;
2541        self.m.edit.nb_pend_win = self.m.edit.nb_win;
2542        self.m.edit.nb_pend_ev = EV_COMMIT;
2543    }
2544
2545    // a box of the live window released the number box text earlier in this frame (nb_release)
2546    fn nb_pend_is(&self, id: u32) -> bool {
2547        let e = &self.m.edit;
2548        e.nb_pend == id && e.nb_pend_win == self.uiw_cur
2549    }
2550
2551    // (port) the store nb_release left for the box (its next call, before it reads v):
2552    // parse, clamp / fix, store; EV_ACT joins the pending events when the value changed
2553    fn nb_pend_store(&mut self, id: u32, vi: Option<&mut i32>, vf: Option<&mut f64>, rg: Option<&Range>, rf: Option<&RangeF>) {
2554        match &self.m.edit.pend_text {
2555            Some((w, pid, _, _)) if *pid == id && *w == self.uiw_cur => {}
2556            _ => return,
2557        }
2558        let Some((_, _, kind, t)) = self.m.edit.pend_text.take() else { return };
2559        let changed = match (kind, vi, vf, rg, rf) {
2560            (0, Some(v), _, Some(rg), _) => {
2561                let old = *v;
2562                *v = rg_clamp(nb_parse_text(&t, *v), rg);
2563                *v != old
2564            }
2565            (1, _, Some(v), _, Some(rf)) => {
2566                let old = v.to_bits();
2567                let x = parse_f64_sep(&t, self.m.value.decimal_sep).unwrap_or(*v);
2568                *v = rf_fix(rf, x);
2569                v.to_bits() != old
2570            }
2571            _ => false,
2572        };
2573        if changed && self.nb_pend_is(id) {
2574            self.m.edit.nb_pend_ev |= EV_ACT;
2575        }
2576    }
2577}
2578
2579// ------------------------------------------------------------------ editors of their own
2580// (ROADMAP §6: richtext in rich_edit.rs, code_editor in code_edit.rs; see ed_core.rs)
2581
2582impl Ui {
2583    /// an editor with a text model of its own holds the edit state while it has the focus:
2584    /// its K_TEXTBOX record's text has the selection anchor .. caret (byte offsets).  The key
2585    /// filter of app-level key(), textbox_selection / textbox_select and a screen reader's
2586    /// selection then work as for a text box; a text box that held the state commits.
2587    pub(crate) fn ed_hold(&mut self, id: u32, anchor: usize, caret: usize) {
2588        let w = self.uiw_cur;
2589        let e = &mut self.m.edit;
2590        if e.ed_id != id || e.ed_win != w {
2591            if e.ed_id != 0 {
2592                e.ed_blur = e.ed_id;
2593                e.ed_blur_win = e.ed_win;
2594            }
2595            e.ed_id = id;
2596            e.ed_win = w;
2597            e.ed_drag = false;
2598            self.ud_reset();
2599        }
2600        let frame = self.frame;
2601        let e = &mut self.m.edit;
2602        if e.ed_blur == id && e.ed_blur_win == w {
2603            e.ed_blur = 0;
2604        }
2605        e.ed_anchor = anchor;
2606        e.ed_caret = caret;
2607        e.ed_seen = frame;
2608        e.ed_multi = true;
2609        e.ed_prefx = -1;
2610    }
2611
2612    /// the editor lost the focus: it gives the edit state back (the IME and magnifiers lose
2613    /// its caret)
2614    pub(crate) fn ed_drop(&mut self, id: u32) {
2615        if self.ed_is(id) {
2616            self.m.edit.ed_id = 0;
2617            self.ime_off();
2618        }
2619    }
2620}