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