Skip to main content

ui/
basic.rs

1// basic -- the basic widgets                                           [src/ui/ui_basic.s]
2//
3//   text(style, s)                         TS_* | TX_* flags (TX_ELLIPSIS: cut with "…" at
4//                                          the placed width, record value 1 when cut;
5//                                          TX_WRAP: lines at the layout width, value = lines;
6//                                          TX_HYPHENATE: words that do not fit hyphenated,
7//                                          hyph.rs; soft hyphens always)
8//   button(id, label) -> EV                standard button
9//   accent_button(id, label) -> EV         accent (primary) button
10//   icon_button(id, icon, label) -> EV     subtle toolbar button (label = tooltip / name;
11//                                          ST_LARGE shows it under a big icon)
12//   toggle_button(id, label, &mut v) -> EV checked look while v != 0; toggles v
13//   tool_button(id, icon, label, &mut sel, mine) -> EV   selects sel = mine (tool palettes)
14//   checkbox(id, label, &mut v) -> EV      v: 0, 1, 2 = indeterminate
15//   radio(id, label, &mut v, mine) -> EV   v = mine when clicked
16//   checkbox_wrap / radio_wrap             the same with a label that wraps (next_size(-1, 0)): a mark
17//                                          beside a long text, a click on the text counts
18//   switch(id, label, &mut v) -> EV        toggle switch
19//   slider(id, &mut v, &range) -> EV       EV_ACT while the value changes, EV_COMMIT on release
20//   progress(id, v, max)                   progress bar
21//   separator()                            divider line across the current box
22//   icon(cp, size_dip)                     an icon in the text colour
23//   image(id, &sf, ver) -> EV              a surface at its natural size (or next_size)
24//   canvas(id, draw, user, version) -> &UiInput   custom area: draw.draw(g, rect) at render
25//                                          time, clipped; bump `version` when its content
26//                                          changes
27//   hair() -> px                           border width for the current dpi
28// Label strings are copied, so they may live in scratch buffers.
29
30use alloc::rc::Rc;
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use super::cmd::DrawRef;
35use super::consts::*;
36use super::keys::*;
37use super::value::part_id;
38use super::{Sp, Ui};
39use gfx::Surface;
40
41const ELL_MAX: usize = 1024; // ellipsis buffer (bytes)
42const WRAP_MAX: usize = 1 << 16; // lines of a TX_WRAP text (more: limit_hit)
43
44// a wrapped line: byte offset in the text, bytes drawn (without trailing blanks), width px
45// (with the hyphen), hyphenated (a hyphen drawn after it)
46#[derive(Clone, Copy)]
47struct Wl {
48    start: usize,
49    len: usize,
50    w: i32,
51    hy: bool,
52}
53
54/// the text up to its first NUL (the asm's strings end there)
55pub(crate) fn cstr(s: &str) -> &str {
56    match s.find('\0') {
57        Some(i) => &s[..i],
58        None => s,
59    }
60}
61
62// utf8_next(s, i) -> index of the next character (i itself at the end)
63pub(crate) fn next_char(s: &[u8], i: usize) -> usize {
64    if i >= s.len() || s[i] == 0 {
65        return i;
66    }
67    let (_, n) = base::utf8_decode(s, i);
68    n
69}
70
71impl Ui {
72    /// ui_hair: max(1, (dpi + 24) / 96)
73    pub fn hair(&self) -> i32 {
74        (((self.wp.rt.dpi as u32).wrapping_add(24)) / 96).max(1) as i32
75    }
76
77    // the colour of a text style's TX_* flags (later flags win; disabled last)
78    pub(crate) fn text_color(&self, style: u32) -> u32 {
79        let mut t = T_TEXT;
80        if style & TX_SECONDARY != 0 {
81            t = T_TEXT2;
82        }
83        if style & TX_TERTIARY != 0 {
84            t = T_TEXT3;
85        }
86        if style & TX_ACCENT != 0 {
87            t = T_ACCENT_TEXT;
88        }
89        if style & TX_ON_ACCENT != 0 {
90            t = T_ON_ACCENT;
91        }
92        if style & TX_DISABLED != 0 || self.wp.ctx.disabled_depth != 0 {
93            t = T_TEXT_DISABLED;
94        }
95        self.color(t)
96    }
97
98    /// ui_focus_ring_draw(ev): the style's focus visual around last_rect when it is shown
99    /// (ev: the widget's events; custom widgets built on hit_rect / behave call it last)
100    pub fn focus_ring_draw(&mut self, ev: u32) {
101        if self.focus_ring(ev) {
102            let st = self.style;
103            let r = self.wp.ctx.last_rect;
104            st.focus_ring(self, r, 4); // corner radius of the controls (DIP)
105        }
106    }
107
108    // sp_placed(sp, ev): the placed part of an SP record: last_rect, the events and the
109    // state index (disabled when the last widget is SS_DISABLED)
110    pub(crate) fn sp_placed(&self, sp: &mut Sp, ev: u32) {
111        sp.place(self.wp.ctx.last_rect);
112        sp.ev = ev;
113        sp.state = if self.wp.ctx.last_state == SS_DISABLED as u32 {
114            3
115        } else if ev & EV_HOVER == 0 {
116            0
117        } else if ev & EV_HELD == 0 {
118            1
119        } else {
120            2
121        };
122    }
123
124    // ------------------------------------------------------------------ text
125
126    /// ui_text(style, s): a text in a text style (TS_* | TX_*)
127    #[cfg_attr(jfgui_dev, track_caller)]
128    pub fn text(&mut self, style: u32, s: &str) {
129        let _src = self.src_scope();
130        let s = cstr(s);
131        if style & TX_WRAP != 0 {
132            self.text_wrap(style, s); // (U2) wrapped text: own function
133            return;
134        }
135        let base = self.ts_base(style & 7); // (also selects the font)
136        let (font, size) = self.text.get_font();
137        let mut w = self.text.width(s);
138        let line = self.ts_line(style & 7);
139        let r = self.place(w, line);
140        // (U1) ellipsis when the placed width is too small
141        let mut cut = 0;
142        let mut drawn: Option<String> = None;
143        if style & TX_ELLIPSIS != 0 && w > r[2] {
144            let (e, ew) = self.ellipsize(s.as_bytes(), r[2]);
145            drawn = Some(e);
146            w = ew;
147            cut = 1;
148        }
149        let c = self.text_color(style);
150        let mut x = r[0];
151        let spare = r[2].wrapping_sub(w);
152        if style & TX_CENTER != 0 {
153            x = x.wrapping_add(spare >> 1);
154        } else if style & TX_RIGHT != 0 {
155            x = x.wrapping_add(spare);
156        }
157        let y = r[1].wrapping_add((r[3].wrapping_sub(line)) >> 1).wrapping_add(base);
158        let t = drawn.as_deref().unwrap_or(s);
159        self.cmd_text(x, y, t.as_bytes(), font, size, c, r[2]); // clipped to the placed width
160        self.wp.ctx.last_state = 0;
161        self.sem(K_TEXT, 0, Some(s), cut);
162    }
163
164    /// A section title: `text(style, s)` that screen readers know as a heading of `level`
165    /// (1 the top level ..= 9; record K_HEADING, value: the level) -- their heading navigation
166    /// finds it.
167    #[cfg_attr(jfgui_dev, track_caller)]
168    pub fn heading(&mut self, level: u32, style: u32, s: &str) {
169        let _src = self.src_scope();
170        let n = self.sems.len();
171        self.text(style, s);
172        if let Some(r) = self.sems.get_mut(n) {
173            r.kind = K_HEADING;
174            r.value = level.clamp(1, 9) as i32;
175        }
176    }
177
178    // text_wrap(style, s): text with TX_WRAP: broken into lines at the width the layout
179    // gives it (avail_width): after the last blank that fits, inside a word that is wider
180    // than the line, and at '\n'.  Natural height = lines x line height; natural width =
181    // the unwrapped width where the layout stretches it, else the widest line.  TX_CENTER /
182    // TX_RIGHT align each line; TX_ELLIPSIS cuts the text with "…" when a fixed height has
183    // room for fewer lines.  Semantic record: the whole text, value = number of lines.
184    fn text_wrap(&mut self, style: u32, s: &str) {
185        let b = s.as_bytes();
186        let hy = self.hyph_take(style & TX_HYPHENATE != 0); // (hyph.rs)
187        let hy = hy.as_deref();
188        let base = self.ts_base(style & 7); // (also selects the font)
189        let (font, size) = self.text.get_font();
190        let line = self.ts_line(style & 7);
191        // unwrapped width: the widest paragraph
192        let mut uw = 0;
193        let mut p = 0;
194        loop {
195            let w = self.text.width(&s[p..]);
196            if w > uw {
197                uw = w;
198            }
199            match b[p..].iter().position(|&c| c == b'\n') {
200                Some(i) => p += i + 1,
201                None => break,
202            }
203        }
204        // the width the layout will give us; <= 0 (unknown yet): no wrapping
205        let (aw, fixed) = self.avail_width(uw);
206        let mut ww = if aw > 0 { aw } else { 0x7FFF_FFFF };
207        let mut lines = Vec::new();
208        let mut widest = self.wrap_lines(b, ww, &mut lines, hy);
209        // place: natural width = unwrapped (stretched / fixed) or the widest line
210        let natw = if fixed != 0 { uw } else { widest };
211        let r = self.place(natw, (lines.len() as i32).wrapping_mul(line));
212        // placed at another width than the layout announced (should not happen): wrap again
213        let pw = r[2];
214        if fixed != 0 && pw > 0 && ww != 0x7FFF_FFFF && pw != ww {
215            ww = pw;
216            widest = self.wrap_lines(b, ww, &mut lines, hy);
217        }
218        let _ = widest;
219        let c = self.text_color(style);
220        // vertical position: centred when there is room, else from the top (TX_ELLIPSIS:
221        // only the lines that fit, the last one cut with "…")
222        let nlines = lines.len() as i32;
223        let mut ndraw = nlines;
224        let mut cut = false;
225        let spare = r[3].wrapping_sub(nlines.wrapping_mul(line));
226        let y0 = if spare >= 0 {
227            r[1].wrapping_add(spare >> 1)
228        } else {
229            if style & TX_ELLIPSIS != 0 {
230                let fit = if line != 0 { r[3] / line } else { 0 }.max(1);
231                if fit < ndraw {
232                    ndraw = fit;
233                    cut = true;
234                }
235            }
236            r[1]
237        };
238        for i in 0..ndraw {
239            let l = lines[i as usize];
240            let mut t: Vec<u8> = b[l.start..l.start + l.len].to_vec();
241            if l.hy {
242                t.push(b'-'); // hyphenated
243            }
244            let mut tw = l.w;
245            // the last line drawn when lines are cut: the rest of its paragraph + "…"
246            if cut && i + 1 == ndraw {
247                let rest = &b[l.start..];
248                let mut k = 0;
249                while k < ELL_MAX - 8 && k < rest.len() && rest[k] != 0 && rest[k] != b'\n' {
250                    k += 1;
251                }
252                // (a UTF-8 sequence cut at ELL_MAX - 8 is dropped by ellipsize's width test)
253                let (e, ew) = self.ellipsize(&rest[..k], pw);
254                t = e.into_bytes();
255                tw = ew;
256            }
257            // x: aligned inside the placed width
258            let mut x = r[0];
259            let sp = pw.wrapping_sub(tw);
260            if sp > 0 {
261                if style & TX_CENTER != 0 {
262                    x = x.wrapping_add(sp >> 1);
263                } else if style & TX_RIGHT != 0 {
264                    x = x.wrapping_add(sp);
265                }
266            }
267            let y = i.wrapping_mul(line).wrapping_add(y0).wrapping_add(base);
268            self.cmd_text(x, y, &t, font, size, c, pw); // clipped to the placed width
269        }
270        self.wp.ctx.last_state = 0;
271        self.sem(K_TEXT, 0, Some(s), nlines);
272    }
273
274    // wrap_lines(s, width, hyph) -> widest line (px); the lines (at most WRAP_MAX; the rest of
275    // a longer text is dropped with a warning).  Lines end at '\n' and where the text reaches `width`: after
276    // the last blank that fits (the blanks stay on the line but are not measured or drawn),
277    // after a soft hyphen, at a hyphenation point (hyph: TX_HYPHENATE) with a hyphen drawn,
278    // inside a word that is wider than a line (at a character boundary, at least one
279    // character per line).  Current font.
280    fn wrap_lines(&mut self, s: &[u8], width: i32, out: &mut Vec<Wl>, hyph: Option<&spell::Hyphenator>) -> i32 {
281        out.clear();
282        let mut widest = 0;
283        let mut pos = 0;
284        'para: loop {
285            // paragraph end: '\n' or the end
286            let pe = pos + s[pos..].iter().position(|&c| c == b'\n' || c == 0).unwrap_or(s.len() - pos);
287            loop {
288                if out.len() >= WRAP_MAX {
289                    self.limit_hit(super::ctx::LIM_WRAP);
290                    break 'para;
291                }
292                let (end, hy) = self.wrap_break(s, pos, pe, width, hyph);
293                // visible part: without the trailing blanks (ASCII and the ideographic space)
294                let mut v = end;
295                while v > pos && (s[v - 1] == b' ' || s[..v].ends_with("\u{3000}".as_bytes())) {
296                    v -= if s[v - 1] == b' ' { 1 } else { 3 };
297                }
298                let w = self.text.width_n(&s[pos..], v - pos) + if hy { self.text.width("-") } else { 0 };
299                out.push(Wl { start: pos, len: v - pos, w, hy });
300                if w > widest {
301                    widest = w;
302                }
303                pos = end;
304                if pos >= pe {
305                    break; // the paragraph is done
306                }
307            }
308            if pe >= s.len() || s[pe] != b'\n' {
309                break;
310            }
311            pos = pe + 1; // after the '\n'
312        }
313        widest
314    }
315
316    // wrap_break(s, pos, pe, width, hyph) -> (where the line starting at pos ends (pos < end
317    // <= pe, or pe when pos == pe), hyphenated): the whole rest when it fits, else the last line
318    // break opportunity (UAX #14: after spaces, after hyphens, after a soft hyphen, between
319    // ideographs ...) whose text fits, a hyphenation point of the next word (hyph), else (one
320    // long word) the grapheme clusters that fit (at least one)
321    fn wrap_break(&mut self, s: &[u8], pos: usize, pe: usize, width: i32, hyph: Option<&spell::Hyphenator>) -> (usize, bool) {
322        let hw = self.text.width("-");
323        let t = &mut self.text;
324        match core::str::from_utf8(s) {
325            Ok(st) => {
326                let mut fits = |a: usize, b: usize, h: bool| t.width_n(&s[a..], b - a) + if h { hw } else { 0 } <= width;
327                match hyph {
328                    Some(hy) => text::segment::wrap_end_hyph(st, pos, pe, &mut fits, Some(&mut |w: &str, out: &mut Vec<usize>| *out = hy.hyphenate(w))),
329                    None => text::segment::wrap_end_hyph(st, pos, pe, &mut fits, None),
330                }
331            }
332            Err(_) => {
333                // not UTF-8: characters while they fit (at least one)
334                let mut best = next_char(s, pos);
335                while best < pe {
336                    let n = next_char(s, best);
337                    if t.width_n(&s[pos..], n - pos) > width {
338                        break;
339                    }
340                    best = n;
341                }
342                (best.min(pe), false)
343            }
344        }
345    }
346
347    // ellipsize(s, maxw) -> (the longest prefix of s that fits into maxw px together with
348    // "…" + "…", its width) (current font; trailing blanks dropped)
349    pub(crate) fn ellipsize(&mut self, s: &[u8], maxw: i32) -> (String, i32) {
350        let avail = maxw.wrapping_sub(self.text.width("…"));
351        let mut n = 0;
352        if avail > 0 {
353            loop {
354                if n >= s.len() || s[n] == 0 {
355                    break;
356                }
357                let m = next_char(s, n); // length with the next character
358                if m > ELL_MAX - 4 {
359                    break;
360                }
361                if self.text.width_n(s, m) > avail {
362                    break;
363                }
364                n = m;
365            }
366        }
367        while n > 0 && s[n - 1] == b' ' {
368            n -= 1;
369        }
370        let mut out = String::from_utf8_lossy(&s[..n]).into_owned();
371        out.push('…'); // U+2026 HORIZONTAL ELLIPSIS
372        let w = self.text.width(&out);
373        (out, w)
374    }
375
376    // ------------------------------------------------------------------ buttons
377
378    // btn_core(id, label, flags) -> EV: measure (style), place, behave, paint (style), focus
379    // ring, semantics, tooltip of icon-only buttons
380    fn btn_core(&mut self, id: u32, label: Option<&str>, flags: u32) -> u32 {
381        let label = label.map(cstr);
382        let flags = flags | self.take_flags();
383        let icon = self.take_icon();
384        let st = self.style;
385        let mut sp = Sp { flags, icon, label, ..Sp::default() };
386        let (w, h) = st.button_size(self, &mut sp);
387        let square = icon != 0 && (flags & ST_ICON_ONLY != 0 || label.is_none_or(str::is_empty));
388        let (w, h) = self.ctrl_size(w, h, flags, square); // (theme) control height
389        self.place(w, h);
390        let mut hb = if flags & ST_NOFOCUS != 0 { 0 } else { HB_FOCUS };
391        if flags & ST_DISABLED != 0 {
392            hb |= HB_DISABLED;
393        }
394        if flags & ST_CHECKED != 0 {
395            hb |= HB_CURRENT; // a focus group is entered at its checked button (kbnav.rs)
396        }
397        let ev = self.behave(id, hb);
398        self.sp_placed(&mut sp, ev);
399        st.button(self, &sp);
400        self.focus_ring_draw(ev);
401        if flags & ST_CHECKED != 0 {
402            self.wp.ctx.last_state |= SS_CHECKED as u32;
403        }
404        self.sem(K_BUTTON, id, label, 0);
405        // (popup agent) icon-only buttons show their label as a tooltip
406        if flags & ST_ICON_ONLY != 0 {
407            if let Some(l) = label {
408                self.tooltip(l);
409            }
410        }
411        ev
412    }
413
414    /// ui_button(id, label) -> EV_* (EV_ACT on click)
415    #[cfg_attr(jfgui_dev, track_caller)]
416    pub fn button(&mut self, id: u32, label: &str) -> u32 {
417        let _src = self.src_scope();
418        self.btn_core(id, Some(label), 0)
419    }
420
421    /// A focusable text action with explicit TS_*/TX_* typography and horizontal inset
422    /// in DIP. Uses the style's button surface and ordinary button semantics. Long labels
423    /// are ellipsized visually; accessibility and agent inspection retain the full label.
424    #[cfg_attr(jfgui_dev, track_caller)]
425    pub fn label_button(&mut self, id: u32, label: &str, typography: u32, inset: i32) -> u32 {
426        let _src = self.src_scope();
427        let label = cstr(label);
428        let flags = self.take_flags();
429        self.take_icon();
430        let inset = self.dp(inset.max(0));
431        self.ts_font(typography & 7);
432        let width = self.text.width(label) + inset * 2;
433        let line = self.ts_line(typography & 7);
434        let r = self.place(width, line);
435        let hb = if flags & ST_NOFOCUS != 0 { 0 } else { HB_FOCUS }
436            | if flags & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
437        let ev = self.behave(id, hb);
438        let mut sp = Sp { flags, ..Sp::default() };
439        self.sp_placed(&mut sp, ev);
440        self.style.button(self, &sp);
441        let base = self.ts_base(typography & 7);
442        let available = (r[2] - inset * 2).max(0);
443        let width = self.text.width(label);
444        let (display, width) = if width > available {
445            self.ellipsize(label.as_bytes(), available)
446        } else { (String::from(label), width) };
447        let x = r[0] + inset + if typography & TX_CENTER != 0 {
448            (available - width) / 2
449        } else if typography & TX_RIGHT != 0 { available - width } else { 0 };
450        let color = if flags & (ST_ACCENT | ST_ON_ACCENT) != 0 {
451            self.color(if sp.state == 3 { T_ON_ACCENT_DIS } else { T_ON_ACCENT })
452        } else if sp.state == 3 { self.color(T_TEXT_DISABLED) }
453        else { self.text_color(typography) };
454        let (font, size) = self.text.get_font();
455        self.draw_clip(r[0], r[1], r[2], r[3]);
456        self.cmd_text(x, r[1] + (r[3] - line) / 2 + base, display.as_bytes(), font, size, color, available);
457        self.draw_unclip();
458        self.focus_ring_draw(ev);
459        if flags & ST_CHECKED != 0 {
460            self.wp.ctx.last_state |= SS_CHECKED as u32;
461        }
462        self.sem(K_BUTTON, id, Some(label), 0);
463        ev
464    }
465
466    /// A hyperlink-style text action: accent-coloured text (T_ACCENT_TEXT), underlined while
467    /// the pointer is over it, hand cursor, focus ring, no button surface.  TS_*/TX_*
468    /// typography as in text(); a narrower slot than the text ellipsizes it.
469    #[cfg_attr(jfgui_dev, track_caller)]
470    pub fn link(&mut self, id: u32, label: &str, typography: u32) -> u32 {
471        let _src = self.src_scope();
472        let label = cstr(label);
473        let flags = self.take_flags();
474        self.take_icon();
475        self.ts_font(typography & 7);
476        let line = self.ts_line(typography & 7);
477        let natural = self.text.width(label);
478        let r = self.place(natural, line);
479        let hb = HB_FOCUS | CUR_HAND << HB_CURSOR_SHIFT | if flags & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
480        let ev = self.behave(id, hb);
481        let (display, width) = if natural > r[2] {
482            self.ellipsize(label.as_bytes(), r[2])
483        } else { (String::from(label), natural) };
484        let color = if flags & ST_DISABLED != 0 {
485            self.color(T_TEXT_DISABLED)
486        } else if ev & EV_HELD != 0 { self.color(T_TEXT2) } else { self.color(T_ACCENT_TEXT) };
487        let base = self.ts_base(typography & 7);
488        let y = r[1] + (r[3] - line) / 2;
489        let (font, size) = self.text.get_font();
490        self.draw_clip(r[0], r[1], r[2], r[3]);
491        self.cmd_text(r[0], y + base, display.as_bytes(), font, size, color, r[2]);
492        if ev & EV_HOVER != 0 && flags & ST_DISABLED == 0 {
493            self.draw_color(color);
494            self.draw_rect(r[0], y + base + self.dp(2), width, self.dp(1).max(1));
495        }
496        self.draw_unclip();
497        self.focus_ring_draw(ev);
498        self.sem(K_LINK, id, Some(label), 0);
499        ev
500    }
501
502    /// ui_accent_button(id, label) -> EV_*: the accent (primary) button
503    #[cfg_attr(jfgui_dev, track_caller)]
504    pub fn accent_button(&mut self, id: u32, label: &str) -> u32 {
505        let _src = self.src_scope();
506        self.btn_core(id, Some(label), ST_ACCENT)
507    }
508
509    /// ui_icon_button(id, icon, label) -> EV_*: subtle toolbar button
510    #[cfg_attr(jfgui_dev, track_caller)]
511    pub fn icon_button(&mut self, id: u32, icon: u32, label: &str) -> u32 {
512        let _src = self.src_scope();
513        self.next_icon(icon);
514        self.btn_icon(id, Some(label), ST_SUBTLE | ST_ICON_ONLY)
515    }
516
517    // btn_icon: ST_LARGE requested by the caller keeps the label visible
518    fn btn_icon(&mut self, id: u32, label: Option<&str>, flags: u32) -> u32 {
519        let f = self.take_flags();
520        let mut flags = flags;
521        if f & ST_LARGE != 0 {
522            flags &= !ST_ICON_ONLY;
523        }
524        self.btn_core(id, label, flags | f)
525    }
526
527    /// ui_toggle_button(id, label, &mut v) -> EV_*: checked look while v != 0; toggles v
528    #[cfg_attr(jfgui_dev, track_caller)]
529    pub fn toggle_button(&mut self, id: u32, label: &str, v: &mut i32) -> u32 {
530        let _src = self.src_scope();
531        let ev = self.btn_core(id, Some(label), if *v != 0 { ST_CHECKED } else { 0 });
532        if ev & EV_ACT != 0 {
533            *v = (*v ^ 1) & 1;
534            self.repass();
535        }
536        ev
537    }
538
539    /// ui_tool_button(id, icon, label, &mut sel, mine) -> EV_*: subtle icon button, checked
540    /// when sel == mine; the tool buttons of one variable are one Tab stop (arrows inside)
541    #[cfg_attr(jfgui_dev, track_caller)]
542    pub fn tool_button(&mut self, id: u32, icon: u32, label: &str, sel: &mut i32, mine: i32) -> u32 {
543        let _src = self.src_scope();
544        self.kb_auto_group(&*sel as *const i32 as usize, mine);
545        self.next_icon(icon);
546        let mut f = ST_SUBTLE | ST_ICON_ONLY;
547        if *sel == mine {
548            f |= ST_CHECKED;
549        }
550        let ev = self.btn_icon(id, Some(label), f);
551        if ev & EV_ACT != 0 {
552            *sel = mine;
553            self.repass();
554        }
555        ev
556    }
557
558    // ------------------------------------------------------------------ check box, radio, switch
559
560    /// ui_checkbox(id, label, &mut v) -> EV_*: v 0, 1, 2 = indeterminate (Space toggles,
561    /// Enter is the dialog's)
562    #[cfg_attr(jfgui_dev, track_caller)]
563    pub fn checkbox(&mut self, id: u32, label: &str, v: &mut i32) -> u32 {
564        let _src = self.src_scope();
565        let label = cstr(label);
566        let f = self.take_flags();
567        let st = self.style;
568        let mut sp = Sp { flags: f, label: Some(label), ..Sp::default() };
569        let (w, h) = st.checkbox_size(self, &mut sp);
570        self.place(w, h);
571        let hb = HB_FOCUS | HB_SPACE | if f & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
572        let mut ev = self.behave(id, hb);
573        if f & ST_DISABLED == 0 && core::mem::take(&mut self.m.kbnav.toggle) {
574            ev |= EV_ACT; // Space on a focused list: the selected row's check box
575        }
576        if ev & EV_ACT != 0 {
577            *v = if *v == 1 { 0 } else { 1 };
578            self.repass();
579        }
580        self.sp_placed(&mut sp, ev);
581        sp.value = *v;
582        st.checkbox(self, &sp);
583        self.focus_ring_draw(ev);
584        if *v != 0 {
585            self.wp.ctx.last_state |= SS_CHECKED as u32;
586        }
587        self.sem(K_CHECKBOX, id, Some(label), *v);
588        ev
589    }
590
591    /// ui_checkbox_wrap(id, label, &mut v) -> EV_*: a check box for a text that may be long: the
592    /// mark, beside it the label wrapped at the width the layout gives (next_size(-1, 0)); a click
593    /// on the text toggles too; the record is the check box's, named by the text
594    #[cfg_attr(jfgui_dev, track_caller)]
595    pub fn checkbox_wrap(&mut self, id: u32, label: &str, v: &mut i32) -> u32 {
596        let _src = self.src_scope();
597        self.box_begin(0, BOX_H, 6);
598        let mut ev = self.checkbox(id, "", v);
599        self.sem_label_last(label);
600        if self.wrap_label(id, label) {
601            *v = if *v == 1 { 0 } else { 1 };
602            ev |= EV_ACT;
603            self.repass();
604        }
605        self.box_end();
606        ev
607    }
608
609    /// ui_radio_wrap(id, label, &mut v, mine) -> EV_*: a radio for a text that may be long, as
610    /// checkbox_wrap
611    #[cfg_attr(jfgui_dev, track_caller)]
612    pub fn radio_wrap(&mut self, id: u32, label: &str, v: &mut i32, mine: i32) -> u32 {
613        let _src = self.src_scope();
614        self.box_begin(0, BOX_H, 6);
615        let mut ev = self.radio(id, "", v, mine);
616        self.sem_label_last(label);
617        if self.wrap_label(id, label) && *v != mine {
618            *v = mine;
619            ev |= EV_ACT;
620            self.repass();
621        }
622        self.box_end();
623        ev
624    }
625
626    // the record just made gets this label
627    fn sem_label_last(&mut self, label: &str) {
628        let label = cstr(label);
629        if let Some(s) = self.sems.last_mut() {
630            s.label = Some(String::from(label));
631        }
632    }
633
634    // the wrapped label of a mark: no record of its own; a click on it goes to the mark -> it was clicked
635    fn wrap_label(&mut self, id: u32, label: &str) -> bool {
636        let n = self.sems.len();
637        self.next_size(-1, 0);
638        self.text(TS_BODY | TX_WRAP, label);
639        self.sems.truncate(n);
640        let clicked = self.behave(part_id(id, 1), 0) & EV_ACT != 0;
641        if clicked {
642            self.focus_quiet(id);
643        }
644        clicked
645    }
646
647    /// ui_radio(id, label, &mut v, mine) -> EV_*: v = mine when clicked; the radios of one
648    /// variable are one Tab stop, the arrows select inside (kbnav.rs)
649    #[cfg_attr(jfgui_dev, track_caller)]
650    pub fn radio(&mut self, id: u32, label: &str, v: &mut i32, mine: i32) -> u32 {
651        let _src = self.src_scope();
652        let label = cstr(label);
653        let f = self.take_flags();
654        let st = self.style;
655        let mut sp = Sp { flags: f, label: Some(label), ..Sp::default() };
656        let (w, h) = st.radio_size(self, &mut sp);
657        self.place(w, h);
658        let mut hb = HB_FOCUS | HB_SPACE | if f & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
659        if *v == mine {
660            hb |= HB_CURRENT;
661        }
662        self.kb_auto_group(&*v as *const i32 as usize, mine);
663        let mut ev = self.behave(id, hb);
664        if ev & EV_FOCUS != 0 && self.kb_moved(id) && *v != mine {
665            ev |= EV_ACT; // the selection follows the arrows
666        }
667        if ev & EV_ACT != 0 {
668            *v = mine;
669            self.repass();
670        }
671        self.sp_placed(&mut sp, ev);
672        sp.value = (*v == mine) as i32;
673        st.radio(self, &sp);
674        self.focus_ring_draw(ev);
675        if *v == mine {
676            self.wp.ctx.last_state |= SS_CHECKED as u32;
677        }
678        self.sem(K_RADIO, id, Some(label), mine);
679        ev
680    }
681
682    /// ui_switch(id, label, &mut v) -> EV_*: toggle switch
683    #[cfg_attr(jfgui_dev, track_caller)]
684    pub fn switch(&mut self, id: u32, label: &str, v: &mut i32) -> u32 {
685        let _src = self.src_scope();
686        let label = cstr(label);
687        let f = self.take_flags();
688        let st = self.style;
689        let mut sp = Sp { flags: f, label: Some(label), ..Sp::default() };
690        let (w, h) = st.switch_size(self, &mut sp);
691        self.place(w, h);
692        let hb = HB_FOCUS | HB_SPACE | if f & ST_DISABLED != 0 { HB_DISABLED } else { 0 };
693        let mut ev = self.behave(id, hb);
694        if f & ST_DISABLED == 0 && core::mem::take(&mut self.m.kbnav.toggle) {
695            ev |= EV_ACT; // Space on a focused list: the selected row's switch
696        }
697        if ev & EV_ACT != 0 {
698            *v = (*v ^ 1) & 1;
699            self.repass();
700        }
701        // knob position animation 0 (off) .. 256 (on)
702        let a = self.anim(id, if *v != 0 { 256 } else { 0 }, 167);
703        self.sp_placed(&mut sp, ev);
704        sp.value = *v;
705        sp.value2 = a as i32;
706        st.switch(self, &sp);
707        self.focus_ring_draw(ev);
708        if *v != 0 {
709            self.wp.ctx.last_state |= SS_CHECKED as u32;
710        }
711        self.sem(K_SWITCH, id, Some(label), *v);
712        ev
713    }
714
715    // ------------------------------------------------------------------ slider
716
717    /// ui_slider(id, &mut v, &range) -> EV_*: EV_ACT while the value changes, EV_COMMIT on
718    /// release
719    #[cfg_attr(jfgui_dev, track_caller)]
720    pub fn slider(&mut self, id: u32, v: &mut i32, rg: &Range) -> u32 {
721        let _src = self.src_scope();
722        let old = *v;
723        let f = self.take_flags();
724        let vert = (rg.flags & RG_VERT) as i32;
725        let st = self.style;
726        // measure (style): s[0] = thumb px
727        let mut sp = Sp { flags: f, value: vert, ..Sp::default() };
728        let (w, h) = st.slider_size(self, &mut sp);
729        let thumb = sp.s[0];
730        let r = self.place(w, h);
731        let mut hb = HB_FOCUS | CUR_HAND << HB_CURSOR_SHIFT | if vert != 0 { HB_GRAB_V } else { HB_GRAB_H };
732        if f & ST_DISABLED != 0 {
733            hb |= HB_DISABLED;
734        }
735        let mut ev = self.behave(id, hb);
736        // track length along the axis (minus the thumb)
737        let len = (if vert != 0 { r[3] } else { r[2] }).wrapping_sub(thumb).max(1);
738        let snap = |x: i32| {
739            if rg.step <= 1 {
740                return x;
741            }
742            (x.wrapping_sub(rg.min).wrapping_add(rg.step >> 1) / rg.step).wrapping_mul(rg.step).wrapping_add(rg.min)
743        };
744        if ev & EV_HELD != 0 {
745            // pointer drag: value from the position (vertical: max at the top)
746            let (mx, my) = (self.wp.ctx.mx, self.wp.ctx.my);
747            let mut pos = if vert != 0 { r[1].wrapping_add(r[3]).wrapping_sub(my) } else { mx.wrapping_sub(r[0]) };
748            pos = pos.wrapping_sub(thumb >> 1); // relative to the track start
749            if pos < 0 {
750                pos = 0;
751            }
752            if pos > len {
753                pos = len;
754            }
755            *v = snap(rg.value_at(pos, len));
756        } else if ev & EV_FOCUS != 0 {
757            let step = if rg.step != 0 { rg.step } else { 1 };
758            let a = self.key(VK_RIGHT);
759            let b = self.key(VK_UP);
760            if a || b {
761                *v = v.wrapping_add(step);
762                self.wp.ctx.focus_visible = true;
763            }
764            let a = self.key(VK_LEFT);
765            let b = self.key(VK_DOWN);
766            if a || b {
767                *v = v.wrapping_sub(step);
768                self.wp.ctx.focus_visible = true;
769            }
770            if self.key(VK_HOME) {
771                *v = rg.min;
772            }
773            if self.key(VK_END) {
774                *v = rg.max;
775            }
776        }
777        // a value a screen reader set (a11y.rs)
778        if let Some(x) = self.a11y_value(id) {
779            *v = snap(x);
780        }
781        if *v < rg.min {
782            *v = rg.min;
783        }
784        if *v > rg.max {
785            *v = rg.max;
786        }
787        if *v != old {
788            ev |= EV_ACT;
789            self.repass();
790        }
791        // thumb offset along the track: (v - min) * len / (max - min)
792        let t = rg.position(*v, len);
793        self.sp_placed(&mut sp, ev);
794        sp.value = t; // thumb offset along the track (px)
795        sp.value2 = len; // track length (px)
796        st.slider(self, &sp);
797        self.focus_ring_draw(ev);
798        let r = self.sem(K_SLIDER, id, None, *v);
799        r.max = rg.max;
800        r.min = rg.min;
801        ev
802    }
803
804    // ------------------------------------------------------------------ progress, separator, icon, image
805
806    /// ui_progress(id, v, max): progress bar
807    #[cfg_attr(jfgui_dev, track_caller)]
808    pub fn progress(&mut self, id: u32, v: i32, max: i32) {
809        let _src = self.src_scope();
810        let st = self.style;
811        let mut sp = Sp::default();
812        let (w, h) = st.progress_size(self, &mut sp);
813        self.place(w, h);
814        self.sp_placed(&mut sp, 0);
815        sp.value = v;
816        sp.value2 = max;
817        st.progress(self, &sp);
818        self.wp.ctx.last_state = 0;
819        self.sem(K_PROGRESS, id, None, v).max = max;
820    }
821
822    /// ui_separator: divider across the current box (the style draws it)
823    #[cfg_attr(jfgui_dev, track_caller)]
824    pub fn separator(&mut self) {
825        let _src = self.src_scope();
826        let st = self.style;
827        let mut sp = Sp::default();
828        let t = st.separator_size(self, &mut sp); // thickness incl. margins
829        let horizontal = self.box_flags() & BOX_V != 0;
830        if horizontal {
831            // in a vertical box: a horizontal line across the full width
832            self.next_size(-1, 0);
833            self.place(0, t);
834        } else {
835            self.next_size(0, -1);
836            self.place(t, 0);
837        }
838        self.sp_placed(&mut sp, 0);
839        sp.value = horizontal as i32; // 1 = horizontal line
840        st.separator(self, &sp);
841    }
842
843    /// ui_icon(cp, size_dip): an icon in the text colour
844    #[cfg_attr(jfgui_dev, track_caller)]
845    pub fn icon(&mut self, cp: u32, size: i32) {
846        let _src = self.src_scope();
847        let s = self.dp(size);
848        let r = self.place(s, s);
849        let c = self.color(if self.wp.ctx.disabled_depth != 0 { T_TEXT_DISABLED } else { T_TEXT });
850        self.draw_color(c);
851        let x = (r[2].wrapping_sub(s) >> 1).wrapping_add(r[0]);
852        let y = (r[3].wrapping_sub(s) >> 1).wrapping_add(r[1]);
853        self.draw_icon(cp, x, y, s << 8);
854    }
855
856    /// ui_image(id, sf, ver) -> EV_* (clickable when id != 0): a surface at its natural size
857    /// (or next_size); ver stands for its SF_AUX version
858    #[cfg_attr(jfgui_dev, track_caller)]
859    pub fn image(&mut self, id: u32, sf: &Rc<Surface>, ver: u32) -> u32 {
860        let _src = self.src_scope();
861        let r = self.place(sf.w, sf.h);
862        self.draw_image(sf, ver, r[0], r[1], r[2], r[3]);
863        if id == 0 {
864            return 0;
865        }
866        self.behave(id, 0)
867    }
868
869    // ------------------------------------------------------------------ canvas
870
871    /// ui_canvas(id, draw, user, version) -> &UiInput: a custom area.  The hit box wants
872    /// every pointer move; the cursor comes from next_flags bits 12..15 (CUR_* <<
873    /// HB_CURSOR_SHIFT), default crosshair.
874    #[cfg_attr(jfgui_dev, track_caller)]
875    pub fn canvas(&mut self, id: u32, draw: Option<DrawRef>, user: usize, version: u32) -> &UiInput {
876        let _src = self.src_scope();
877        let f = self.take_flags();
878        let r = self.place(0, 0);
879        let mut cur = f & (15 << HB_CURSOR_SHIFT);
880        if cur == 0 {
881            cur = CUR_CROSS << HB_CURSOR_SHIFT;
882        }
883        let ev = self.behave(id, cur | HB_MOVES | HB_GRAB_H | HB_GRAB_V); // (a finger is its input)
884        // custom draw command
885        self.cmd_custom(r[0], r[1], r[2], r[3], draw, user, version);
886        let key = self.rtl_text_key(self.key_first()); // right-to-left: arrows as drawn
887        let (mx, px) = (self.rtl_map(self.wp.ctx.mx, r[0], r[2]), self.rtl_map(self.wp.ctx.press_x, r[0], r[2]));
888        let c = &mut self.wp.ctx;
889        let inp = &mut self.uiinput;
890        inp.ev = ev;
891        inp.btn = c.mbtn;
892        // behave() clears active on release, including when the pointer is outside
893        // the captured canvas. Keep that release visible to gesture controllers.
894        let (p, rl) = if ev & (EV_HOVER | EV_COMMIT) != 0 || c.active == id { (c.pressed, c.released) } else { (0, 0) };
895        inp.pressed = p;
896        inp.released = rl;
897        // a press on the canvas takes the keyboard focus (no focus ring), so keys reach the
898        // canvas and not the widget focused before
899        if p != 0 && ev & EV_HOVER != 0 {
900            c.focus = id;
901            c.focus_visible = false;
902        }
903        inp.x = mx.wrapping_sub(r[0]);
904        inp.y = c.my.wrapping_sub(r[1]);
905        inp.press_x = px.wrapping_sub(r[0]);
906        inp.press_y = c.press_y.wrapping_sub(r[1]);
907        let (wx, wy) = if ev & EV_HOVER != 0 { (c.wheel_x, c.wheel_y) } else { (0, 0) };
908        inp.wheel_x = wx;
909        inp.wheel_y = wy;
910        inp.mods = self.mods;
911        inp.rect = r;
912        inp.dpi = self.wp.rt.dpi;
913        inp.key = key;
914        inp.pressure = self.mpress; // (PN1) pen state
915        inp.ptype = self.mptype;
916        inp.tilt_x = self.mtilt[0];
917        inp.tilt_y = self.mtilt[1];
918        // move points relative to the widget (only while it is hovered or active)
919        let n = if ev & (EV_HOVER | EV_HELD) != 0 && self.uiw_cur == self.mpts_win { self.mpts.len().min(2048) } else { 0 };
920        inp.pts.clear();
921        inp.ppress.clear();
922        let (rtl, mirror) = (self.rtl, r[0].wrapping_add(r[0]).wrapping_add(r[2]).wrapping_sub(1));
923        for i in 0..n {
924            let p = self.mpts[i];
925            let x = if rtl { mirror.wrapping_sub(p[0]) } else { p[0] };
926            inp.pts.push([x.wrapping_sub(r[0]), p[1].wrapping_sub(r[1])]);
927            inp.ppress.push(self.mprs[i]);
928        }
929        self.dmg_canvas_seen(id); // (U1) rectangle for canvas_damage
930        self.sem(K_CANVAS, id, None, 0);
931        &self.uiinput
932    }
933}