Skip to main content

material_ui_rs/widget/component/
badge.rs

1//! Material 3 badge constructors with token-backed layout defaults.
2
3use iced_widget::core::text as core_text;
4use iced_widget::core::widget::{Operation, Tree};
5use iced_widget::core::{
6    Clipboard, Element, Event, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector,
7    Widget, alignment, layout, mouse, overlay, renderer,
8};
9use iced_widget::text;
10use iced_widget::{Container, Text};
11
12use super::absolute_line_height;
13use crate::{Theme, style::badge as badge_style, tokens};
14
15use std::fmt;
16
17pub fn small<'a, Message, Renderer>() -> Container<'a, Message, Theme, Renderer>
18where
19    Message: 'a,
20    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
21{
22    Container::new(Text::new(""))
23        .width(Length::Fixed(tokens::component::badge::SMALL_SIZE))
24        .height(Length::Fixed(tokens::component::badge::SMALL_SIZE))
25        .style(badge_style::default)
26}
27
28pub fn large<'a, Message, Renderer>(
29    label: impl text::IntoFragment<'a>,
30) -> Container<'a, Message, Theme, Renderer>
31where
32    Message: 'a,
33    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
34{
35    let label_text = tokens::component::badge::LABEL_TEXT;
36
37    Container::new(
38        Text::new(label)
39            .size(label_text.size)
40            .line_height(absolute_line_height(label_text.line_height)),
41    )
42    .height(Length::Fixed(
43        tokens::component::badge::LARGE_CONTAINER_HEIGHT,
44    ))
45    .max_width(tokens::component::badge::LARGE_CONTAINER_MAX_WIDTH)
46    .padding(Padding::from([
47        0.0,
48        tokens::component::badge::LARGE_HORIZONTAL_SPACE,
49    ]))
50    .align_x(alignment::Horizontal::Center)
51    .align_y(alignment::Vertical::Center)
52    .style(badge_style::default)
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum BadgedBoxPlacement {
57    IconOnly,
58    WithContent,
59}
60
61impl BadgedBoxPlacement {
62    fn horizontal_offset(self) -> f32 {
63        match self {
64            Self::IconOnly => tokens::component::badge::ICON_ONLY_OFFSET,
65            Self::WithContent => tokens::component::badge::WITH_CONTENT_HORIZONTAL_OFFSET,
66        }
67    }
68
69    fn vertical_offset(self) -> f32 {
70        match self {
71            Self::IconOnly => tokens::component::badge::ICON_ONLY_OFFSET,
72            Self::WithContent => tokens::component::badge::WITH_CONTENT_VERTICAL_OFFSET,
73        }
74    }
75}
76
77pub fn badged_box<'a, Message, Renderer>(
78    anchor: impl Into<Element<'a, Message, Theme, Renderer>>,
79    badge: impl Into<Element<'a, Message, Theme, Renderer>>,
80    placement: BadgedBoxPlacement,
81) -> BadgedBox<'a, Message, Renderer>
82where
83    Renderer: iced_widget::core::Renderer,
84{
85    BadgedBox::new(anchor, badge, placement)
86}
87
88pub struct BadgedBox<'a, Message, Renderer = iced_widget::Renderer>
89where
90    Renderer: iced_widget::core::Renderer,
91{
92    width: Length,
93    height: Length,
94    placement: BadgedBoxPlacement,
95    children: Vec<Element<'a, Message, Theme, Renderer>>,
96}
97
98impl<'a, Message, Renderer> BadgedBox<'a, Message, Renderer>
99where
100    Renderer: iced_widget::core::Renderer,
101{
102    pub fn new(
103        anchor: impl Into<Element<'a, Message, Theme, Renderer>>,
104        badge: impl Into<Element<'a, Message, Theme, Renderer>>,
105        placement: BadgedBoxPlacement,
106    ) -> Self {
107        let anchor = anchor.into();
108        let size_hint = anchor.as_widget().size_hint();
109
110        Self {
111            width: size_hint.width,
112            height: size_hint.height,
113            placement,
114            children: vec![anchor, badge.into()],
115        }
116    }
117}
118
119impl<Message, Renderer> fmt::Debug for BadgedBox<'_, Message, Renderer>
120where
121    Renderer: iced_widget::core::Renderer,
122{
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.debug_struct("BadgedBox")
125            .field("width", &self.width)
126            .field("height", &self.height)
127            .field("placement", &self.placement)
128            .finish_non_exhaustive()
129    }
130}
131
132impl<Message, Renderer> Widget<Message, Theme, Renderer> for BadgedBox<'_, Message, Renderer>
133where
134    Renderer: iced_widget::core::Renderer,
135{
136    fn children(&self) -> Vec<Tree> {
137        self.children.iter().map(Tree::new).collect()
138    }
139
140    fn diff(&self, tree: &mut Tree) {
141        tree.diff_children(&self.children);
142    }
143
144    fn size(&self) -> Size<Length> {
145        Size {
146            width: self.width,
147            height: self.height,
148        }
149    }
150
151    fn layout(
152        &mut self,
153        tree: &mut Tree,
154        renderer: &Renderer,
155        limits: &layout::Limits,
156    ) -> layout::Node {
157        let limits = limits.width(self.width).height(self.height);
158        let anchor =
159            self.children[0]
160                .as_widget_mut()
161                .layout(&mut tree.children[0], renderer, &limits);
162        let size = limits.resolve(self.width, self.height, anchor.size());
163        let badge = self.children[1].as_widget_mut().layout(
164            &mut tree.children[1],
165            renderer,
166            &layout::Limits::NONE,
167        );
168        let badge_position = badged_box_badge_position(&anchor, &badge, self.placement);
169
170        layout::Node::with_children(
171            size,
172            vec![anchor.move_to(Point::ORIGIN), badge.move_to(badge_position)],
173        )
174    }
175
176    fn operate(
177        &mut self,
178        tree: &mut Tree,
179        layout: Layout<'_>,
180        renderer: &Renderer,
181        operation: &mut dyn Operation,
182    ) {
183        operation.container(None, layout.bounds());
184        operation.traverse(&mut |operation| {
185            self.children
186                .iter_mut()
187                .zip(&mut tree.children)
188                .zip(layout.children())
189                .for_each(|((child, state), layout)| {
190                    child
191                        .as_widget_mut()
192                        .operate(state, layout, renderer, operation);
193                });
194        });
195    }
196
197    fn update(
198        &mut self,
199        tree: &mut Tree,
200        event: &Event,
201        layout: Layout<'_>,
202        cursor: mouse::Cursor,
203        renderer: &Renderer,
204        clipboard: &mut dyn Clipboard,
205        shell: &mut Shell<'_, Message>,
206        viewport: &Rectangle,
207    ) {
208        for ((child, tree), layout) in self
209            .children
210            .iter_mut()
211            .zip(&mut tree.children)
212            .zip(layout.children())
213            .rev()
214        {
215            child.as_widget_mut().update(
216                tree, event, layout, cursor, renderer, clipboard, shell, viewport,
217            );
218
219            if shell.is_event_captured() {
220                return;
221            }
222        }
223    }
224
225    fn mouse_interaction(
226        &self,
227        tree: &Tree,
228        layout: Layout<'_>,
229        cursor: mouse::Cursor,
230        viewport: &Rectangle,
231        renderer: &Renderer,
232    ) -> mouse::Interaction {
233        self.children
234            .iter()
235            .rev()
236            .zip(tree.children.iter().rev())
237            .zip(layout.children().rev())
238            .map(|((child, tree), layout)| {
239                child
240                    .as_widget()
241                    .mouse_interaction(tree, layout, cursor, viewport, renderer)
242            })
243            .find(|interaction| *interaction != mouse::Interaction::None)
244            .unwrap_or_default()
245    }
246
247    fn draw(
248        &self,
249        tree: &Tree,
250        renderer: &mut Renderer,
251        theme: &Theme,
252        style: &renderer::Style,
253        layout: Layout<'_>,
254        cursor: mouse::Cursor,
255        viewport: &Rectangle,
256    ) {
257        if layout.bounds().intersection(viewport).is_some() {
258            for (index, ((child, tree), layout)) in self
259                .children
260                .iter()
261                .zip(&tree.children)
262                .zip(layout.children())
263                .enumerate()
264            {
265                if index == 0 {
266                    child
267                        .as_widget()
268                        .draw(tree, renderer, theme, style, layout, cursor, viewport);
269                } else {
270                    renderer.with_layer(*viewport, |renderer| {
271                        child
272                            .as_widget()
273                            .draw(tree, renderer, theme, style, layout, cursor, viewport);
274                    });
275                }
276            }
277        }
278    }
279
280    fn overlay<'b>(
281        &'b mut self,
282        tree: &'b mut Tree,
283        layout: Layout<'b>,
284        renderer: &Renderer,
285        viewport: &Rectangle,
286        translation: Vector,
287    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
288        overlay::from_children(
289            &mut self.children,
290            tree,
291            layout,
292            renderer,
293            viewport,
294            translation,
295        )
296    }
297}
298
299impl<'a, Message, Renderer> From<BadgedBox<'a, Message, Renderer>>
300    for Element<'a, Message, Theme, Renderer>
301where
302    Message: 'a,
303    Renderer: iced_widget::core::Renderer + 'a,
304{
305    fn from(badged_box: BadgedBox<'a, Message, Renderer>) -> Self {
306        Self::new(badged_box)
307    }
308}
309
310fn badged_box_badge_position(
311    anchor: &layout::Node,
312    badge: &layout::Node,
313    placement: BadgedBoxPlacement,
314) -> Point {
315    let anchor_measured = anchor.size();
316    let badge_measured = badge.size();
317
318    Point::new(
319        anchor_measured.width - placement.horizontal_offset(),
320        -badge_measured.height + placement.vertical_offset(),
321    )
322}
323
324#[cfg(test)]
325#[path = "../../../tests/widget/component/badge.rs"]
326mod tests;