Skip to main content

egui_reactor/
paint.rs

1//! Paint attributes: [`PaintStyle`], the look of a box.
2//!
3//! [`layout`](crate::layout) says where a box is; this says what it looks like.
4//! It is a field of [`ItemStyle`](crate::layout::ItemStyle), so every element
5//! takes it through the one `style` prop it already has, and `rsx!` spells it
6//! with the shorthands `bg` `border` `radius` `shadow` `custom_shadow`
7//! `opacity`.
8//!
9//! Nothing here draws. The engine knows the rect of a node only after the
10//! layout is solved, so it claims a shape slot in draw order and fills it with
11//! what these helpers return once the frame's layout is final. That is why the
12//! helpers take a rect and hand back a [`Shape`]: the same three shapes serve
13//! the taffy path and the lite path.
14
15use egui::epaint::RectShape;
16use egui::{Color32, Rect, Shadow, Shape, Stroke, StrokeKind, Visuals};
17
18/// How a box looks: background, border, corner radius, shadow and opacity.
19///
20/// Every field is optional and `Default` paints nothing, so an element that
21/// nobody styled costs no shapes at all.
22///
23/// The three shapes all sit on the node's *border box* — its whole box, not
24/// the content rect — and share one `radius`. A `border` of width `n` is also
25/// reserved by the layout (`ItemStyle::to_taffy` sets taffy's `border`), so the
26/// stroke is drawn `StrokeKind::Inside` and lands exactly in the band the
27/// children were kept out of.
28#[derive(Clone, Copy, Debug, Default, PartialEq)]
29pub struct PaintStyle {
30    /// Background colour, filled behind the children or the widget.
31    pub bg: Option<Color32>,
32    /// Border stroke, drawn on top of them and reserved by the layout.
33    pub border: Option<Stroke>,
34    /// Corner radius, shared by the shadow, the background and the border.
35    pub radius: Option<f32>,
36    /// Cast the theme's `Visuals::window_shadow`.
37    pub shadow: bool,
38    /// Cast this shadow instead, whatever `shadow` says.
39    pub custom_shadow: Option<Shadow>,
40    /// Multiply the opacity of the node and everything under it.
41    pub opacity: Option<f32>,
42}
43
44impl PaintStyle {
45    /// Set the background colour.
46    #[must_use]
47    pub fn bg(mut self, v: impl Into<Color32>) -> Self {
48        self.bg = Some(v.into());
49        self
50    }
51
52    /// Set the border stroke. The layout reserves its width.
53    #[must_use]
54    pub fn border(mut self, v: impl Into<Stroke>) -> Self {
55        self.border = Some(v.into());
56        self
57    }
58
59    /// Set the corner radius of all three shapes.
60    #[must_use]
61    pub fn radius(mut self, v: f32) -> Self {
62        self.radius = Some(v);
63        self
64    }
65
66    /// Cast the theme's window shadow, or stop casting it.
67    #[must_use]
68    pub fn shadow(mut self, v: bool) -> Self {
69        self.shadow = v;
70        self
71    }
72
73    /// Cast this shadow instead of the theme's.
74    #[must_use]
75    pub fn custom_shadow(mut self, v: Shadow) -> Self {
76        self.custom_shadow = Some(v);
77        self
78    }
79
80    /// Multiply the opacity of this node and everything under it.
81    #[must_use]
82    pub fn opacity(mut self, v: f32) -> Self {
83        self.opacity = Some(v);
84        self
85    }
86
87    /// Whether this style does nothing, so the engine can skip the node.
88    ///
89    /// `radius` alone does not count: it only rounds shapes that another field
90    /// asks for. `opacity` does, even though it draws nothing itself, because
91    /// the engine still has to set it around the children.
92    pub fn is_none(&self) -> bool {
93        self.bg.is_none()
94            && self.border.is_none()
95            && !self.shadow
96            && self.custom_shadow.is_none()
97            && self.opacity.is_none()
98    }
99
100    /// The corner radius the three shapes share, `0.0` when unset.
101    fn corner_radius(&self) -> f32 {
102        self.radius.unwrap_or(0.0)
103    }
104
105    /// The shadow shape under `rect`, if this style casts one.
106    ///
107    /// `custom_shadow` wins over `shadow`; `shadow` alone is the theme's
108    /// window shadow, which is why this needs the `Visuals`. `Shadow::as_shape`
109    /// offsets and expands the rect itself, so `rect` is the plain border box.
110    pub fn shadow_shape(&self, rect: Rect, visuals: &Visuals) -> Option<Shape> {
111        let shadow = self
112            .custom_shadow
113            .or_else(|| self.shadow.then_some(visuals.window_shadow))?;
114        Some(shadow.as_shape(rect, self.corner_radius()).into())
115    }
116
117    /// The background shape filling `rect`, if there is a `bg`.
118    pub fn bg_shape(&self, rect: Rect) -> Option<Shape> {
119        let bg = self.bg?;
120        Some(RectShape::filled(rect, self.corner_radius(), bg).into())
121    }
122
123    /// The border shape around `rect`, if there is a `border`.
124    ///
125    /// `StrokeKind::Inside`: the layout already reserved the stroke's width
126    /// inside the border box, so drawing it there costs no extra room and two
127    /// neighbours with borders do not overlap.
128    pub fn border_shape(&self, rect: Rect) -> Option<Shape> {
129        let border = self.border?;
130        Some(RectShape::stroke(rect, self.corner_radius(), border, StrokeKind::Inside).into())
131    }
132}