Skip to main content

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::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # use iced::widget::Widget;
7//! # pub type State = ();
8//! use iced::widget::rule;
9//!
10//! #[derive(Clone)]
11//! enum Message {
12//!     // ...,
13//! }
14//!
15//! fn view(state: &State) -> impl Widget<Message> {
16//!     rule::horizontal(2)
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::{Meta, Tree};
25use crate::core::{Color, 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::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
56/// # use iced::widget::Widget;
57/// # pub type State = ();
58/// use iced::widget::rule;
59///
60/// #[derive(Clone)]
61/// enum Message {
62///     // ...,
63/// }
64///
65/// fn view(state: &State) -> impl Widget<Message> {
66///     rule::horizontal(2)
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<Theme> Meta for Rule<'_, Theme> where Theme: Catalog {}
102
103impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<'_, Theme>
104where
105    Renderer: core::Renderer,
106    Theme: Catalog,
107{
108    fn size(&self) -> Size<Length> {
109        if self.is_vertical {
110            Size {
111                width: self.thickness,
112                height: Length::Fill,
113            }
114        } else {
115            Size {
116                width: Length::Fill,
117                height: self.thickness,
118            }
119        }
120    }
121
122    fn layout(&mut self, tree: &mut Tree, _renderer: &Renderer, limits: &layout::Limits) {
123        let size = <Self as Widget<(), Theme, Renderer>>::size(self);
124
125        tree.size = layout::atomic(limits, size.width, size.height);
126    }
127
128    fn draw(
129        &self,
130        _tree: &Tree,
131        renderer: &mut Renderer,
132        theme: &Theme,
133        _style: &renderer::Style,
134        layout: Layout,
135        _cursor: mouse::Cursor,
136        _viewport: &Rectangle,
137    ) {
138        let bounds = layout.bounds();
139        let style = theme.style(&self.class);
140
141        let mut bounds = if self.is_vertical {
142            let line_x = bounds.x;
143
144            let (offset, line_height) = style.fill_mode.fill(bounds.height);
145            let line_y = bounds.y + offset;
146
147            Rectangle {
148                x: line_x,
149                y: line_y,
150                width: bounds.width,
151                height: line_height,
152            }
153        } else {
154            let line_y = bounds.y;
155
156            let (offset, line_width) = style.fill_mode.fill(bounds.width);
157            let line_x = bounds.x + offset;
158
159            Rectangle {
160                x: line_x,
161                y: line_y,
162                width: line_width,
163                height: bounds.height,
164            }
165        };
166
167        if style.snap {
168            let unit = 1.0 / renderer.hint_factor().unwrap_or(1.0);
169
170            bounds.width = bounds.width.max(unit);
171            bounds.height = bounds.height.max(unit);
172        }
173
174        renderer.fill_quad(
175            renderer::Quad {
176                bounds,
177                border: border::rounded(style.radius),
178                snap: style.snap,
179                ..renderer::Quad::default()
180            },
181            style.color,
182        );
183    }
184}
185
186/// The appearance of a rule.
187#[derive(Debug, Clone, Copy, PartialEq)]
188pub struct Style {
189    /// The color of the rule.
190    pub color: Color,
191    /// The radius of the line corners.
192    pub radius: border::Radius,
193    /// The [`FillMode`] of the rule.
194    pub fill_mode: FillMode,
195    /// Whether the rule should be snapped to the pixel grid.
196    pub snap: bool,
197}
198
199/// The fill mode of a rule.
200#[derive(Debug, Clone, Copy, PartialEq)]
201pub enum FillMode {
202    /// Fill the whole length of the container.
203    Full,
204    /// Fill a percent of the length of the container. The rule
205    /// will be centered in that container.
206    ///
207    /// The range is `[0.0, 100.0]`.
208    Percent(f32),
209    /// Uniform offset from each end, length units.
210    Padded(u16),
211    /// Different offset on each end of the rule, length units.
212    /// First = top or left.
213    AsymmetricPadding(u16, u16),
214}
215
216impl FillMode {
217    /// Return the starting offset and length of the rule.
218    ///
219    /// * `space` - The space to fill.
220    ///
221    /// # Returns
222    ///
223    /// * (`starting_offset`, `length`)
224    pub fn fill(&self, space: f32) -> (f32, f32) {
225        match *self {
226            FillMode::Full => (0.0, space),
227            FillMode::Percent(percent) => {
228                if percent >= 100.0 {
229                    (0.0, space)
230                } else {
231                    let percent_width = (space * percent / 100.0).round();
232
233                    (((space - percent_width) / 2.0).round(), percent_width)
234                }
235            }
236            FillMode::Padded(padding) => {
237                if padding == 0 {
238                    (0.0, space)
239                } else {
240                    let padding = padding as f32;
241                    let mut line_width = space - (padding * 2.0);
242                    if line_width < 0.0 {
243                        line_width = 0.0;
244                    }
245
246                    (padding, line_width)
247                }
248            }
249            FillMode::AsymmetricPadding(first_pad, second_pad) => {
250                let first_pad = first_pad as f32;
251                let second_pad = second_pad as f32;
252                let mut line_width = space - first_pad - second_pad;
253                if line_width < 0.0 {
254                    line_width = 0.0;
255                }
256
257                (first_pad, line_width)
258            }
259        }
260    }
261}
262
263/// The theme catalog of a [`Rule`].
264pub trait Catalog: Sized {
265    /// The item class of the [`Catalog`].
266    type Class<'a>;
267
268    /// The default class produced by the [`Catalog`].
269    fn default<'a>() -> Self::Class<'a>;
270
271    /// The [`Style`] of a class with the given status.
272    fn style(&self, class: &Self::Class<'_>) -> Style;
273}
274
275/// A styling function for a [`Rule`].
276///
277/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
278pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
279
280impl Catalog for Theme {
281    type Class<'a> = StyleFn<'a, Self>;
282
283    fn default<'a>() -> Self::Class<'a> {
284        Box::new(default)
285    }
286
287    fn style(&self, class: &Self::Class<'_>) -> Style {
288        class(self)
289    }
290}
291
292/// The default styling of a [`Rule`].
293pub fn default(theme: &Theme) -> Style {
294    let palette = theme.palette();
295
296    Style {
297        color: palette.background.strong.color,
298        radius: 0.0.into(),
299        fill_mode: FillMode::Full,
300        snap: true,
301    }
302}
303
304/// A [`Rule`] styling using the weak background color.
305pub fn weak(theme: &Theme) -> Style {
306    let palette = theme.palette();
307
308    Style {
309        color: palette.background.weak.color,
310        radius: 0.0.into(),
311        fill_mode: FillMode::Full,
312        snap: true,
313    }
314}