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