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::*; } }
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;
25use crate::core::widget::Tree;
26use crate::core::widget::operation::accessible::{Accessible, Role};
27use crate::core::{Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget};
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> 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(
123        &mut self,
124        _tree: &mut Tree,
125        _renderer: &Renderer,
126        limits: &layout::Limits,
127    ) -> layout::Node {
128        let size = <Self as Widget<(), Theme, Renderer>>::size(self);
129
130        layout::atomic(limits, size.width, size.height)
131    }
132
133    fn draw(
134        &self,
135        _tree: &Tree,
136        renderer: &mut Renderer,
137        theme: &Theme,
138        _style: &renderer::Style,
139        layout: Layout<'_>,
140        _cursor: mouse::Cursor,
141        _viewport: &Rectangle,
142    ) {
143        let bounds = layout.bounds();
144        let style = theme.style(&self.class);
145
146        let mut bounds = if self.is_vertical {
147            let line_x = bounds.x;
148
149            let (offset, line_height) = style.fill_mode.fill(bounds.height);
150            let line_y = bounds.y + offset;
151
152            Rectangle {
153                x: line_x,
154                y: line_y,
155                width: bounds.width,
156                height: line_height,
157            }
158        } else {
159            let line_y = bounds.y;
160
161            let (offset, line_width) = style.fill_mode.fill(bounds.width);
162            let line_x = bounds.x + offset;
163
164            Rectangle {
165                x: line_x,
166                y: line_y,
167                width: line_width,
168                height: bounds.height,
169            }
170        };
171
172        if style.snap {
173            let unit = 1.0 / renderer.scale_factor().unwrap_or(1.0);
174
175            bounds.width = bounds.width.max(unit);
176            bounds.height = bounds.height.max(unit);
177        }
178
179        renderer.fill_quad(
180            renderer::Quad {
181                bounds,
182                border: border::rounded(style.radius),
183                snap: style.snap,
184                ..renderer::Quad::default()
185            },
186            style.color,
187        );
188    }
189
190    fn operate(
191        &mut self,
192        _tree: &mut Tree,
193        layout: Layout<'_>,
194        _renderer: &Renderer,
195        operation: &mut dyn widget::Operation,
196    ) {
197        operation.accessible(
198            None,
199            layout.bounds(),
200            &Accessible {
201                role: Role::Separator,
202                ..Accessible::default()
203            },
204        );
205    }
206}
207
208impl<'a, Message, Theme, Renderer> From<Rule<'a, Theme>> for Element<'a, Message, Theme, Renderer>
209where
210    Message: 'a,
211    Theme: 'a + Catalog,
212    Renderer: 'a + core::Renderer,
213{
214    fn from(rule: Rule<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
215        Element::new(rule)
216    }
217}
218
219/// The appearance of a rule.
220#[derive(Debug, Clone, Copy, PartialEq)]
221pub struct Style {
222    /// The color of the rule.
223    pub color: Color,
224    /// The radius of the line corners.
225    pub radius: border::Radius,
226    /// The [`FillMode`] of the rule.
227    pub fill_mode: FillMode,
228    /// Whether the rule should be snapped to the pixel grid.
229    pub snap: bool,
230}
231
232/// The fill mode of a rule.
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum FillMode {
235    /// Fill the whole length of the container.
236    Full,
237    /// Fill a percent of the length of the container. The rule
238    /// will be centered in that container.
239    ///
240    /// The range is `[0.0, 100.0]`.
241    Percent(f32),
242    /// Uniform offset from each end, length units.
243    Padded(u16),
244    /// Different offset on each end of the rule, length units.
245    /// First = top or left.
246    AsymmetricPadding(u16, u16),
247}
248
249impl FillMode {
250    /// Return the starting offset and length of the rule.
251    ///
252    /// * `space` - The space to fill.
253    ///
254    /// # Returns
255    ///
256    /// * (`starting_offset`, `length`)
257    pub fn fill(&self, space: f32) -> (f32, f32) {
258        match *self {
259            FillMode::Full => (0.0, space),
260            FillMode::Percent(percent) => {
261                if percent >= 100.0 {
262                    (0.0, space)
263                } else {
264                    let percent_width = (space * percent / 100.0).round();
265
266                    (((space - percent_width) / 2.0).round(), percent_width)
267                }
268            }
269            FillMode::Padded(padding) => {
270                if padding == 0 {
271                    (0.0, space)
272                } else {
273                    let padding = padding as f32;
274                    let mut line_width = space - (padding * 2.0);
275                    if line_width < 0.0 {
276                        line_width = 0.0;
277                    }
278
279                    (padding, line_width)
280                }
281            }
282            FillMode::AsymmetricPadding(first_pad, second_pad) => {
283                let first_pad = first_pad as f32;
284                let second_pad = second_pad as f32;
285                let mut line_width = space - first_pad - second_pad;
286                if line_width < 0.0 {
287                    line_width = 0.0;
288                }
289
290                (first_pad, line_width)
291            }
292        }
293    }
294}
295
296/// The theme catalog of a [`Rule`].
297pub trait Catalog: Sized {
298    /// The item class of the [`Catalog`].
299    type Class<'a>;
300
301    /// The default class produced by the [`Catalog`].
302    fn default<'a>() -> Self::Class<'a>;
303
304    /// The [`Style`] of a class with the given status.
305    fn style(&self, class: &Self::Class<'_>) -> Style;
306}
307
308/// A styling function for a [`Rule`].
309///
310/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
311pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
312
313impl Catalog for Theme {
314    type Class<'a> = StyleFn<'a, Self>;
315
316    fn default<'a>() -> Self::Class<'a> {
317        Box::new(default)
318    }
319
320    fn style(&self, class: &Self::Class<'_>) -> Style {
321        class(self)
322    }
323}
324
325/// The default styling of a [`Rule`].
326pub fn default(theme: &Theme) -> Style {
327    let palette = theme.palette();
328
329    Style {
330        color: palette.background.strong.color,
331        radius: 0.0.into(),
332        fill_mode: FillMode::Full,
333        snap: true,
334    }
335}
336
337/// A [`Rule`] styling using the weak background color.
338pub fn weak(theme: &Theme) -> Style {
339    let palette = theme.palette();
340
341    Style {
342        color: palette.background.weak.color,
343        radius: 0.0.into(),
344        fill_mode: FillMode::Full,
345        snap: true,
346    }
347}