Skip to main content

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;