Skip to main content

ui/
action_card.rs

1// action_card -- a card or a list row that does one thing when it is chosen: a template on a
2// start page, a recent file, a lesson
3//
4//   ui.action_card(id, name, &CardDesc) -> EV_*   EV_ACT when it was clicked, or Enter or Space
5//                                                 while it has the keyboard
6//
7// CardDesc says what it shows: a title (with an icon and a badge beside it), a text below (wrapped,
8// or one line cut with "…"), a caption at the far end, and above them a picture area (`picture_h`
9// DIP, 0: none) with the app's drawing, without one the icon large and centred.  `name` is what
10// screen readers and scripts call it (K_BUTTON); the texts follow as records of their own.
11//
12// next_size sets the width (natural: 200 DIP); the height follows the content, and in a row of
13// cards they all take the row's height.  next_flags: ST_SUBTLE a list row (no card at rest, the
14// hover fill alone), ST_CHECKED selected, ST_DISABLED no input (the texts in the disabled colour).
15// The card is one Tab stop; a button of its own (remove from a list) goes beside it, not into it.
16
17use crate::consts::*;
18use crate::{DrawRef, Ui};
19
20const PAD: u32 = 10;
21const GAP: i32 = 8;
22
23/// what an action card shows (ui.action_card); ..Default::default() for the parts it has not
24#[derive(Clone, Default)]
25pub struct CardDesc<'a> {
26    pub title: &'a str,
27    /// beside the title; with a picture area and no drawing, large in that area (0: none)
28    pub icon: u32,
29    /// below the title ("": none)
30    pub text: &'a str,
31    /// title and text stay on one line, cut with "…" (lists); else they wrap
32    pub one_line: bool,
33    /// a BADGE_* kind and its label, beside the title
34    pub badge: Option<(u32, &'a str)>,
35    /// a caption at the far end ("": none)
36    pub aside: &'a str,
37    /// the height of the picture area above the texts (DIP; 0: none)
38    pub picture_h: i32,
39    /// what it shows and the version of that drawing (render time, as cmd_custom)
40    pub draw: Option<DrawRef>,
41    pub draw_ver: u32,
42}
43
44impl Ui {
45    /// action_card(id, name, &desc) -> EV_*: a card or list row that does one thing (the top of
46    /// action_card.rs); next_size sets its width, next_flags ST_SUBTLE, ST_CHECKED, ST_DISABLED
47    #[cfg_attr(jfgui_dev, track_caller)]
48    pub fn action_card(&mut self, id: u32, name: &str, d: &CardDesc) -> u32 {
49        let _src = self.src_scope();
50        let flags = self.take_flags();
51        let disabled = flags & ST_DISABLED != 0;
52        if self.wp.lay.next_w == 0 {
53            self.next_size(200, 0);
54        }
55        self.box_begin(0, BOX_V, 0);
56        let r = self.box_rect();
57        self.wp.ctx.last_rect = r;
58        let ev = self.behave(id, HB_FOCUS | CUR_HAND << HB_CURSOR_SHIFT | if disabled { HB_DISABLED } else { 0 });
59        let state = self.wp.ctx.last_state;
60        let hot = !disabled && ev & EV_HOVER != 0;
61        let held = hot && ev & EV_HELD != 0;
62        let checked = flags & ST_CHECKED != 0;
63        let subtle = flags & ST_SUBTLE != 0;
64        // the surface: the theme's card, or nothing at rest for a list row
65        let radius = self.dp(if subtle { 4 } else { 6 });
66        let line = self.dp(1);
67        let (fill, stroke) = if subtle {
68            let f = self.color(if held { T_SUBTLE_PRESSED } else if hot || checked { T_SUBTLE_HOVER } else { T_SUBTLE });
69            (f, f)
70        } else {
71            let f = self.color(if held { T_CTRL_PRESSED } else if hot || checked { T_CTRL_HOVER } else { T_CARD });
72            (f, self.color(if hot || checked { T_ACCENT } else { T_CARD_STROKE }))
73        };
74        if r[2] > 0 && r[3] > 0 {
75            self.cmd_rrect(r[0], r[1], r[2], r[3], radius, line, fill, stroke, stroke);
76        }
77        // the picture area: the drawing, else the icon large
78        if d.picture_h > 0 {
79            let area = self.place(r[2].max(1), self.dp(d.picture_h));
80            if area[2] > 2 * line && area[3] > line {
81                let (x, y, w, h) = (area[0] + line, area[1] + line, area[2] - 2 * line, area[3] - line);
82                let top = self.color(T_LAYER);
83                self.cmd_rrect(x, y, w, h, radius, 0, top, top, top);
84                if let Some(dr) = &d.draw {
85                    self.cmd_custom(x, y, w, h, Some(dr.clone()), 0, d.draw_ver);
86                } else if d.icon != 0 {
87                    let s = self.dp(32).min(h);
88                    let c = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT3 });
89                    self.draw_color(c);
90                    self.draw_icon(d.icon, x + (w - s) / 2, y + (h - s) / 2, s << 8);
91                }
92            }
93        }
94        // the texts
95        let dis = if disabled { TX_DISABLED } else { 0 };
96        let how = if d.one_line { TX_ELLIPSIS } else { TX_WRAP };
97        self.next_size(-1, 0);
98        self.box_begin(0, BOX_H | pad(PAD), GAP);
99        if d.icon != 0 && d.picture_h == 0 {
100            let s = self.dp(20);
101            let at = self.place(s, s);
102            let c = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT2 });
103            self.draw_color(c);
104            self.draw_icon(d.icon, at[0], at[1], s << 8);
105        }
106        self.next_size(-1, 0);
107        self.box_begin(0, BOX_V, 2);
108        self.next_size(-1, 0);
109        self.box_begin(0, BOX_H, GAP);
110        self.next_size(-1, 0);
111        self.text(TS_BODY_STRONG | how | dis, d.title);
112        if let Some((kind, label)) = d.badge {
113            self.badge(kind, label);
114        }
115        self.box_end();
116        if !d.text.is_empty() {
117            self.next_size(-1, 0);
118            self.text(TS_CAPTION | TX_SECONDARY | how | dis, d.text);
119        }
120        self.box_end();
121        if !d.aside.is_empty() {
122            self.text(TS_CAPTION | TX_SECONDARY | dis, d.aside);
123        }
124        self.box_end();
125        self.box_end();
126        // the focus ring and the record over the whole card
127        self.wp.ctx.last_rect = r;
128        self.wp.ctx.last_state = state | if checked { SS_CHECKED as u32 } else { 0 };
129        self.focus_ring_draw(ev);
130        self.sem(K_BUTTON, id, Some(name), 0);
131        ev
132    }
133}