smudge_core/variants.rs
1//! Style variants: patches the base style takes on while a node is hovered,
2//! keyboard focused, pressed or disabled.
3//!
4//! Every node carries one complete base [`Style`]. An
5//! element can declare a patch per interaction state, and the tree keeps the
6//! base style and the resolved style apart: builders and bindings write the
7//! base style, and layout and paint read the resolved style, which the tree
8//! recomputes whenever the base style or the interaction bits change.
9//!
10//! The patches apply in one fixed order: base, then hovered, then focused,
11//! then pressed, then disabled. A patch overrides exactly the fields its
12//! setters wrote, so a hovered background leaves the base width alone. The
13//! focused state applies only while the focus last moved by keyboard
14//! navigation, so a focus ring follows Tab and not a mouse click.
15//!
16//! A patch holds static values only. There are no reactive values inside a
17//! patch, and the setters of [`PatchBuilder`] take plain values, so a closure
18//! over signals cannot be written into one. A value that changes with state
19//! belongs in a binding on the base style, which the patches then override
20//! while their state is active.
21//!
22//! ```
23//! use smudge_core::geometry::Color;
24//! use smudge_core::style::px;
25//! use smudge_core::widget::panel;
26//!
27//! let button = panel()
28//! .width(px(80.0))
29//! .height(px(24.0))
30//! .background(Color::rgb(60, 60, 60))
31//! .hover(|p| p.background(Color::rgb(90, 90, 90)))
32//! .pressed(|p| p.background(Color::rgb(40, 40, 40)))
33//! .disabled_style(|p| p.background(Color::rgb(30, 30, 30)));
34//! ```
35
36use std::rc::Rc;
37
38use crate::geometry::Size;
39use crate::style::{Align, Direction, Edges, Justify, Length, Overflow, Position, Style};
40use crate::theme::{ColorValue, RadiusValue};
41
42use crate::tree::interaction::Interaction;
43
44/// One recorded setter of a style patch.
45type Setter = Rc<dyn Fn(&mut Style)>;
46
47/// A patch of static style values, applied over the base style while one
48/// interaction state is active.
49///
50/// Build one with [`PatchBuilder`], which the variant methods of
51/// [`Element`](crate::element::Element) run for you. The patch overrides the
52/// fields its setters wrote and leaves every other field of the base style
53/// alone.
54pub struct StylePatch {
55 /// The setters, run in the order they were recorded.
56 setters: Vec<Setter>,
57}
58
59impl StylePatch {
60 /// Applies every setter of the patch to `style`, in the order they were
61 /// recorded.
62 pub(crate) fn apply(&self, style: &mut Style) {
63 for setter in &self.setters {
64 setter(style);
65 }
66 }
67}
68
69/// Builds a [`StylePatch`] with the same setters the element builders have,
70/// for static values only.
71///
72/// A variant method such as [`hover`](crate::element::Element::hover) hands a
73/// fresh builder to the closure the caller passes and takes the builder back
74/// when the closure returns it, so a patch reads as a chain:
75/// `|p| p.background(color).border(1.0)`.
76#[derive(Default)]
77pub struct PatchBuilder {
78 /// The setters, in the order they were recorded.
79 setters: Vec<Setter>,
80}
81
82impl PatchBuilder {
83 /// Records a setter that writes one field of the style.
84 fn push(mut self, set: impl Fn(&mut Style) + 'static) -> Self {
85 self.setters.push(Rc::new(set));
86 self
87 }
88
89 /// Sets the width. This takes part in layout.
90 pub fn width(self, width: Length) -> Self {
91 self.push(move |style| style.layout.width = width)
92 }
93
94 /// Sets the height. This takes part in layout.
95 pub fn height(self, height: Length) -> Self {
96 self.push(move |style| style.layout.height = height)
97 }
98
99 /// Sets the width and the height, in logical pixels. This takes part in
100 /// layout.
101 pub fn size(self, size: Size) -> Self {
102 self.push(move |style| {
103 style.layout.width = Length::px(size.width);
104 style.layout.height = Length::px(size.height);
105 })
106 }
107
108 /// Sets the lower bound of the width. This takes part in layout.
109 pub fn min_width(self, width: Length) -> Self {
110 self.push(move |style| style.layout.min_width = width)
111 }
112
113 /// Sets the lower bound of the height. This takes part in layout.
114 pub fn min_height(self, height: Length) -> Self {
115 self.push(move |style| style.layout.min_height = height)
116 }
117
118 /// Sets the upper bound of the width. This takes part in layout.
119 pub fn max_width(self, width: Length) -> Self {
120 self.push(move |style| style.layout.max_width = width)
121 }
122
123 /// Sets the upper bound of the height. This takes part in layout.
124 pub fn max_height(self, height: Length) -> Self {
125 self.push(move |style| style.layout.max_height = height)
126 }
127
128 /// Sets the same padding on all four sides. This takes part in layout.
129 pub fn padding(self, padding: Length) -> Self {
130 self.push(move |style| style.layout.padding = Edges::all(padding))
131 }
132
133 /// Sets the horizontal padding from `width` and the vertical padding from
134 /// `height`. This takes part in layout.
135 pub fn padding_xy(self, padding: Size) -> Self {
136 self.push(move |style| {
137 style.layout.padding = Edges::xy(Length::px(padding.width), Length::px(padding.height));
138 })
139 }
140
141 /// Sets the same margin on all four sides. This takes part in layout.
142 pub fn margin(self, margin: Length) -> Self {
143 self.push(move |style| style.layout.margin = Edges::all(margin))
144 }
145
146 /// Sets the space between adjacent children. This takes part in layout.
147 pub fn gap(self, gap: Size) -> Self {
148 self.push(move |style| style.layout.gap = gap)
149 }
150
151 /// Sets the main axis of the children's layout. This takes part in layout.
152 pub fn direction(self, direction: Direction) -> Self {
153 self.push(move |style| style.layout.direction = direction)
154 }
155
156 /// Lays the children out left to right. This takes part in layout.
157 pub fn row(self) -> Self {
158 self.push(|style| style.layout.direction = Direction::Row)
159 }
160
161 /// Lays the children out top to bottom. This takes part in layout.
162 pub fn column(self) -> Self {
163 self.push(|style| style.layout.direction = Direction::Column)
164 }
165
166 /// Lets children that do not fit move to a new line instead of shrinking.
167 /// This takes part in layout.
168 pub fn wrap(self, wrap: bool) -> Self {
169 self.push(move |style| style.layout.wrap = wrap)
170 }
171
172 /// Sets the share of the leftover space on the main axis this node takes.
173 /// This takes part in layout.
174 pub fn grow(self, grow: f32) -> Self {
175 self.push(move |style| style.layout.grow = grow)
176 }
177
178 /// Sets how readily this node gives up space when its siblings overflow.
179 /// This takes part in layout.
180 pub fn shrink(self, shrink: f32) -> Self {
181 self.push(move |style| style.layout.shrink = shrink)
182 }
183
184 /// Sets the main axis size the node starts from before growing or
185 /// shrinking. This takes part in layout.
186 pub fn basis(self, basis: Length) -> Self {
187 self.push(move |style| style.layout.basis = basis)
188 }
189
190 /// Sets how the children line up across the main axis. This takes part in
191 /// layout.
192 pub fn align_items(self, align: Align) -> Self {
193 self.push(move |style| style.layout.align_items = Some(align))
194 }
195
196 /// Sets how the children are spread along the main axis. This takes part
197 /// in layout.
198 pub fn justify_content(self, justify: Justify) -> Self {
199 self.push(move |style| style.layout.justify_content = Some(justify))
200 }
201
202 /// Takes the node out of normal flow, so its insets place it against its
203 /// containing block. This takes part in layout.
204 pub fn absolute(self) -> Self {
205 self.push(|style| style.layout.position = Position::Absolute)
206 }
207
208 /// Sets the offsets from the containing block's edges. This takes part in
209 /// layout.
210 pub fn inset(self, inset: Edges<Length>) -> Self {
211 self.push(move |style| style.layout.inset = inset)
212 }
213
214 /// Sets the border width, the same on all four sides. This takes part in
215 /// layout, because the border takes space away from the content.
216 pub fn border(self, width: f32) -> Self {
217 self.push(move |style| style.layout.border_width = Edges::all(width))
218 }
219
220 /// Clips the children to the node's bounds. This takes part in layout.
221 pub fn clip(self, clip: bool) -> Self {
222 self.push(move |style| {
223 style.layout.overflow = if clip {
224 Overflow::Clip
225 } else {
226 Overflow::Visible
227 };
228 })
229 }
230
231 /// Hides the node and its subtree, or shows them again. This takes part
232 /// in layout.
233 pub fn hidden(self, hidden: bool) -> Self {
234 self.push(move |style| style.layout.hidden = hidden)
235 }
236
237 /// Sets the fill painted behind the node.
238 ///
239 /// The color can be a [`Color`](crate::geometry::Color) or a
240 /// [`ColorToken`](crate::theme::ColorToken). A token resolves against the
241 /// theme that is current when the node paints.
242 pub fn background(self, color: impl Into<ColorValue>) -> Self {
243 let color = color.into();
244 self.push(move |style| style.paint.background = Some(color))
245 }
246
247 /// Sets the color of the border whose width [`PatchBuilder::border`]
248 /// sets.
249 ///
250 /// The color can be a [`Color`](crate::geometry::Color) or a
251 /// [`ColorToken`](crate::theme::ColorToken). A token resolves against the
252 /// theme that is current when the node paints.
253 pub fn border_color(self, color: impl Into<ColorValue>) -> Self {
254 let color = color.into();
255 self.push(move |style| style.paint.border_color = Some(color))
256 }
257
258 /// Sets the color a widget's text paints with.
259 ///
260 /// Widgets that show text read it; widgets without text ignore it. The
261 /// color can be a [`Color`](crate::geometry::Color) or a
262 /// [`ColorToken`](crate::theme::ColorToken). A token resolves against the
263 /// theme that is current when the node paints.
264 pub fn text_color(self, color: impl Into<ColorValue>) -> Self {
265 let color = color.into();
266 self.push(move |style| style.paint.text_color = Some(color))
267 }
268
269 /// Sets the corner radius of the background and the border, the same on
270 /// all four corners.
271 ///
272 /// The radius can be an [`f32`] or a
273 /// [`RadiusToken`](crate::theme::RadiusToken). A token resolves against
274 /// the theme that is current when the node paints.
275 pub fn corner_radius(self, radius: impl Into<RadiusValue>) -> Self {
276 let radius = radius.into();
277 self.push(move |style| style.paint.corner_radius = radius)
278 }
279
280 /// Consumes the builder into the patch it recorded.
281 pub(crate) fn patch(self) -> StylePatch {
282 StylePatch {
283 setters: self.setters,
284 }
285 }
286}
287
288/// The style patches of one node, one per interaction state.
289///
290/// An element builds this through its
291/// [`hover`](crate::element::Element::hover),
292/// [`focused`](crate::element::Element::focused),
293/// [`pressed`](crate::element::Element::pressed) and
294/// [`disabled_style`](crate::element::Element::disabled_style) methods. A node
295/// without any patch never changes its style with its interaction state.
296pub struct Variants {
297 /// Applied while the node is in the hovered chain.
298 pub hovered: Option<StylePatch>,
299 /// Applied while the node holds the keyboard focus and the focus last
300 /// moved by keyboard navigation.
301 pub focused: Option<StylePatch>,
302 /// Applied while a primary press is held on the node.
303 pub pressed: Option<StylePatch>,
304 /// Applied while the node is disabled.
305 pub disabled: Option<StylePatch>,
306}
307
308impl Default for Variants {
309 /// A set with no patches, which leaves the base style in force in every
310 /// state.
311 fn default() -> Variants {
312 Variants {
313 hovered: None,
314 focused: None,
315 pressed: None,
316 disabled: None,
317 }
318 }
319}
320
321impl Variants {
322 /// Returns whether no state carries a patch.
323 pub(crate) fn is_empty(&self) -> bool {
324 self.hovered.is_none()
325 && self.focused.is_none()
326 && self.pressed.is_none()
327 && self.disabled.is_none()
328 }
329}
330
331/// Resolves the style of a node from its base style, its patches and its
332/// interaction state.
333///
334/// The base style comes first, then the hovered patch, then the focused one,
335/// then the pressed one, then the disabled one. Each patch overrides the
336/// fields its setters wrote; a later patch wins over an earlier one for the
337/// same field. The base style is not modified.
338pub(crate) fn resolve(base: &Style, variants: &Variants, interaction: Interaction) -> Style {
339 let mut style = base.clone();
340 if interaction.has(Interaction::HOVERED)
341 && let Some(patch) = &variants.hovered
342 {
343 patch.apply(&mut style);
344 }
345 if interaction.has(Interaction::KEYBOARD_FOCUS)
346 && let Some(patch) = &variants.focused
347 {
348 patch.apply(&mut style);
349 }
350 if interaction.has(Interaction::PRESSED)
351 && let Some(patch) = &variants.pressed
352 {
353 patch.apply(&mut style);
354 }
355 if interaction.has(Interaction::DISABLED)
356 && let Some(patch) = &variants.disabled
357 {
358 patch.apply(&mut style);
359 }
360 style
361}
362
363#[cfg(test)]
364#[path = "variants_tests.rs"]
365mod tests;