Skip to main content

denise_ui/widgets/
panel.rs

1//! A themed rectangle: the background every other widget sits on.
2
3use denise::Pen;
4use denise::{Radius, Role};
5
6use crate::widget::{PaintCtx, Widget};
7use crate::widgets::describe::{
8    Describe, DynDescribe, Group, Mismatch, Property, PropertyKind, RADII, ROLES, Value,
9    role_from_name,
10};
11
12/// A filled, optionally bordered rounded rectangle.
13///
14/// Panels are not interactive and are invisible to hit testing, so putting a
15/// button on one does not mean the panel steals the click. [`Panel::backdrop`]
16/// is the exception, for the sheet under an overlay's contents.
17#[derive(Clone, Copy, Debug)]
18pub struct Panel {
19    /// Background role, or `None` to leave what is underneath alone.
20    pub fill: Option<Role>,
21    /// Border role, or `None` for no border.
22    pub border: Option<Role>,
23    /// Border thickness in pixels, drawn inside the bounds.
24    pub border_width: i32,
25    /// Corner rounding token. The theme decides the pixels.
26    pub radius: Radius,
27    /// Whether presses stop here instead of falling through.
28    ///
29    /// See [`Panel::backdrop`]. Off for every ordinary panel.
30    pub backdrop: bool,
31}
32
33impl Default for Panel {
34    fn default() -> Self {
35        Self {
36            fill: Some(Role::Base200),
37            border: Some(Role::Base300),
38            border_width: 1,
39            radius: Radius::Box,
40            backdrop: false,
41        }
42    }
43}
44
45impl Panel {
46    /// A panel filled with `role` and no border.
47    pub const fn filled(role: Role) -> Self {
48        Self {
49            fill: Some(role),
50            border: None,
51            border_width: 0,
52            radius: Radius::Box,
53            backdrop: false,
54        }
55    }
56
57    /// A panel that draws nothing: no fill, no border.
58    ///
59    /// A container, for when the *grouping* is the point — a `tabs` node's
60    /// pages, one per tab, shown and hidden as a unit. The tree already gives a
61    /// node a rectangle, a clip and children; this is the widget for a node
62    /// that wants those and no appearance of its own.
63    ///
64    /// ```
65    /// # use denise_ui::widgets::Panel;
66    /// // Nothing to see, and that is the whole idea.
67    /// let page = Panel::bare();
68    /// # let _ = page;
69    /// ```
70    pub const fn bare() -> Self {
71        Self {
72            fill: None,
73            border: None,
74            border_width: 0,
75            radius: Radius::Box,
76            backdrop: false,
77        }
78    }
79
80    /// A panel that presses stop at, without disturbing the focus.
81    ///
82    /// The sheet behind an overlay's contents. An ordinary panel is invisible to
83    /// hit testing, which is right for a card with a button on it and wrong for
84    /// the sheet under an on-screen keyboard: a finger landing in the gap
85    /// between two keys falls through to whatever is behind the overlay, and
86    /// pressing *that* takes the focus away from the field being typed into —
87    /// so a near-miss dismisses the keyboard.
88    ///
89    /// This absorbs the press and leaves the focus exactly where it was, which
90    /// is the same bargain [`Button::no_focus`](crate::widgets::Button::no_focus)
91    /// makes for the keys themselves.
92    #[must_use]
93    pub const fn backdrop(mut self) -> Self {
94        self.backdrop = true;
95        self
96    }
97
98    /// Sets the corner rounding token.
99    pub const fn with_radius(mut self, radius: Radius) -> Self {
100        self.radius = radius;
101        self
102    }
103
104    /// Sets the border role and thickness.
105    pub const fn with_border(mut self, role: Role, width: i32) -> Self {
106        self.border = Some(role);
107        self.border_width = width;
108        self
109    }
110}
111
112impl<M: 'static> Widget<M> for Panel {
113    fn describe(&self) -> Option<&dyn DynDescribe> {
114        Some(self)
115    }
116
117    fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
118        Some(self)
119    }
120    fn accepts_pointer(&self) -> bool {
121        self.backdrop
122    }
123
124    /// A backdrop is pressed *past*, not pressed: it must not move the focus and
125    /// must not clear it.
126    fn preserves_focus(&self) -> bool {
127        self.backdrop
128    }
129
130    fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
131        let radius = ctx.theme.radius(self.radius);
132        if let Some(role) = self.fill {
133            canvas.fill_rounded_rect(ctx.bounds, radius, ctx.theme.color(role));
134        }
135        if let Some(role) = self.border
136            && self.border_width > 0
137        {
138            canvas.stroke_rounded_rect(
139                ctx.bounds,
140                radius,
141                self.border_width,
142                ctx.theme.color(role),
143            );
144        }
145    }
146}
147
148/// The name that clears a colour rather than choosing one.
149const NONE: &str = "none";
150
151/// Every [`Role`], plus [`NONE`].
152///
153/// `fill` and `border` are `Option<Role>`, so a form file needs a way to say the
154/// absence — `fill=none` — and an inspector needs to offer it in the same list it
155/// offers the colours in. Built from [`ROLES`] rather than written out again
156/// because a slice cannot be concatenated in a `const`, and a second copy of the
157/// role names is a second thing to forget when one is added.
158const fn roles_or_none() -> [&'static str; ROLES.len() + 1] {
159    let mut names = [NONE; ROLES.len() + 1];
160    let mut i = 0;
161    while i < ROLES.len() {
162        names[i] = ROLES[i];
163        i += 1;
164    }
165    // The last stays `NONE`, which is what the array was filled with.
166    names
167}
168
169/// [`roles_or_none`] as a slice, which is what [`PropertyKind::Enum`] takes.
170const ROLES_OR_NONE: &[&str] = &roles_or_none();
171
172/// A role, or the absence of one.
173fn role_or_none(value: Value) -> Result<Option<Role>, Mismatch> {
174    let name = value.as_name()?;
175    if name == NONE {
176        return Ok(None);
177    }
178    role_from_name(name).map(Some).ok_or(Mismatch::WrongType {
179        expected: PropertyKind::Enum(ROLES_OR_NONE),
180    })
181}
182
183impl Describe for Panel {
184    const KIND: &'static str = "panel";
185    const DOC: &'static str = "A themed rectangle: the background other widgets sit on.";
186    const GROUP: Group = Group::Container;
187    const ICON: &'static denise::icon::Icon = &super::icons::PANEL;
188
189    const PROPERTIES: &'static [Property] = &[
190        Property::new(
191            "fill",
192            PropertyKind::Enum(ROLES_OR_NONE),
193            "Surface colour. `none` leaves what is underneath alone.",
194        ),
195        Property::new(
196            "border",
197            PropertyKind::Enum(ROLES_OR_NONE),
198            "Border colour. `none` for no border.",
199        ),
200        Property::new(
201            "border-width",
202            PropertyKind::Int { min: 0, max: 16 },
203            "Border thickness in pixels, drawn inside the bounds.",
204        )
205        .in_pixels(),
206        Property::new(
207            "radius",
208            PropertyKind::Enum(RADII),
209            "Corner rounding token. The theme decides the pixels.",
210        ),
211        Property::new(
212            "backdrop",
213            PropertyKind::Bool,
214            "This panel absorbs presses rather than letting them fall through, and leaves the focus where it is. What the sheet under an on-screen keyboard is.",
215        ),
216    ];
217
218    fn get(&self, name: &str) -> Option<Value> {
219        Some(match name {
220            // A panel with no fill reports nothing rather than reporting
221            // `none`: an unset property is one nothing has to write down.
222            "fill" => Value::role(self.fill?),
223            "border" => Value::role(self.border?),
224            "border-width" => Value::Int(self.border_width),
225            "radius" => Value::radius(self.radius),
226            "backdrop" => Value::Bool(self.backdrop),
227            _ => return None,
228        })
229    }
230
231    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
232        match name {
233            "fill" => self.fill = role_or_none(value)?,
234            "border" => self.border = role_or_none(value)?,
235            // Negative is not a thinner border, it is an inverted rectangle by
236            // the time `stroke_rounded_rect` sees it.
237            "border-width" => self.border_width = value.as_int()?.max(0),
238            "radius" => self.radius = value.as_radius()?,
239            "backdrop" => self.backdrop = value.as_bool()?,
240            _ => return Err(Mismatch::Unknown),
241        }
242        Ok(())
243    }
244}