iced_widget/
rule.rs

1//! Rules divide space horizontally or vertically.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } }
6//! # pub type State = ();
7//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
8//! use iced::widget::rule;
9//!
10//! #[derive(Clone)]
11//! enum Message {
12//!     // ...,
13//! }
14//!
15//! fn view(state: &State) -> Element<'_, Message> {
16//!     rule::horizontal(2).into()
17//! }
18//! ```
19use crate::core;
20use crate::core::border;
21use crate::core::layout;
22use crate::core::mouse;
23use crate::core::renderer;
24use crate::core::widget::Tree;
25use crate::core::{Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget};
26
27/// Creates a new horizontal [`Rule`] with the given height.
28pub fn horizontal<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme>
29where
30    Theme: Catalog,
31{
32    Rule {
33        thickness: Length::Fixed(height.into().0),
34        is_vertical: false,
35        class: Theme::default(),
36    }
37}
38
39/// Creates a new vertical [`Rule`] with the given width.
40pub fn vertical<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme>
41where
42    Theme: Catalog,
43{
44    Rule {
45        thickness: Length::Fixed(width.into().0),
46        is_vertical: true,
47        class: Theme::default(),
48    }
49}
50
51/// Display a horizontal or vertical rule for dividing content.
52///
53/// # Example
54/// ```no_run
55/// # mod iced { pub mod widget { pub use iced_widget::*; } }
56/// # pub type State = ();
57/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
58/// use iced::widget::rule;
59///
60/// #[derive(Clone)]
61/// enum Message {
62///     // ...,
63/// }
64///
65/// fn view(state: &State) -> Element<'_, Message> {
66///     rule::horizontal(2).into()
67/// }
68/// ```
69pub struct Rule<'a, Theme = crate::Theme>
70where
71    Theme: Catalog,
72{
73    thickness: Length,
74    is_vertical: bool,
75    class: Theme::Class<'a>,
76}
77
78impl<'a, Theme> Rule<'a, Theme>
79where
80    Theme: Catalog,
81{
82    /// Sets the style of the [`Rule`].
83    #[must_use]
84    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
85    where
86        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
87    {
88        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
89        self
90    }
91
92    /// Sets the style class of the [`Rule`].
93    #[cfg(feature = "advanced")]
94    #[must_use]
95    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
96        self.class = class.into();
97        self
98    }
99}
100
101impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<'_, Theme>
102where
103    Renderer: core::Renderer,
104    Theme: Catalog,
105{
106    fn size(&self) -> Size<Length> {
107        if self.is_vertical {
108            Size {
109                width: self.thickness,
110                height: Length::Fill,
111            }
112        } else {
113            Size {
114                width: Length::Fill,
115                height: self.thickness,
116            }
117        }
118    }
119
120    fn layout(
121        &mut self,
122        _tree: &mut Tree,
123        _renderer: &Renderer,
124        limits: &layout::Limits,
125    ) -> layout::Node {
126        let size = <Self as Widget<(), Theme, Renderer>>::size(self);
127
128        layout::atomic(limits, size.width, size.height)
129    }
130
131    fn draw(
132        &self,
133        _tree: &Tree,
134        renderer: &mut Renderer,
135        theme: &Theme,
136        _style: &renderer::Style,
137        layout: Layout<'_>,
138        _cursor: mouse::Cursor,
139        _viewport: &Rectangle,
140    ) {
141        let bounds = layout.bounds();
142        let style = theme.style(&self.class);
143
144        let bounds = if self.is_vertical {
145            let line_x = bounds.x.round();
146
147            let (offset, line_height) = style.fill_mode.fill(bounds.height);
148            let line_y = bounds.y + offset;
149
150            Rectangle {
151                x: line_x,
152                y: line_y,
153                width: bounds.width,
154                height: line_height,
155            }
156        } else {
157            let line_y = bounds.y.round();
158
159            let (offset, line_width) = style.fill_mode.fill(bounds.width);
160            let line_x = bounds.x + offset;
161
162            Rectangle {
163                x: line_x,
164                y: line_y,
165                width: line_width,
166                height: bounds.height,
167            }
168        };
169
170        renderer.fill_quad(
171            renderer::Quad {
172                bounds,
173                border: border::rounded(style.radius),
174                snap: style.snap,
175                ..renderer::Quad::default()
176            },
177            style.color,
178        );
179    }
180}
181
182impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>> for Element<'a, Message, Theme, Renderer>
183where
184    Message: 'a,
185    Theme: 'a + Catalog,
186    Renderer: 'a + core::Renderer,
187{
188    fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
189        Element::new(rule)
190    }
191}
192
193/// The appearance of a rule.
194#[derive(Debug, Clone, Copy, PartialEq)]
195pub struct Style {
196    /// The color of the rule.
197    pub color: Color,
198    /// The radius of the line corners.
199    pub radius: border::Radius,
200    /// The [`FillMode`] of the rule.
201    pub fill_mode: FillMode,
202    /// Whether the rule should be snapped to the pixel grid.
203    pub snap: bool,
204}
205
206/// The fill mode of a rule.
207#[derive(Debug, Clone, Copy, PartialEq)]
208pub enum FillMode {
209    /// Fill the whole length of the container.
210    Full,
211    /// Fill a percent of the length of the container. The rule
212    /// will be centered in that container.
213    ///
214    /// The range is `[0.0, 100.0]`.
215    Percent(f32),
216    /// Uniform offset from each end, length units.
217    Padded(u16),
218    /// Different offset on each end of the rule, length units.
219    /// First = top or left.
220    AsymmetricPadding(u16, u16),
221}
222
223impl FillMode {
224    /// Return the starting offset and length of the rule.
225    ///
226    /// * `space` - The space to fill.
227    ///
228    /// # Returns
229    ///
230    /// * (`starting_offset`, `length`)
231    pub fn fill(&self, space: f32) -> (f32, f32) {
232        match *self {
233            FillMode::Full => (0.0, space),
234            FillMode::Percent(percent) => {
235                if percent >= 100.0 {
236                    (0.0, space)
237                } else {
238                    let percent_width = (space * percent / 100.0).round();
239
240                    (((space - percent_width) / 2.0).round(), percent_width)
241                }
242            }
243            FillMode::Padded(padding) => {
244                if padding == 0 {
245                    (0.0, space)
246                } else {
247                    let padding = padding as f32;
248                    let mut line_width = space - (padding * 2.0);
249                    if line_width < 0.0 {
250                        line_width = 0.0;
251                    }
252
253                    (padding, line_width)
254                }
255            }
256            FillMode::AsymmetricPadding(first_pad, second_pad) => {
257                let first_pad = first_pad as f32;
258                let second_pad = second_pad as f32;
259                let mut line_width = space - first_pad - second_pad;
260                if line_width < 0.0 {
261                    line_width = 0.0;
262                }
263
264                (first_pad, line_width)
265            }
266        }
267    }
268}
269
270/// The theme catalog of a [`Rule`].
271pub trait Catalog: Sized {
272    /// The item class of the [`Catalog`].
273    type Class<'a>;
274
275    /// The default class produced by the [`Catalog`].
276    fn default<'a>() -> Self::Class<'a>;
277
278    /// The [`Style`] of a class with the given status.
279    fn style(&self, class: &Self::Class<'_>) -> Style;
280}
281
282/// A styling function for a [`Rule`].
283///
284/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
285pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
286
287impl Catalog for Theme {
288    type Class<'a> = StyleFn<'a, Self>;
289
290    fn default<'a>() -> Self::Class<'a> {
291        Box::new(default)
292    }
293
294    fn style(&self, class: &Self::Class<'_>) -> Style {
295        class(self)
296    }
297}
298
299/// The default styling of a [`Rule`].
300pub fn default(theme: &Theme) -> Style {
301    let palette = theme.extended_palette();
302
303    Style {
304        color: palette.background.strong.color,
305        radius: 0.0.into(),
306        fill_mode: FillMode::Full,
307        snap: true,
308    }
309}
310
311/// A [`Rule`] styling using the weak background color.
312pub fn weak(theme: &Theme) -> Style {
313    let palette = theme.extended_palette();
314
315    Style {
316        color: palette.background.weak.color,
317        radius: 0.0.into(),
318        fill_mode: FillMode::Full,
319        snap: true,
320    }
321}