Skip to main content

ui/
stock.rs

1// stock -- the default painters of the data views and the app shell parts: drawn from the
2// theme tokens only, so every style shows grids, trees, badges, meters, charts, navigation
3// panes, command bars, splitters, breadcrumbs, segmented controls, wizard steps, toasts,
4// spinners, calendars, dock areas, the windows of a window area and taskbars (desk.rs; high
5// contrast: no shadows, 2 px frames, the active window in the accent).  A style that wants
6// its own look returns its StyleData / StyleShell from Style::data / Style::shell and
7// overrides the methods it likes; the others keep these painters (call them from the
8// override to extend them).
9//
10//   kind_colors(ui, BADGE_*) -> (text, fill, solid)   the colours of a status kind
11//   Stock                                             the stock StyleData + StyleShell
12
13use alloc::string::String;
14
15use crate::consts::*;
16use crate::keys::*;
17use crate::theme::color_mix;
18use crate::{Sp, StyleData, StyleShell, Ui, DOCK_MK_BOTTOM, DOCK_MK_LEFT, DOCK_MK_RIGHT, DOCK_MK_TOP, DOCK_TAB_HOLD, DTB_ICONS};
19
20/// the stock data-view and shell painters (Style::data / Style::shell default)
21pub struct Stock;
22
23impl StyleData for Stock {}
24impl StyleShell for Stock {}
25
26// ------------------------------------------------------------------ helpers
27
28fn fill(ui: &mut Ui, x: i32, y: i32, w: i32, h: i32, r: i32, argb: u32) {
29    if argb & 0xFF00_0000 != 0 && w > 0 && h > 0 {
30        ui.cmd_rrect(x, y, w, h, r, 0, argb, 0, 0);
31    }
32}
33
34fn frame(ui: &mut Ui, x: i32, y: i32, w: i32, h: i32, r: i32, t: i32, argb: u32) {
35    if argb & 0xFF00_0000 != 0 && w > 0 && h > 0 {
36        ui.cmd_rrect(x, y, w, h, r, t, 0, argb, argb);
37    }
38}
39
40/// baseline of text style ts centred in y .. y + h (selects the font)
41pub(crate) fn baseline(ui: &mut Ui, ts: u32, y: i32, h: i32) -> i32 {
42    let line = ui.ts_line(ts);
43    let b = ui.ts_base(ts);
44    y + ((h - line) >> 1) + b
45}
46
47/// text in style ts, vertically centred in r, from x, cut with "…" at maxw (0 = unclipped)
48pub(crate) fn text_at(ui: &mut Ui, ts: u32, x: i32, r: [i32; 4], s: &str, argb: u32, maxw: i32) {
49    let y = baseline(ui, ts, r[1], r[3]);
50    let (font, size) = ui.text.get_font();
51    let b = s.as_bytes();
52    let n = b.iter().position(|&c| c == 0).unwrap_or(b.len());
53    if maxw > 0 && ui.text.width_n(b, n) > maxw {
54        let (e, _) = ui.ellipsize(&b[..n], maxw);
55        ui.cmd_text(x, y, e.as_bytes(), font, size, argb, maxw);
56        return;
57    }
58    ui.cmd_text(x, y, &b[..n], font, size, argb, maxw);
59}
60
61/// s cut with "…" to maxw in the current font, and its width
62pub(crate) fn fit(ui: &mut Ui, s: &str, maxw: i32) -> (String, i32) {
63    let w = ui.text.width(s);
64    if w <= maxw {
65        return (String::from(s), w);
66    }
67    ui.ellipsize(s.as_bytes(), maxw)
68}
69
70/// an icon of size `px` centred in the rectangle
71pub(crate) fn icon_in(ui: &mut Ui, cp: u32, r: [i32; 4], px: i32, argb: u32) {
72    ui.draw_color(argb);
73    ui.draw_icon(cp, r[0] + ((r[2] - px) >> 1), r[1] + ((r[3] - px) >> 1), px << 8);
74}
75
76/// the card colour as it shows on the window: a translucent card token (some styles layer
77/// their cards) composited over the window background
78pub fn card_opaque(ui: &Ui) -> u32 {
79    let card = ui.color(T_CARD);
80    let a = (card >> 24) as i32;
81    if a >= 255 {
82        return card;
83    }
84    color_mix(ui.color(T_BG) | 0xFF00_0000, card | 0xFF00_0000, a)
85}
86
87/// the (text, fill, solid) colours of a BADGE_* kind in the current theme
88pub fn kind_colors(ui: &Ui, kind: u32) -> (u32, u32, u32) {
89    let dark = ui.theme_dark();
90    let card = card_opaque(ui);
91    let solid = match kind {
92        BADGE_INFO => ui.color(T_INFO),
93        BADGE_OK => ui.color(T_OK),
94        BADGE_WARN => ui.color(T_WARN),
95        BADGE_CRIT => ui.color(T_CRIT),
96        BADGE_ACCENT => ui.color(T_ACCENT),
97        _ => ui.color(T_TEXT3),
98    };
99    let bg = match kind {
100        BADGE_INFO => ui.color(T_INFO_BG),
101        BADGE_OK => ui.color(T_OK_BG),
102        BADGE_WARN => ui.color(T_WARN_BG),
103        BADGE_CRIT => ui.color(T_CRIT_BG),
104        BADGE_ACCENT => color_mix(card, ui.color(T_ACCENT), if dark { 72 } else { 40 }),
105        _ => color_mix(card, ui.color(T_TEXT), if dark { 36 } else { 22 }),
106    };
107    // light theme: status colours are darkened for legible text on their light fill
108    let text = if kind == BADGE_NEUTRAL {
109        ui.color(T_TEXT2)
110    } else if dark {
111        color_mix(solid, 0xFFFF_FFFF, 48)
112    } else {
113        color_mix(solid, 0xFF00_0000, 72)
114    };
115    (text, bg | 0xFF00_0000, solid)
116}
117
118// ------------------------------------------------------------------ data views
119
120pub(crate) fn grid_header_bg(ui: &mut Ui, sp: &Sp) {
121    let h = ui.hair();
122    let c = ui.color(T_DIVIDER);
123    ui.draw_color(c);
124    ui.draw_rect(sp.x, sp.y + sp.h - h, sp.w, h);
125}
126
127pub(crate) fn grid_header(ui: &mut Ui, sp: &Sp) {
128    let r = sp.rect();
129    if sp.ev & (EV_HOVER | EV_HELD) != 0 {
130        let c = ui.color(if sp.ev & EV_HELD != 0 { T_SUBTLE_PRESSED } else { T_SUBTLE_HOVER });
131        fill(ui, r[0], r[1], r[2], r[3] - ui.hair(), 0, c);
132    }
133    let inset = sp.s[0];
134    let chev = ui.dp(12);
135    let sorted = sp.flags & (GH_SORT_ASC | GH_SORT_DESC) != 0;
136    let avail = r[2] - inset * 2 - if sorted { chev + ui.dp(4) } else { 0 };
137    // a narrow column: the label cut with "…"
138    ui.ts_font(TS_CAPTION);
139    let (label, tw) = fit(ui, sp.label.unwrap_or(""), avail.max(1));
140    let tx = if sp.flags & GH_RIGHT != 0 { r[0] + r[2] - inset - tw.min(avail) - if sorted { chev + ui.dp(4) } else { 0 } } else { r[0] + inset };
141    let c = ui.color(if sorted { T_TEXT } else { T_TEXT2 });
142    text_at(ui, TS_CAPTION, tx, r, &label, c, avail.max(1));
143    if sorted {
144        let x = tx + tw.min(avail) + ui.dp(4);
145        let cp = if sp.flags & GH_SORT_DESC != 0 { IC_CHEV_DOWN_SMALL } else { IC_CHEV_UP_SMALL };
146        icon_in(ui, cp, [x, r[1], chev, r[3]], chev, c);
147    }
148    if sp.flags & GH_LAST == 0 {
149        let h = ui.hair();
150        let m = r[3] / 4;
151        let c = ui.color(T_DIVIDER);
152        ui.draw_color(c);
153        ui.draw_rect(r[0] + r[2] - h, r[1] + m, h, r[3] - m * 2);
154    }
155}
156
157pub(crate) fn grid_row(ui: &mut Ui, sp: &Sp) {
158    let r = sp.rect();
159    let card = card_opaque(ui);
160    let sel = sp.value != 0;
161    let bg = if sel {
162        let a = ui.color(T_ACCENT);
163        color_mix(card, a, if ui.theme_dark() { 64 } else { 36 })
164    } else if sp.ev & EV_HELD != 0 {
165        ui.color(T_SUBTLE_PRESSED)
166    } else if sp.ev & EV_HOVER != 0 {
167        ui.color(T_SUBTLE_HOVER)
168    } else if sp.flags & GR_STRIPE != 0 {
169        color_mix(card, ui.color(T_TEXT), 6)
170    } else {
171        0
172    };
173    fill(ui, r[0], r[1], r[2], r[3], 0, bg);
174    if sel {
175        let c = ui.color(T_ACCENT);
176        let (pw, ph) = (ui.dp(3), ui.dp(16).min(r[3] - ui.dp(8)));
177        fill(ui, r[0], r[1] + ((r[3] - ph) >> 1), pw, ph, pw >> 1, c);
178    }
179    let h = ui.hair();
180    let c = ui.color(T_DIVIDER);
181    ui.draw_color(c);
182    ui.draw_rect(r[0], r[1] + r[3] - h, r[2], h);
183    if sp.flags & GR_FOCUS != 0 {
184        let c = ui.color(T_FOCUS_OUT);
185        frame(ui, r[0] + h, r[1] + h, r[2] - h * 2, r[3] - h * 2, ui.dp(2), h, c);
186    }
187}
188
189pub(crate) fn tree_twisty(ui: &mut Ui, sp: &Sp) {
190    let r = sp.rect();
191    if sp.ev & EV_HOVER != 0 {
192        let c = ui.color(T_SUBTLE_HOVER);
193        fill(ui, r[0], r[1], r[2], r[3], ui.dp(4), c);
194    }
195    let cp = if sp.value != 0 { IC_CHEV_DOWN_SMALL } else { IC_CHEV_RIGHT_SMALL };
196    let c = ui.color(T_TEXT2);
197    let px = ui.dp(12);
198    icon_in(ui, cp, r, px, c);
199}
200
201pub(crate) fn badge_size(ui: &mut Ui, sp: &mut Sp) -> (i32, i32) {
202    ui.ts_font(TS_CAPTION);
203    let tw = ui.text.width(sp.label.unwrap_or(""));
204    sp.s[0] = tw;
205    let dot = if sp.icon != 0 { ui.dp(14) } else { 0 };
206    (tw + ui.dp(16) + dot, ui.dp_text(20))
207}
208
209pub(crate) fn badge(ui: &mut Ui, sp: &Sp) {
210    let r = sp.rect();
211    let (text, bg, solid) = kind_colors(ui, sp.flags & 7);
212    let h = r[3].min(ui.dp_text(20));
213    let y = r[1] + ((r[3] - h) >> 1);
214    fill(ui, r[0], y, r[2], h, h >> 1, bg);
215    let mut x = r[0] + ui.dp(8);
216    if sp.icon != 0 {
217        let px = ui.dp(12);
218        icon_in(ui, sp.icon, [x, y, px, h], px, solid);
219        x += ui.dp(14);
220    }
221    text_at(ui, TS_CAPTION, x, [r[0], y, r[2], h], sp.label.unwrap_or(""), text, r[0] + r[2] - x);
222}
223
224pub(crate) fn status_dot(ui: &mut Ui, sp: &Sp) {
225    let r = sp.rect();
226    let (_, bg, solid) = kind_colors(ui, sp.flags & 7);
227    let d = r[2].min(r[3]);
228    let cx = (r[0] << 8) + (r[2] << 7);
229    let cy = (r[1] << 8) + (r[3] << 7);
230    ui.draw_color(bg);
231    ui.draw_circle(cx, cy, d << 7, 0);
232    ui.draw_color(solid);
233    ui.draw_circle(cx, cy, (d << 7) * 5 / 8, 0);
234}
235
236pub(crate) fn meter(ui: &mut Ui, sp: &Sp) {
237    let r = sp.rect();
238    let th = r[3].min(ui.dp(8));
239    let y = r[1] + ((r[3] - th) >> 1);
240    let c = ui.color(T_TRACK);
241    fill(ui, r[0], y, r[2], th, th >> 1, c);
242    if sp.value2 <= 0 || sp.value <= 0 {
243        return;
244    }
245    let w = ((sp.value.min(sp.value2) as i64 * r[2] as i64) / sp.value2 as i64) as i32;
246    let (_, _, solid) = kind_colors(ui, sp.flags & 7);
247    fill(ui, r[0], y, w.max(th), th, th >> 1, solid);
248}
249
250pub(crate) fn chart_palette(ui: &Ui) -> [u32; 8] {
251    let a = ui.color(T_ACCENT);
252    let t = ui.color(T_TEXT);
253    [
254        a,
255        ui.color(T_INFO),
256        ui.color(T_OK),
257        ui.color(T_WARN),
258        ui.color(T_CRIT),
259        color_mix(a, t, 112),
260        ui.color(T_TEXT3),
261        color_mix(ui.color(T_INFO), ui.color(T_OK), 128),
262    ]
263}
264
265// ------------------------------------------------------------------ shell
266
267pub(crate) fn nav_pane(ui: &mut Ui, sp: &Sp) {
268    let r = sp.rect();
269    let c = ui.color(T_LAYER);
270    fill(ui, r[0], r[1], r[2], r[3], 0, c);
271    let h = ui.hair();
272    let c = ui.color(T_DIVIDER);
273    ui.draw_color(c);
274    ui.draw_rect(r[0] + r[2] - h, r[1], h, r[3]);
275}
276
277pub(crate) fn nav_item_size(ui: &mut Ui, sp: &mut Sp) -> (i32, i32) {
278    if sp.flags & NAV_COMPACT != 0 {
279        return (ui.dp(40), ui.dp(36));
280    }
281    ui.ts_font(TS_BODY);
282    let tw = ui.text.width(sp.label.unwrap_or(""));
283    let bw = match sp.label2 {
284        Some(b) if !b.is_empty() => {
285            ui.ts_font(TS_CAPTION);
286            ui.text.width(b) + ui.dp(16)
287        }
288        _ => 0,
289    };
290    sp.s[0] = tw;
291    sp.s[1] = bw;
292    (ui.dp(16 + 16 + 12 + 12) + tw + bw, ui.dp(36))
293}
294
295pub(crate) fn nav_item(ui: &mut Ui, sp: &Sp) {
296    let r = sp.rect();
297    let m = ui.dp(4);
298    let (x, y, w, h) = (r[0] + m, r[1] + ui.dp(2), r[2] - m * 2, r[3] - ui.dp(4));
299    let sel = sp.value != 0;
300    let bg = if sp.ev & EV_HELD != 0 {
301        ui.color(T_SUBTLE_PRESSED)
302    } else if sel || sp.ev & EV_HOVER != 0 {
303        ui.color(T_SUBTLE_HOVER)
304    } else {
305        0
306    };
307    fill(ui, x, y, w, h, ui.dp(4), bg);
308    if sel {
309        let c = ui.color(T_ACCENT);
310        let (pw, ph) = (ui.dp(3), ui.dp(16));
311        fill(ui, x, y + ((h - ph) >> 1), pw, ph, pw >> 1, c);
312    }
313    let disabled = sp.state == 3;
314    let fg = ui.color(if disabled { T_TEXT_DISABLED } else { T_TEXT });
315    let px = ui.dp(16);
316    let compact = sp.flags & NAV_COMPACT != 0;
317    let ix = if compact { x + ((w - px) >> 1) } else { x + ui.dp(12) };
318    if sp.icon != 0 {
319        icon_in(ui, sp.icon, [ix, y, px, h], px, if sel { ui.color(T_ACCENT_TEXT) } else { fg });
320    }
321    // badge: a count pill at the end (compact: a dot on the icon)
322    let badge = sp.label2.filter(|b| !b.is_empty());
323    if compact {
324        if badge.is_some() {
325            let c = ui.color(T_ACCENT);
326            ui.draw_color(c);
327            ui.draw_circle((ix + px) << 8, (y + ui.dp(8)) << 8, ui.dp_fx(3 * 256), 0);
328        }
329        return;
330    }
331    let tx = ix + px + ui.dp(12);
332    let mut end = x + w - ui.dp(8);
333    if let Some(b) = badge {
334        ui.ts_font(TS_CAPTION);
335        // (large text: a taller pill, at least round)
336        let bh = ui.dp_text(18).min(h);
337        let bw = (ui.text.width(b) + ui.dp(12)).max(bh);
338        end -= bw;
339        let a = ui.color(T_ACCENT);
340        fill(ui, end, y + ((h - bh) >> 1), bw, bh, bh >> 1, a);
341        let c = ui.color(T_ON_ACCENT);
342        text_at(ui, TS_CAPTION, end + ui.dp(6), [end, y, bw, h], b, c, 0);
343        end -= ui.dp(6);
344    }
345    let ts = if sel { TS_BODY_STRONG } else { TS_BODY };
346    text_at(ui, ts, tx, [x, y, w, h], sp.label.unwrap_or(""), fg, (end - tx).max(1));
347}
348
349pub(crate) fn nav_section_size(ui: &mut Ui, sp: &mut Sp) -> (i32, i32) {
350    if sp.flags & NAV_COMPACT != 0 {
351        return (ui.dp(40), ui.dp(13));
352    }
353    (ui.dp(40), ui.dp(30))
354}
355
356pub(crate) fn nav_section(ui: &mut Ui, sp: &Sp) {
357    let r = sp.rect();
358    if sp.flags & NAV_COMPACT != 0 {
359        let h = ui.hair();
360        let c = ui.color(T_DIVIDER);
361        ui.draw_color(c);
362        ui.draw_rect(r[0] + ui.dp(10), r[1] + ((r[3] - h) >> 1), r[2] - ui.dp(20), h);
363        return;
364    }
365    let c = ui.color(T_TEXT3);
366    let y = r[1] + ui.dp(8);
367    text_at(ui, TS_CAPTION, r[0] + ui.dp(16), [r[0], y, r[2], r[3] - ui.dp(8)], sp.label.unwrap_or(""), c, r[2] - ui.dp(24));
368}
369
370pub(crate) fn command_bar(_ui: &mut Ui, _sp: &Sp) {}
371
372pub(crate) fn split_grip(ui: &mut Ui, sp: &Sp) {
373    let r = sp.rect();
374    let active = sp.ev & (EV_HOVER | EV_HELD) != 0;
375    let t = if active { ui.dp(2) } else { ui.hair() };
376    let c = ui.color(if active { T_ACCENT } else { T_DIVIDER });
377    ui.draw_color(c);
378    if sp.flags & 1 != 0 {
379        ui.draw_rect(r[0], r[1] + ((r[3] - t) >> 1), r[2], t);
380    } else {
381        ui.draw_rect(r[0] + ((r[2] - t) >> 1), r[1], t, r[3]);
382    }
383}
384
385pub(crate) fn breadcrumb_sep(ui: &mut Ui, sp: &Sp) {
386    let px = ui.dp(12);
387    let c = ui.color(T_TEXT3);
388    icon_in(ui, IC_CHEV_RIGHT_SMALL, sp.rect(), px, c);
389}
390
391pub(crate) fn segment(ui: &mut Ui, sp: &Sp) {
392    let r = sp.rect();
393    let rad = ui.dp(4);
394    if sp.value2 == 1 {
395        // the track behind every item
396        let c = ui.color(T_SUBTLE_HOVER);
397        let c = if c & 0xFF00_0000 == 0 { ui.color(T_CTRL_DISABLED) } else { c };
398        fill(ui, r[0], r[1], r[2], r[3], rad + ui.dp(2), c);
399        return;
400    }
401    let m = ui.dp(2);
402    let (x, y, w, h) = (r[0] + m, r[1] + m, r[2] - m * 2, r[3] - m * 2);
403    if sp.value != 0 {
404        // light themes show the control fill solid; a dark theme's translucent white is
405        // layered over the track (made opaque it turns white under the white text)
406        let f = ui.color(T_CTRL);
407        let f = if ui.theme_dark() { f } else { f | 0xFF00_0000 };
408        let s = ui.color(T_CTRL_STROKE);
409        ui.cmd_rrect(x, y, w, h, rad, ui.hair(), f, s, ui.color(T_CTRL_STROKE2));
410    } else if sp.ev & EV_HOVER != 0 {
411        let c = ui.color(T_SUBTLE_HOVER);
412        fill(ui, x, y, w, h, rad, c);
413    }
414    let ts = if sp.value != 0 { TS_BODY_STRONG } else { TS_BODY };
415    ui.ts_font(ts);
416    // a squeezed item: its label cut with "…", centred
417    let (label, tw) = fit(ui, sp.label.unwrap_or(""), (w - ui.dp(8)).max(1));
418    let c = ui.color(if sp.state == 3 { T_TEXT_DISABLED } else if sp.value != 0 { T_TEXT } else { T_TEXT2 });
419    text_at(ui, ts, x + ((w - tw) >> 1).max(0), r, &label, c, w);
420}
421
422pub(crate) fn wizard_step(ui: &mut Ui, sp: &Sp) {
423    let r = sp.rect();
424    let d = ui.dp(24);
425    let cx = r[0] + ui.dp(12) + (d >> 1);
426    let cy = r[1] + (r[3] >> 1);
427    // connector to the next step
428    if sp.flags & 1 != 0 {
429        let t = ui.dp(2);
430        let c = ui.color(if sp.value2 == WS_DONE { T_ACCENT } else { T_DIVIDER });
431        ui.draw_color(c);
432        ui.draw_rect(cx - (t >> 1), cy + (d >> 1) + ui.dp(2), t, r[1] + r[3] - cy - (d >> 1) + (r[3] >> 1) - ui.dp(4));
433    }
434    let hover = sp.ev & EV_HOVER != 0;
435    if hover && sp.value2 != WS_CURRENT {
436        let c = ui.color(T_SUBTLE_HOVER);
437        fill(ui, r[0] + ui.dp(4), r[1] + ui.dp(2), r[2] - ui.dp(8), r[3] - ui.dp(4), ui.dp(4), c);
438    }
439    let (ring, inner, num) = match sp.value2 {
440        WS_CURRENT => (ui.color(T_ACCENT), ui.color(T_ACCENT), ui.color(T_ON_ACCENT)),
441        WS_DONE => (ui.color(T_ACCENT), color_mix(card_opaque(ui), ui.color(T_ACCENT), 40), ui.color(T_ACCENT_TEXT)),
442        WS_ERROR => (ui.color(T_CRIT), ui.color(T_CRIT_BG), ui.color(T_CRIT)),
443        _ => (ui.color(T_STRONG_STROKE), card_opaque(ui), ui.color(T_TEXT2)),
444    };
445    ui.draw_color(ring);
446    ui.draw_circle(cx << 8, cy << 8, d << 7, 0);
447    ui.draw_color(inner);
448    ui.draw_circle(cx << 8, cy << 8, (d << 7) - (ui.hair() << 8) - 128, 0);
449    let cr = [cx - (d >> 1), cy - (d >> 1), d, d];
450    if sp.value2 == WS_DONE {
451        let px = ui.dp(12);
452        icon_in(ui, IC_CHECK_MARK, cr, px, num);
453    } else if sp.value2 == WS_ERROR {
454        let px = ui.dp(12);
455        icon_in(ui, IC_CANCEL, cr, px, num);
456    } else {
457        let s = alloc::format!("{}", sp.value);
458        ui.ts_font(TS_CAPTION);
459        let tw = ui.text.width(&s);
460        text_at(ui, TS_CAPTION, cx - (tw >> 1), cr, &s, num, 0);
461    }
462    let ts = if sp.value2 == WS_CURRENT { TS_BODY_STRONG } else { TS_BODY };
463    let c = ui.color(match sp.value2 {
464        WS_CURRENT => T_TEXT,
465        WS_TODO => T_TEXT2,
466        _ => T_TEXT,
467    });
468    let tx = cx + (d >> 1) + ui.dp(12);
469    text_at(ui, ts, tx, r, sp.label.unwrap_or(""), c, r[0] + r[2] - tx - ui.dp(8));
470}
471
472pub(crate) fn toast(ui: &mut Ui, sp: &mut Sp) {
473    let r = sp.rect();
474    let rad = ui.dp(8);
475    ui.draw_color(ui.color(T_SHADOW));
476    ui.cmd_shadow(r[0], r[1], r[2], r[3], rad, ui.dp(12));
477    let f = ui.color(T_FLYOUT) | 0xFF00_0000;
478    let s = ui.color(T_FLYOUT_STROKE);
479    ui.cmd_rrect(r[0], r[1], r[2], r[3], rad, ui.hair(), f, s, s);
480    let kind = match sp.flags & 3 {
481        IB_SUCCESS => BADGE_OK,
482        IB_WARNING => BADGE_WARN,
483        IB_ERROR => BADGE_CRIT,
484        _ => BADGE_INFO,
485    };
486    let (_, _, solid) = kind_colors(ui, kind);
487    // severity bar and icon
488    fill(ui, r[0] + ui.dp(6), r[1] + ui.dp(10), ui.dp(3), r[3] - ui.dp(20), ui.dp(1), solid);
489    let icon = match sp.flags & 3 {
490        IB_SUCCESS => IC_CHECK_CIRCLE_FILL,
491        IB_WARNING => IC_WARNING,
492        IB_ERROR => IC_ERROR_FILL,
493        _ => IC_INFO,
494    };
495    let px = ui.dp(16);
496    icon_in(ui, icon, [r[0] + ui.dp(18), r[1] + ui.dp(14), px, px], px, solid);
497    let tx = r[0] + ui.dp(44);
498    let cs = ui.dp(28);
499    let cx = r[0] + r[2] - cs - ui.dp(8);
500    let maxw = cx - tx - ui.dp(4);
501    let t = ui.color(T_TEXT);
502    text_at(ui, TS_BODY_STRONG, tx, [r[0], r[1] + ui.dp(10), r[2], ui.dp(24)], sp.label.unwrap_or(""), t, maxw);
503    if let Some(m) = sp.label2 {
504        let c = ui.color(T_TEXT2);
505        text_at(ui, TS_CAPTION, tx, [r[0], r[1] + ui.dp(32), r[2], ui.dp(20)], m, c, maxw);
506    }
507    sp.s[0] = cx;
508    sp.s[1] = cs;
509}
510
511pub(crate) fn spinner(ui: &mut Ui, sp: &Sp) {
512    let r = sp.rect();
513    let d = r[2].min(r[3]);
514    let cx = ((r[0] << 8) + (r[2] << 7)) as f32;
515    let cy = ((r[1] << 8) + (r[3] << 7)) as f32;
516    let t = (d / 8).max(ui.dp(2));
517    let rad = ((d - t) << 7) as f32;
518    // a quarter-to-three-quarter arc that turns with the phase
519    let phase = (sp.value & 1023) as f32 / 1024.0;
520    let a0 = phase * core::f32::consts::TAU;
521    let track = ui.color(T_TRACK);
522    let accent = ui.color(T_ACCENT);
523    let n = 32;
524    for k in 0..n {
525        let b0 = k as f32 / n as f32 * core::f32::consts::TAU;
526        let b1 = (k + 1) as f32 / n as f32 * core::f32::consts::TAU;
527        let (s0, c0) = sincos(b0);
528        let (s1, c1) = sincos(b1);
529        ui.draw_color(track);
530        ui.draw_line(rt::fx::cvt(cx + c0 * rad), rt::fx::cvt(cy + s0 * rad), rt::fx::cvt(cx + c1 * rad), rt::fx::cvt(cy + s1 * rad), t << 8);
531    }
532    let span = 0.30 * core::f32::consts::TAU;
533    let m = 12;
534    for k in 0..m {
535        let b0 = a0 + span * k as f32 / m as f32;
536        let b1 = a0 + span * (k + 1) as f32 / m as f32;
537        let (s0, c0) = sincos(b0);
538        let (s1, c1) = sincos(b1);
539        ui.draw_color(accent);
540        ui.draw_line(rt::fx::cvt(cx + c0 * rad), rt::fx::cvt(cy + s0 * rad), rt::fx::cvt(cx + c1 * rad), rt::fx::cvt(cy + s1 * rad), t << 8);
541    }
542}
543
544pub(crate) fn progress_indeterminate(ui: &mut Ui, sp: &Sp) {
545    let r = sp.rect();
546    let th = r[3].min(ui.dp(4));
547    let y = r[1] + ((r[3] - th) >> 1);
548    let c = ui.color(T_TRACK);
549    fill(ui, r[0], y, r[2], th, th >> 1, c);
550    let seg = r[2] * 3 / 10;
551    let pos = ((sp.value & 1023) as i64 * (r[2] + seg) as i64 / 1024) as i32 - seg;
552    let x0 = (r[0] + pos).max(r[0]);
553    let x1 = (r[0] + pos + seg).min(r[0] + r[2]);
554    if x1 > x0 {
555        let c = ui.color(T_ACCENT);
556        fill(ui, x0, y, x1 - x0, th, th >> 1, c);
557    }
558}
559
560pub(crate) fn calendar_day(ui: &mut Ui, sp: &Sp) {
561    let r = sp.rect();
562    let d = r[2].min(r[3]) - ui.dp(4);
563    let cx = r[0] + (r[2] >> 1);
564    let cy = r[1] + (r[3] >> 1);
565    let sel = sp.value != 0;
566    if sel {
567        let c = ui.color(T_ACCENT);
568        ui.draw_color(c);
569        ui.draw_circle(cx << 8, cy << 8, d << 7, 0);
570    } else if sp.ev & EV_HOVER != 0 {
571        let c = ui.color(T_SUBTLE_HOVER);
572        ui.draw_color(c);
573        ui.draw_circle(cx << 8, cy << 8, d << 7, 0);
574    }
575    if sp.value2 != 0 && !sel {
576        let c = ui.color(T_ACCENT);
577        ui.draw_color(c);
578        ui.draw_circle(cx << 8, cy << 8, (d << 7) - 128, ui.hair() << 8);
579    }
580    let label = sp.label.unwrap_or("");
581    ui.ts_font(TS_BODY);
582    let tw = ui.text.width(label);
583    let c = ui.color(if sel {
584        T_ON_ACCENT
585    } else if sp.state == 3 {
586        T_TEXT_DISABLED
587    } else if sp.flags & 1 != 0 {
588        T_TEXT3
589    } else {
590        T_TEXT
591    });
592    text_at(ui, TS_BODY, cx - (tw >> 1), r, label, c, 0);
593}
594
595// ------------------------------------------------------------------ docking (dock.rs)
596
597pub(crate) fn dock_group(ui: &mut Ui, sp: &Sp) {
598    let r = sp.rect();
599    let c = ui.color(T_LAYER);
600    fill(ui, r[0], r[1], r[2], r[3], 0, c);
601    let h = ui.hair();
602    let c = ui.color(T_DIVIDER);
603    frame(ui, r[0], r[1], r[2], r[3], 0, h, c);
604}
605
606pub(crate) fn dock_header(ui: &mut Ui, sp: &Sp) {
607    let r = sp.rect();
608    let h = ui.hair();
609    let c = ui.color(T_DIVIDER);
610    ui.draw_color(c);
611    ui.draw_rect(r[0], r[1] + r[3] - h, r[2], h);
612}
613
614pub(crate) fn dock_tab_size(ui: &mut Ui, sp: &mut Sp) -> (i32, i32) {
615    ui.ts_font(TS_BODY);
616    let tw = ui.text.width(sp.label.unwrap_or(""));
617    let icon = if sp.icon != 0 { 16 + 6 } else { 0 };
618    (ui.dp(10 + 10 + icon) + tw, ui.dp(32))
619}
620
621pub(crate) fn dock_tab(ui: &mut Ui, sp: &Sp) {
622    let r = sp.rect();
623    let m = ui.dp(2);
624    let (x, y, w, h) = (r[0] + m, r[1] + ui.dp(4), r[2] - m * 2, r[3] - ui.dp(8));
625    let hold = sp.flags & DOCK_TAB_HOLD != 0;
626    let bg = if hold || sp.ev & EV_HELD != 0 {
627        ui.color(T_SUBTLE_PRESSED)
628    } else if sp.ev & EV_HOVER != 0 {
629        ui.color(T_SUBTLE_HOVER)
630    } else {
631        0
632    };
633    fill(ui, x, y, w, h, ui.dp(4), bg);
634    if hold {
635        // held long enough to be dragged: a hairline of the accent around the pressed fill
636        let a = ui.color(T_ACCENT);
637        frame(ui, x, y, w, h, ui.dp(4), ui.hair(), a);
638    }
639    let sel = sp.value != 0;
640    let fg = ui.color(if sel { T_TEXT } else { T_TEXT2 });
641    let mut tx = r[0] + ui.dp(10);
642    if sp.icon != 0 {
643        let px = ui.dp(16);
644        icon_in(ui, sp.icon, [tx, r[1], px, r[3]], px, fg);
645        tx += px + ui.dp(6);
646    }
647    let avail = (r[0] + r[2] - ui.dp(8) - tx).max(1);
648    let label = sp.label.unwrap_or("");
649    ui.ts_font(TS_BODY);
650    let s = if ui.text.width(label) > avail { ui.ellipsize(label.as_bytes(), avail).0 } else { alloc::string::String::from(label) };
651    // too narrow for a part of the title: the icon alone
652    if sp.icon == 0 || s != "…" {
653        text_at(ui, TS_BODY, tx, r, &s, fg, avail);
654    }
655    if sel {
656        // the shown tab: a bar along the bottom, accent while the group has the focus
657        let t = ui.dp(2);
658        let c = ui.color(if sp.value2 != 0 { T_ACCENT } else { T_TEXT3 });
659        let i = ui.dp(8);
660        fill(ui, r[0] + i, r[1] + r[3] - t, r[2] - i * 2, t, t >> 1, c);
661    }
662}
663
664pub(crate) fn dock_rail(ui: &mut Ui, sp: &Sp) {
665    let r = sp.rect();
666    let c = ui.color(T_LAYER);
667    fill(ui, r[0], r[1], r[2], r[3], 0, c);
668    let h = ui.hair();
669    let c = ui.color(T_DIVIDER);
670    ui.draw_color(c);
671    match sp.flags {
672        0 => ui.draw_rect(r[0] + r[2] - h, r[1], h, r[3]),
673        1 => ui.draw_rect(r[0], r[1], h, r[3]),
674        _ => ui.draw_rect(r[0], r[1], r[2], h),
675    }
676}
677
678pub(crate) fn dock_marker(ui: &mut Ui, sp: &Sp) {
679    let r = sp.rect();
680    let hover = sp.ev & EV_HOVER != 0;
681    let rad = ui.dp(6);
682    let a = ui.color(T_ACCENT);
683    ui.draw_color(ui.color(T_SHADOW));
684    ui.cmd_shadow(r[0], r[1], r[2], r[3], rad, ui.dp(8));
685    let bg = card_opaque(ui) | 0xFF00_0000;
686    let stroke = if hover { a } else { ui.color(T_CTRL_STROKE) };
687    let t = if hover { ui.dp(2) } else { ui.hair() };
688    ui.cmd_rrect(r[0], r[1], r[2], r[3], rad, t, bg, stroke, stroke);
689    // the place in small: a frame and the part the panel takes
690    let i = ui.dp(8);
691    let f = [r[0] + i, r[1] + i, r[2] - i * 2, r[3] - i * 2];
692    let c = ui.color(T_TEXT2);
693    frame(ui, f[0], f[1], f[2], f[3], ui.dp(2), ui.hair(), c);
694    let (pw, ph) = (f[2] * 2 / 5, f[3] * 2 / 5);
695    let p = match sp.value {
696        DOCK_MK_LEFT => [f[0], f[1], pw, f[3]],
697        DOCK_MK_RIGHT => [f[0] + f[2] - pw, f[1], pw, f[3]],
698        DOCK_MK_TOP => [f[0], f[1], f[2], ph],
699        DOCK_MK_BOTTOM => [f[0], f[1] + f[3] - ph, f[2], ph],
700        _ => {
701            let m = ui.dp(3);
702            [f[0] + m, f[1] + m, f[2] - m * 2, f[3] - m * 2]
703        }
704    };
705    let fc = if hover { a } else { (a & 0x00FF_FFFF) | 0xA000_0000 };
706    fill(ui, p[0], p[1], p[2], p[3], ui.dp(1), fc);
707}
708
709pub(crate) fn dock_preview(ui: &mut Ui, sp: &Sp) {
710    let r = sp.rect();
711    let c = ui.color(T_ACCENT);
712    let tint = (c & 0x00FF_FFFF) | if sp.value != 0 { 0x1800_0000 } else { 0x3000_0000 };
713    let (t, rad) = (ui.dp(2), ui.dp(4));
714    ui.cmd_rrect(r[0], r[1], r[2], r[3], rad, t, tint, c, c);
715}
716
717// ------------------------------------------------------------------ window areas (desk.rs)
718
719// the window colour, opaque
720fn bg_opaque(ui: &Ui) -> u32 {
721    ui.color(T_BG) | 0xFF00_0000
722}
723
724// a token as it shows over the window colour (translucent layer tokens), opaque
725fn over_bg(ui: &Ui, tok: u32) -> u32 {
726    let c = ui.color(tok);
727    color_mix(bg_opaque(ui), c | 0xFF00_0000, (c >> 24) as i32)
728}
729
730// the radius of a window in an area (DIP); its title bar keeps the top corners
731const DESK_R: i32 = 6;
732
733pub(crate) fn desk_bg(ui: &mut Ui, sp: &Sp) {
734    let r = sp.rect();
735    // the window colour a little towards the text (high contrast: the window colour)
736    let c = if ui.high_contrast() { bg_opaque(ui) } else { color_mix(bg_opaque(ui), ui.color(T_TEXT) | 0xFF00_0000, if ui.theme_dark() { 14 } else { 22 }) };
737    fill(ui, r[0], r[1], r[2], r[3], 0, c);
738}
739
740pub(crate) fn desk_window(ui: &mut Ui, sp: &Sp) {
741    let r = sp.rect();
742    let (act, max, hc) = (sp.value != 0, sp.flags & 1 != 0, ui.high_contrast());
743    let rad = if max { 0 } else { ui.dp(DESK_R) };
744    if !max && !hc {
745        ui.draw_color(ui.color(T_SHADOW));
746        ui.cmd_shadow(r[0], r[1] + ui.dp(2), r[2], r[3], rad, ui.dp(if act { 12 } else { 6 }));
747    }
748    let bg = bg_opaque(ui);
749    let stroke = if hc {
750        ui.color(if act { T_ACCENT } else { T_TEXT })
751    } else if act {
752        color_mix(ui.color(T_CTRL_STROKE) | 0xFF00_0000, ui.color(T_ACCENT) | 0xFF00_0000, 140)
753    } else {
754        over_bg(ui, T_CTRL_STROKE)
755    };
756    let t = if max { 0 } else if hc { ui.dp(2) } else { ui.hair() };
757    ui.cmd_rrect(r[0], r[1], r[2], r[3], rad, t, bg, stroke, stroke);
758}
759
760pub(crate) fn desk_titlebar(ui: &mut Ui, sp: &Sp) {
761    let r = sp.rect();
762    let (act, hc) = (sp.value != 0, ui.high_contrast());
763    // the active one: a light accent tint (high contrast: the accent), the inactive one the
764    // window colour
765    let fillc = if hc && act {
766        ui.color(T_ACCENT) | 0xFF00_0000
767    } else if act {
768        color_mix(bg_opaque(ui), ui.color(T_ACCENT) | 0xFF00_0000, if ui.theme_dark() { 40 } else { 26 })
769    } else {
770        0
771    };
772    if fillc != 0 && r[2] > 0 && r[3] > 0 {
773        // rounded at the top like the window, square at the bottom
774        let rad = (ui.dp(DESK_R) - ui.hair()).max(0).min(r[3] / 2);
775        ui.cmd_rrect(r[0], r[1], r[2], r[3], rad, 0, fillc, 0, 0);
776        ui.draw_color(fillc);
777        ui.draw_rect(r[0], r[1] + rad, r[2], r[3] - rad);
778    }
779    let h = ui.hair();
780    let line = over_bg(ui, T_DIVIDER);
781    ui.draw_color(line);
782    ui.draw_rect(r[0], r[1] + r[3] - h, r[2], h);
783    let fg = ui.color(if hc && act {
784        T_ON_ACCENT
785    } else if act {
786        T_TEXT
787    } else {
788        T_TEXT3
789    });
790    let (mut x, mut w) = (sp.s[0], sp.s[1]);
791    let px = ui.dp(16);
792    if sp.icon != 0 && w > px {
793        icon_in(ui, sp.icon, [x, r[1], px, r[3]], px, fg);
794        x += px + ui.dp(8);
795        w -= px + ui.dp(8);
796    }
797    let ts = {
798        let st = ui.style;
799        let v = st.metric(SM_TB_TITLE_TS) as u32;
800        if v != 0 { v } else { TS_BODY }
801    };
802    let title = sp.label.unwrap_or("");
803    if w > 0 && !title.is_empty() {
804        text_at(ui, ts, x, r, title, fg, w);
805    }
806}
807
808pub(crate) fn desk_snap(ui: &mut Ui, sp: &Sp) {
809    let m = ui.dp(4);
810    let mut q = *sp;
811    q.place([sp.x + m, sp.y + m, sp.w - m * 2, sp.h - m * 2]);
812    q.value = 0;
813    dock_preview(ui, &q);
814}
815
816pub(crate) fn taskbar(ui: &mut Ui, sp: &Sp) {
817    let r = sp.rect();
818    let c = over_bg(ui, T_LAYER);
819    fill(ui, r[0], r[1], r[2], r[3], 0, c);
820    let h = ui.hair();
821    let c = over_bg(ui, T_LAYER_STROKE);
822    ui.draw_color(c);
823    ui.draw_rect(r[0], r[1], r[2], h);
824}
825
826pub(crate) fn taskbar_button_size(ui: &mut Ui, sp: &mut Sp) -> (i32, i32) {
827    if sp.flags & DTB_ICONS != 0 {
828        return (ui.dp(40), ui.dp(32));
829    }
830    ui.ts_font(TS_BODY);
831    let tw = ui.text.width(sp.label.unwrap_or(""));
832    let icon = if sp.icon != 0 { 16 + 8 } else { 0 };
833    (ui.dp(10 + icon + 12) + tw, ui.dp(32))
834}
835
836pub(crate) fn taskbar_button(ui: &mut Ui, sp: &Sp) {
837    let r = sp.rect();
838    let (on, min, hc) = (sp.value != 0, sp.value2 != 0, ui.high_contrast());
839    let rad = ui.dp(4);
840    let bg = if hc && on {
841        ui.color(T_ACCENT)
842    } else if sp.ev & EV_HELD != 0 {
843        ui.color(T_SUBTLE_PRESSED)
844    } else if sp.ev & EV_HOVER != 0 || on {
845        ui.color(T_SUBTLE_HOVER)
846    } else {
847        0
848    };
849    fill(ui, r[0], r[1], r[2], r[3], rad, bg);
850    if hc && !on {
851        frame(ui, r[0], r[1], r[2], r[3], rad, ui.hair(), ui.color(T_TEXT));
852    }
853    let fg = ui.color(if hc && on {
854        T_ON_ACCENT
855    } else if min {
856        T_TEXT3
857    } else {
858        T_TEXT
859    });
860    let px = ui.dp(16);
861    let icons = sp.flags & DTB_ICONS != 0;
862    let mut tx = r[0] + ui.dp(10);
863    if sp.icon != 0 {
864        if icons {
865            icon_in(ui, sp.icon, r, px, fg);
866        } else {
867            icon_in(ui, sp.icon, [tx, r[1], px, r[3]], px, fg);
868            tx += px + ui.dp(8);
869        }
870    }
871    if !icons {
872        let avail = (r[0] + r[2] - ui.dp(8) - tx).max(1);
873        let label = sp.label.unwrap_or("");
874        ui.ts_font(TS_BODY);
875        let s = if ui.text.width(label) > avail { ui.ellipsize(label.as_bytes(), avail).0 } else { String::from(label) };
876        if sp.icon == 0 || s != "…" {
877            text_at(ui, TS_BODY, tx, r, &s, fg, avail);
878        }
879    }
880    // a bar along the bottom: the accent under the active window, short and quiet under the
881    // others
882    let t = ui.dp(3);
883    let (bw, c) = if on { (ui.dp(16), ui.color(if hc { T_ON_ACCENT } else { T_ACCENT })) } else { (ui.dp(6), ui.color(T_TEXT3)) };
884    fill(ui, r[0] + ((r[2] - bw) >> 1), r[1] + r[3] - t - ui.dp(1), bw, t, t >> 1, c);
885}
886
887/// (sin, cos) of an angle in radians (single precision, enough for drawing)
888pub(crate) fn sincos(a: f32) -> (f32, f32) {
889    let (s, c) = gfx::xform::sincos(a as f64);
890    (s as f32, c as f32)
891}