iced_aw/widget/
labeled_frame.rs

1//! This module provides a [`LabeledFrame`] widget which allows you to draw a border with a title around some content.
2
3use iced_core::{Length, Pixels, Rectangle};
4
5/// The style of a [`LabeledFrame`]
6pub struct Style {
7    /// The color of the border/frame
8    pub color: iced_core::Background,
9    /// The border radius of the border/frame
10    pub radius: iced_core::border::Radius,
11}
12
13/// The theme catalog of a [`LabeledFrame`]
14pub trait Catalog {
15    /// The item class of [`Catalog`]
16    type Class<'a>;
17    /// The default class produced by the [`Catalog`]
18    fn default<'a>() -> Self::Class<'a>;
19    /// The [`Style`] of a class
20    fn style(&self, class: &Self::Class<'_>) -> Style;
21}
22
23/// A styling function for a [`LabeledFrame`]
24pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
25
26impl Catalog for iced_widget::Theme {
27    type Class<'a> = StyleFn<'a, Self>;
28
29    fn default<'a>() -> Self::Class<'a> {
30        Box::new(|theme| Style {
31            color: iced_core::Background::Color(theme.extended_palette().secondary.weak.color),
32            radius: iced_core::border::Radius::default(),
33        })
34    }
35
36    fn style(&self, class: &Self::Class<'_>) -> Style {
37        (class)(self)
38    }
39}
40
41/// A labeled frame widget
42pub struct LabeledFrame<'a, Message, Theme, Renderer>
43where
44    Theme: Catalog,
45{
46    title: iced_core::Element<'a, Message, Theme, Renderer>,
47    content: iced_core::Element<'a, Message, Theme, Renderer>,
48    width: Length,
49    height: Length,
50    class: Theme::Class<'a>,
51    inset: f32,
52    outset: f32,
53    stroke_width: f32,
54    horizontal_title_padding: f32,
55}
56
57impl<'a, Message, Theme, Renderer> LabeledFrame<'a, Message, Theme, Renderer>
58where
59    Theme: Catalog,
60{
61    /// Creates a new [`LabeledFrame`]
62    pub fn new(
63        title: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
64        content: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
65    ) -> Self {
66        Self {
67            title: title.into(),
68            content: content.into(),
69            width: Length::Shrink,
70            height: Length::Shrink,
71            class: Theme::default(),
72            inset: 5.0,
73            outset: 5.0,
74            stroke_width: 3.0,
75            horizontal_title_padding: 5.0,
76        }
77    }
78
79    /// Sets the width of the [`LabeledFrame`]
80    #[must_use]
81    pub fn width(mut self, width: impl Into<Length>) -> Self {
82        self.width = width.into();
83        self
84    }
85
86    /// Sets the height of the [`LabeledFrame`]
87    #[must_use]
88    pub fn height(mut self, height: impl Into<Length>) -> Self {
89        self.height = height.into();
90        self
91    }
92
93    /// Sets the inset that is between the border and the inner content
94    #[must_use]
95    pub fn inset(mut self, inset: impl Into<Pixels>) -> Self {
96        self.inset = inset.into().0;
97        self
98    }
99
100    /// Sets the outset that is put around the border
101    #[must_use]
102    pub fn outset(mut self, outset: impl Into<Pixels>) -> Self {
103        self.outset = outset.into().0;
104        self
105    }
106
107    /// Sets the width of the stroke/border drawn around the content
108    #[must_use]
109    pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
110        self.stroke_width = stroke_width.into().0;
111        self
112    }
113
114    /// Sets the padding that the title gets on the horizontal axis
115    #[must_use]
116    pub fn horizontal_title_padding(mut self, padding: impl Into<Pixels>) -> Self {
117        self.horizontal_title_padding = padding.into().0;
118        self
119    }
120
121    /// Changes the style of the [`LabeledFrame`]
122    #[must_use]
123    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
124    where
125        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
126    {
127        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
128        self
129    }
130}
131
132impl<Message, Theme, Renderer> iced_core::Widget<Message, Theme, Renderer>
133    for LabeledFrame<'_, Message, Theme, Renderer>
134where
135    Renderer: iced_core::Renderer,
136    Theme: Catalog,
137{
138    fn size(&self) -> iced_core::Size<Length> {
139        iced_core::Size::new(self.width, self.height)
140    }
141
142    fn layout(
143        &mut self,
144        tree: &mut iced_core::widget::Tree,
145        renderer: &Renderer,
146        limits: &iced_core::layout::Limits,
147    ) -> iced_core::layout::Node {
148        let limits = (*limits).height(self.height).width(self.width).loose();
149        let title_layout = self
150            .title
151            .as_widget_mut()
152            .layout(
153                &mut tree.children[0],
154                renderer,
155                &limits.shrink(
156                    iced_core::Padding::default()
157                        .left(
158                            (self.outset + self.inset + self.stroke_width) * 2.0
159                                + self.horizontal_title_padding,
160                        )
161                        .right(
162                            (self.outset + self.inset + self.stroke_width) * 2.0
163                                + self.horizontal_title_padding,
164                        ),
165                ),
166            )
167            .translate([
168                (self.outset + self.inset + self.stroke_width) * 2.0
169                    + self.horizontal_title_padding,
170                0.0,
171            ]);
172        let content_layout = self
173            .content
174            .as_widget_mut()
175            .layout(
176                &mut tree.children[1],
177                renderer,
178                &limits.shrink(
179                    iced_core::Padding::default()
180                        .left(self.outset + self.inset + self.stroke_width)
181                        .right(self.outset + self.inset + self.stroke_width)
182                        .bottom(self.outset + self.inset + self.stroke_width)
183                        .top(
184                            (self.outset + self.inset + self.stroke_width)
185                                .max(title_layout.size().height),
186                        ),
187                ),
188            )
189            .translate([
190                self.outset + self.inset + self.stroke_width,
191                (self.outset + self.inset + self.stroke_width).max(title_layout.bounds().height),
192            ]);
193
194        iced_core::layout::Node::with_children(
195            limits.resolve(
196                self.width,
197                self.height,
198                iced_core::Size::new(
199                    (content_layout.size().width
200                        + (self.outset + self.inset + self.stroke_width) * 2.0)
201                        .max(
202                            title_layout.size().width
203                                + (self.outset + self.inset + self.stroke_width) * 4.0,
204                        ),
205                    content_layout.bounds().y
206                        + content_layout.size().height
207                        + (self.outset + self.inset + self.stroke_width),
208                ),
209            ),
210            vec![title_layout, content_layout],
211        )
212    }
213
214    fn children(&self) -> Vec<iced_core::widget::Tree> {
215        vec![
216            iced_core::widget::Tree::new(&self.title),
217            iced_core::widget::Tree::new(&self.content),
218        ]
219    }
220
221    fn diff(&self, tree: &mut iced_core::widget::Tree) {
222        tree.diff_children(&[&self.title, &self.content]);
223    }
224
225    fn draw(
226        &self,
227        tree: &iced_core::widget::Tree,
228        renderer: &mut Renderer,
229        theme: &Theme,
230        style: &iced_core::renderer::Style,
231        layout: iced_core::Layout<'_>,
232        cursor: iced_core::mouse::Cursor,
233        viewport: &iced_core::Rectangle,
234    ) {
235        [&self.title, &self.content]
236            .iter()
237            .zip(&tree.children)
238            .zip(layout.children())
239            .for_each(|((child, state), layout)| {
240                child
241                    .as_widget()
242                    .draw(state, renderer, theme, style, layout, cursor, viewport);
243            });
244        let style = theme.style(&self.class);
245        let title_layout = layout.children().next().expect("missing title layout");
246        let top_line_y =
247            title_layout.position().y + (title_layout.bounds().height - self.stroke_width) / 2.0;
248        // left line
249        renderer.fill_quad(
250            iced_core::renderer::Quad {
251                bounds: iced_core::Rectangle {
252                    x: layout.position().x + self.outset,
253                    y: top_line_y,
254                    width: self.stroke_width,
255                    height: layout.bounds().height
256                        - self.outset
257                        - (top_line_y - layout.position().y),
258                },
259                border: iced_core::Border {
260                    radius: iced_core::border::Radius::default()
261                        .top_left(style.radius.top_left)
262                        .bottom_left(style.radius.bottom_left),
263                    ..Default::default()
264                },
265                shadow: iced_core::Shadow::default(),
266                ..Default::default()
267            },
268            style.color,
269        );
270        // right line
271        renderer.fill_quad(
272            iced_core::renderer::Quad {
273                bounds: iced_core::Rectangle {
274                    x: layout.position().x + layout.bounds().width
275                        - self.outset
276                        - self.stroke_width,
277                    y: top_line_y,
278                    width: self.stroke_width,
279                    height: layout.bounds().height
280                        - self.outset
281                        - (top_line_y - layout.position().y),
282                },
283                border: iced_core::Border {
284                    radius: iced_core::border::Radius::default()
285                        .top_right(style.radius.top_right)
286                        .bottom_right(style.radius.bottom_right),
287                    ..Default::default()
288                },
289                shadow: iced_core::Shadow::default(),
290                ..Default::default()
291            },
292            style.color,
293        );
294        // bottom line
295        renderer.fill_quad(
296            iced_core::renderer::Quad {
297                bounds: iced_core::Rectangle {
298                    x: layout.position().x + self.outset,
299                    y: layout.position().y + layout.bounds().height
300                        - self.outset
301                        - self.stroke_width,
302                    width: layout.bounds().width - self.outset * 2.0,
303                    height: self.stroke_width,
304                },
305                border: iced_core::Border {
306                    radius: iced_core::border::Radius::default()
307                        .bottom_right(style.radius.top_right)
308                        .bottom_left(style.radius.top_left),
309                    ..Default::default()
310                },
311                shadow: iced_core::Shadow::default(),
312                ..Default::default()
313            },
314            style.color,
315        );
316        // top line left
317        renderer.fill_quad(
318            iced_core::renderer::Quad {
319                bounds: iced_core::Rectangle {
320                    x: layout.position().x + self.outset,
321                    y: top_line_y,
322                    width: title_layout.position().x
323                        - (layout.position().x + self.outset)
324                        - self.horizontal_title_padding,
325                    height: self.stroke_width,
326                },
327                border: iced_core::Border {
328                    radius: iced_core::border::Radius::default().top_left(style.radius.top_left),
329                    ..Default::default()
330                },
331                shadow: iced_core::Shadow::default(),
332                ..Default::default()
333            },
334            style.color,
335        );
336        // top line right
337        renderer.fill_quad(
338            iced_core::renderer::Quad {
339                bounds: iced_core::Rectangle {
340                    x: title_layout.position().x
341                        + title_layout.bounds().width
342                        + self.horizontal_title_padding,
343                    y: top_line_y,
344                    width: (layout.position().x + layout.bounds().width - self.outset)
345                        - (title_layout.position().x + title_layout.bounds().width)
346                        - self.horizontal_title_padding,
347                    height: self.stroke_width,
348                },
349                border: iced_core::Border {
350                    radius: iced_core::border::Radius::default().top_right(style.radius.top_right),
351                    ..Default::default()
352                },
353                shadow: iced_core::Shadow::default(),
354                ..Default::default()
355            },
356            style.color,
357        );
358    }
359
360    fn mouse_interaction(
361        &self,
362        state: &iced_core::widget::Tree,
363        layout: iced_core::Layout<'_>,
364        cursor: iced_core::mouse::Cursor,
365        viewport: &iced_core::Rectangle,
366        renderer: &Renderer,
367    ) -> iced_core::mouse::Interaction {
368        [&self.title, &self.content]
369            .iter()
370            .zip(&state.children)
371            .zip(layout.children())
372            .map(|((child, state), layout)| {
373                child
374                    .as_widget()
375                    .mouse_interaction(state, layout, cursor, viewport, renderer)
376            })
377            .max()
378            .unwrap_or_default()
379    }
380
381    fn update(
382        &mut self,
383        state: &mut iced_core::widget::Tree,
384        event: &iced_core::Event,
385        layout: iced_core::Layout<'_>,
386        cursor: iced_core::mouse::Cursor,
387        renderer: &Renderer,
388        clipboard: &mut dyn iced_core::Clipboard,
389        shell: &mut iced_core::Shell<'_, Message>,
390        viewport: &iced_core::Rectangle,
391    ) {
392        for ((child, state), layout) in [&mut self.title, &mut self.content]
393            .iter_mut()
394            .zip(&mut state.children)
395            .zip(layout.children())
396        {
397            child.as_widget_mut().update(
398                state, event, layout, cursor, renderer, clipboard, shell, viewport,
399            );
400        }
401    }
402
403    fn operate(
404        &mut self,
405        state: &mut iced_core::widget::Tree,
406        layout: iced_core::Layout<'_>,
407        renderer: &Renderer,
408        operation: &mut dyn iced_core::widget::Operation,
409    ) {
410        operation.container(None, layout.bounds());
411        operation.traverse(&mut |operation| {
412            [&mut self.title, &mut self.content]
413                .iter_mut()
414                .zip(&mut state.children)
415                .zip(layout.children())
416                .for_each(|((child, state), layout)| {
417                    child
418                        .as_widget_mut()
419                        .operate(state, layout, renderer, operation);
420                });
421        });
422    }
423
424    fn overlay<'b>(
425        &'b mut self,
426        state: &'b mut iced_core::widget::Tree,
427        layout: iced_core::Layout<'b>,
428        renderer: &Renderer,
429        viewport: &Rectangle,
430        translation: iced_core::Vector,
431    ) -> Option<iced_core::overlay::Element<'b, Message, Theme, Renderer>> {
432        let children = vec![&mut self.title, &mut self.content]
433            .into_iter()
434            .zip(&mut state.children)
435            .zip(layout.children())
436            .filter_map(|((child, state), layout)| {
437                child
438                    .as_widget_mut()
439                    .overlay(state, layout, renderer, viewport, translation)
440            })
441            .collect::<Vec<_>>();
442
443        (!children.is_empty()).then(|| iced_core::overlay::Group::with_children(children).overlay())
444    }
445
446    fn size_hint(&self) -> iced_core::Size<Length> {
447        self.size()
448    }
449}
450
451impl<'a, Message, Theme, Renderer> From<LabeledFrame<'a, Message, Theme, Renderer>>
452    for iced_core::Element<'a, Message, Theme, Renderer>
453where
454    Message: 'a,
455    Theme: 'a + Catalog,
456    Renderer: iced_core::Renderer + 'a,
457{
458    fn from(value: LabeledFrame<'a, Message, Theme, Renderer>) -> Self {
459        iced_core::Element::new(value)
460    }
461}