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}