Skip to main content

vizia_core/views/
badge.rs

1use crate::prelude::*;
2
3/// Enum which represents the placement of a badge on its parent.
4#[derive(Default, Debug, Clone, Copy, PartialEq)]
5pub enum BadgePlacement {
6    /// The badge should be placed at the top-left of the view.
7    TopLeft,
8    /// The badge should be placed at the top of the view.
9    Top,
10    /// The badge should be placed at the top-right of the view.
11    #[default]
12    TopRight,
13    /// The badge should be placed at the left of the view.
14    Left,
15    /// The badge should be placed at the right of the view.
16    Right,
17    /// The badge should be placed at the bottom-left of the view.
18    BottomLeft,
19    /// The badge should be placed at the bottom of the view.
20    Bottom,
21    /// The badge should be placed at the bottom-right of the view.
22    BottomRight,
23}
24
25impl_res_simple!(BadgePlacement);
26
27/// A Badge view for showing notifications, counts, or status information.
28pub struct Badge {
29    placement: Signal<BadgePlacement>,
30}
31
32impl BadgePlacement {
33    /// Mirrors Left↔Right placements for RTL layouts. Top and Bottom are unchanged.
34    fn flip_h(self) -> Self {
35        match self {
36            Self::TopLeft => Self::TopRight,
37            Self::TopRight => Self::TopLeft,
38            Self::BottomLeft => Self::BottomRight,
39            Self::BottomRight => Self::BottomLeft,
40            Self::Left => Self::Right,
41            Self::Right => Self::Left,
42            other => other,
43        }
44    }
45}
46
47impl Badge {
48    fn common<F>(cx: &mut Context, content: F) -> Handle<Self>
49    where
50        F: FnOnce(&mut Context),
51    {
52        let placement = Signal::new(BadgePlacement::TopRight);
53        let direction = cx.environment().direction;
54        let combined = Memo::new(move |_| (placement.get(), direction.get()));
55
56        Self { placement }.build(cx, content).bind(combined, move |mut handle| {
57            let (raw_placement, env_dir) = combined.get();
58            // Prefer the per-entity computed direction (set via CSS or a direction modifier)
59            // so a user can override direction on a specific badge without changing the global
60            // setting. Fall back to env_dir to stay reactive on global direction changes.
61            let entity = handle.entity();
62            let dir = handle.cx.style.direction.get(entity).copied().unwrap_or(env_dir);
63            let placement =
64                if dir == Direction::RightToLeft { raw_placement.flip_h() } else { raw_placement };
65
66            let (t, b) = match placement {
67                BadgePlacement::TopLeft | BadgePlacement::TopRight => {
68                    (Stretch(1.0), Percentage(85.35))
69                }
70                BadgePlacement::Top => (Stretch(1.0), Percentage(100.0)),
71                BadgePlacement::Bottom => (Percentage(100.0), Stretch(1.0)),
72                BadgePlacement::BottomLeft | BadgePlacement::BottomRight => {
73                    (Percentage(85.35), Stretch(1.0))
74                }
75                BadgePlacement::Left | BadgePlacement::Right => (Stretch(1.0), Stretch(1.0)),
76            };
77
78            let (l, r) = match placement {
79                BadgePlacement::TopLeft | BadgePlacement::BottomLeft => {
80                    (Stretch(1.0), Percentage(85.35))
81                }
82                BadgePlacement::TopRight | BadgePlacement::BottomRight => {
83                    (Percentage(85.35), Stretch(1.0))
84                }
85                BadgePlacement::Left => (Stretch(1.0), Percentage(100.0)),
86                BadgePlacement::Right => (Percentage(100.0), Stretch(1.0)),
87                BadgePlacement::Top | BadgePlacement::Bottom => (Stretch(1.0), Stretch(1.0)),
88            };
89
90            handle = handle.top(t).bottom(b).left(l).right(r);
91
92            let translate = match placement {
93                BadgePlacement::TopLeft => (Percentage(50.0), Percentage(50.0)),
94                BadgePlacement::Top => (Percentage(0.0), Percentage(50.0)),
95                BadgePlacement::TopRight => (Percentage(-50.0), Percentage(50.0)),
96                BadgePlacement::BottomLeft => (Percentage(50.0), Percentage(-50.0)),
97                BadgePlacement::Bottom => (Percentage(0.0), Percentage(-50.0)),
98                BadgePlacement::BottomRight => (Percentage(-50.0), Percentage(-50.0)),
99                BadgePlacement::Left => (Percentage(50.0), Percentage(0.0)),
100                BadgePlacement::Right => (Percentage(-50.0), Percentage(0.0)),
101            };
102            handle.translate(translate);
103        })
104    }
105
106    /// Creates an empty badge.
107    ///
108    /// ```
109    /// # use vizia_core::prelude::*;
110    /// # use vizia_core::icons::ICON_USER;
111    /// # let cx = &mut Context::default();
112    /// Avatar::new(cx, |cx|{
113    ///     Svg::new(cx, ICON_USER);
114    /// })
115    /// .badge(|cx| Badge::empty(cx).class("error"));
116    /// ```
117    pub fn empty(cx: &mut Context) -> Handle<Self> {
118        Self::common(cx, |_| {})
119    }
120
121    /// Creates a new badge with the provided content.
122    ///
123    /// ```
124    /// # use vizia_core::prelude::*;
125    /// # use vizia_core::icons::ICON_USER;
126    /// # let cx = &mut Context::default();
127    /// Avatar::new(cx, |cx|{
128    ///     Svg::new(cx, ICON_USER);
129    /// })
130    /// .badge(|cx| Badge::new(cx, |cx| Label::new(cx, "2")));
131    /// ```
132    pub fn new<F, V>(cx: &mut Context, content: F) -> Handle<Self>
133    where
134        F: FnOnce(&mut Context) -> Handle<V>,
135        V: View,
136    {
137        Self::common(cx, |cx| {
138            (content)(cx);
139        })
140    }
141}
142
143impl View for Badge {
144    fn element(&self) -> Option<&'static str> {
145        Some("badge")
146    }
147}
148
149impl Handle<'_, Badge> {
150    /// Sets the placement of a badge relative to its parent. Accepts a value or signal of type [BadgePlacement].
151    pub fn placement<U: Into<BadgePlacement> + Clone + 'static>(
152        self,
153        placement: impl Res<U> + 'static,
154    ) -> Self {
155        let placement = placement.to_signal(self.cx);
156        self.bind(placement, move |handle| {
157            let value = placement.get();
158            let converted: BadgePlacement = value.into();
159            handle.modify(|badge| {
160                badge.placement.set(converted);
161            });
162        })
163    }
164}