Skip to main content

vizia_core/views/
avatar.rs

1use vizia_storage::LayoutChildIterator;
2
3use crate::prelude::*;
4
5/// Enum which represents the geometric variants of an avatar view.
6#[derive(Debug, Default, Clone, Copy, PartialEq)]
7pub enum AvatarVariant {
8    #[default]
9    /// Represents a circular avatar shape.
10    Circle,
11    /// Represents a square avatar shape.
12    Square,
13    /// Represents a  rounded rectangle avatar shape.
14    Rounded,
15}
16
17/// An avatar view is used to visually represent a person or entity and can contain text, an icon, or an image.
18///
19/// # Example
20/// ```
21/// # use vizia_core::prelude::*;
22/// # use vizia_core::icons::ICON_USER;
23/// # let cx = &mut Context::default();
24/// Avatar::new(cx, |cx|{
25///     Svg::new(cx, ICON_USER);
26/// });
27/// ```
28pub struct Avatar {}
29
30impl Avatar {
31    /// Creates a new avatar with the given content.
32    ///
33    /// ```
34    /// # use vizia_core::prelude::*;
35    /// # use vizia_core::icons::ICON_USER;
36    /// # let cx = &mut Context::default();
37    /// Avatar::new(cx, |cx|{
38    ///     Svg::new(cx, ICON_USER);
39    /// });
40    /// ```
41    pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
42    where
43        F: FnOnce(&mut Context),
44    {
45        Self {}.build(cx, content).variant(AvatarVariant::Circle).control_size(ControlSize::Medium)
46    }
47}
48
49impl View for Avatar {
50    fn element(&self) -> Option<&'static str> {
51        Some("avatar")
52    }
53}
54
55/// Modifiers for changing the appearance and content of an [Avatar].
56pub trait AvatarModifiers: Sized {
57    /// Selects the geometric variant of the Avatar. Accepts a value or signal of type [AvatarVariant].
58    ///
59    /// ```
60    /// # use vizia_core::prelude::*;
61    /// # use vizia_core::icons::ICON_USER;
62    /// # let cx = &mut Context::default();
63    /// Avatar::new(cx, |cx|{
64    ///     Svg::new(cx, ICON_USER);
65    /// })
66    /// .variant(AvatarVariant::Rounded);
67    /// ```
68    fn variant<U: Into<AvatarVariant> + Clone + PartialEq + 'static>(
69        self,
70        variant: impl Res<U> + 'static,
71    ) -> Self;
72
73    /// Adds a badge to the Avatar.
74    ///
75    /// ```
76    /// # use vizia_core::prelude::*;
77    /// # use vizia_core::icons::ICON_USER;
78    /// # let cx = &mut Context::default();
79    /// Avatar::new(cx, |cx|{
80    ///     Svg::new(cx, ICON_USER);
81    /// })
82    /// .badge(|cx| Badge::empty(cx).class("error"));
83    /// ```
84    #[allow(unused_variables)]
85    fn badge<F>(self, content: F) -> Self
86    where
87        F: FnOnce(&mut Context) -> Handle<'_, Badge>,
88    {
89        self
90    }
91}
92
93impl AvatarModifiers for Handle<'_, Avatar> {
94    fn variant<U: Into<AvatarVariant> + Clone + PartialEq + 'static>(
95        mut self,
96        variant: impl Res<U> + 'static,
97    ) -> Self {
98        let avatar_variant = variant.to_signal(self.context()).map(|value| value.clone().into());
99
100        let is_circle = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Circle);
101
102        let is_square = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Square);
103
104        let is_rounded = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Rounded);
105
106        self.toggle_class("circle", is_circle)
107            .toggle_class("square", is_square)
108            .toggle_class("rounded", is_rounded)
109    }
110
111    fn badge<F>(mut self, content: F) -> Self
112    where
113        F: FnOnce(&mut Context) -> Handle<'_, Badge>,
114    {
115        let entity = self.entity();
116
117        self.context().with_current(entity, |cx| {
118            (content)(cx);
119        });
120
121        self
122    }
123}
124
125impl ControlModifiers for Handle<'_, Avatar> {
126    fn control_size<U: Into<ControlSize> + Clone + 'static>(
127        self,
128        size: impl Res<U> + 'static,
129    ) -> Self {
130        crate::modifiers::bind_control_size(self, size)
131    }
132}
133
134/// The [AvatarGroup] view can be used to group a series of avatars together.
135pub struct AvatarGroup {}
136
137impl AvatarGroup {
138    /// Create a new [AvatarGroup]. The content should be a series of [Avatar] views.
139    pub fn new<F>(cx: &mut Context, content: F) -> Handle<Self>
140    where
141        F: FnOnce(&mut Context),
142    {
143        Self {}.build(cx, content)
144    }
145}
146
147fn apply_avatar_group_max_visible(cx: &mut Context, entity: Entity, max_visible: usize) {
148    let mut avatars = Vec::new();
149    let mut overflow_avatar = None;
150
151    for child in LayoutChildIterator::new(&cx.tree, entity) {
152        let is_overflow = cx
153            .style
154            .classes
155            .get(child)
156            .is_some_and(|class_list| class_list.contains("avatar-group-overflow"));
157
158        if is_overflow {
159            overflow_avatar = Some(child);
160        } else {
161            avatars.push(child);
162        }
163    }
164
165    if let Some(overflow_avatar) = overflow_avatar {
166        cx.remove(overflow_avatar);
167    }
168
169    let hidden_count = avatars.len().saturating_sub(max_visible);
170
171    for (index, avatar) in avatars.into_iter().enumerate() {
172        let display = if index < max_visible { Display::Flex } else { Display::None };
173
174        Handle::<Avatar> { current: entity, entity: avatar, p: Default::default(), cx }
175            .display(display);
176    }
177
178    if hidden_count > 0 {
179        cx.with_current(entity, |cx| {
180            Avatar::new(cx, move |cx| {
181                Label::new(cx, format!("+{}", hidden_count));
182            })
183            .class("avatar-group-overflow");
184        });
185    }
186}
187
188impl View for AvatarGroup {
189    fn element(&self) -> Option<&'static str> {
190        Some("avatar-group")
191    }
192}
193
194impl AvatarModifiers for Handle<'_, AvatarGroup> {
195    fn variant<U: Into<AvatarVariant> + Clone + PartialEq + 'static>(
196        mut self,
197        variant: impl Res<U> + 'static,
198    ) -> Self {
199        let avatar_variant = variant.to_signal(self.context()).map(|value| value.clone().into());
200
201        let is_circle = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Circle);
202
203        let is_square = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Square);
204
205        let is_rounded = Memo::new(move |_| avatar_variant.get() == AvatarVariant::Rounded);
206
207        self.toggle_class("circle", is_circle)
208            .toggle_class("square", is_square)
209            .toggle_class("rounded", is_rounded)
210    }
211}
212
213impl ControlModifiers for Handle<'_, AvatarGroup> {
214    fn control_size<U: Into<ControlSize> + Clone + 'static>(
215        self,
216        size: impl Res<U> + 'static,
217    ) -> Self {
218        crate::modifiers::bind_control_size(self, size)
219    }
220}
221
222impl Handle<'_, AvatarGroup> {
223    /// Limits the number of visible avatars in a group and adds a final overflow avatar with
224    /// a `+N` label when more avatars are present.
225    pub fn max_visible(self, max_visible: impl Res<usize> + 'static) -> Self {
226        let max_visible = max_visible.to_signal(self.cx);
227        self.bind(max_visible, move |mut handle| {
228            let max_visible = max_visible.get();
229            let entity = handle.entity();
230            apply_avatar_group_max_visible(handle.context(), entity, max_visible);
231        })
232    }
233}