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