Skip to main content

gpui_base/
avatar.rs

1use gpui::{
2    AnyElement, App, Div, ImageSource, InteractiveElement, Interactivity, IntoElement,
3    ParentElement, RenderOnce, StyleRefinement, Styled, Window, div, img,
4};
5use smallvec::SmallVec;
6
7use crate::StyledExt as _;
8
9/// An unstyled avatar root that renders its image slot or fallback slot.
10#[derive(IntoElement)]
11pub struct Avatar {
12    base: Div,
13    style: StyleRefinement,
14    image: Option<AvatarImage>,
15    fallback: Option<AvatarFallback>,
16}
17
18impl Avatar {
19    pub fn new() -> Self {
20        Self {
21            base: div(),
22            style: StyleRefinement::default(),
23            image: None,
24            fallback: None,
25        }
26    }
27
28    pub fn image(mut self, image: AvatarImage) -> Self {
29        self.image = Some(image);
30        self
31    }
32
33    pub fn fallback(mut self, fallback: AvatarFallback) -> Self {
34        self.fallback = Some(fallback);
35        self
36    }
37}
38
39impl Default for Avatar {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl Styled for Avatar {
46    fn style(&mut self) -> &mut StyleRefinement {
47        &mut self.style
48    }
49}
50
51impl InteractiveElement for Avatar {
52    fn interactivity(&mut self) -> &mut Interactivity {
53        self.base.interactivity()
54    }
55}
56
57impl RenderOnce for Avatar {
58    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
59        let content = self
60            .image
61            .map(IntoElement::into_any_element)
62            .or_else(|| self.fallback.map(IntoElement::into_any_element));
63        self.base.children(content).refine_style(&self.style)
64    }
65}
66
67/// An unstyled avatar image slot.
68#[derive(IntoElement)]
69pub struct AvatarImage {
70    image: gpui::Img,
71    style: StyleRefinement,
72}
73
74impl AvatarImage {
75    pub fn new(source: impl Into<ImageSource>) -> Self {
76        Self {
77            image: img(source),
78            style: StyleRefinement::default(),
79        }
80    }
81}
82
83impl Styled for AvatarImage {
84    fn style(&mut self) -> &mut StyleRefinement {
85        &mut self.style
86    }
87}
88
89impl InteractiveElement for AvatarImage {
90    fn interactivity(&mut self) -> &mut Interactivity {
91        self.image.interactivity()
92    }
93}
94
95impl RenderOnce for AvatarImage {
96    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
97        self.image.refine_style(&self.style)
98    }
99}
100
101/// An unstyled avatar fallback slot for initials or an application-owned icon.
102#[derive(IntoElement)]
103pub struct AvatarFallback {
104    base: Div,
105    style: StyleRefinement,
106    children: SmallVec<[AnyElement; 1]>,
107}
108
109impl AvatarFallback {
110    pub fn new() -> Self {
111        Self {
112            base: div(),
113            style: StyleRefinement::default(),
114            children: SmallVec::new(),
115        }
116    }
117}
118
119impl Default for AvatarFallback {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl Styled for AvatarFallback {
126    fn style(&mut self) -> &mut StyleRefinement {
127        &mut self.style
128    }
129}
130
131impl ParentElement for AvatarFallback {
132    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
133        self.children.extend(elements);
134    }
135}
136
137impl RenderOnce for AvatarFallback {
138    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
139        self.base.children(self.children).refine_style(&self.style)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use gpui::{Context, Render, prelude::FluentBuilder as _, px};
147
148    struct Harness {
149        image: bool,
150    }
151
152    impl Render for Harness {
153        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
154            Avatar::new()
155                .when(self.image, |this| {
156                    this.image(
157                        AvatarImage::new("avatar.png")
158                            .debug_selector(|| "avatar-image".into())
159                            .size(px(20.)),
160                    )
161                })
162                .fallback(
163                    AvatarFallback::new().child(
164                        div()
165                            .debug_selector(|| "avatar-fallback".into())
166                            .size(px(20.)),
167                    ),
168                )
169        }
170    }
171
172    #[gpui::test]
173    fn image_slot_takes_precedence_over_fallback(cx: &mut gpui::TestAppContext) {
174        let (_, cx) = cx.add_window_view(|_, _| Harness { image: true });
175        cx.update(|window, cx| window.draw(cx).clear(cx));
176        assert!(cx.debug_bounds("avatar-image").is_some());
177        assert!(cx.debug_bounds("avatar-fallback").is_none());
178    }
179
180    #[gpui::test]
181    fn fallback_renders_without_an_image(cx: &mut gpui::TestAppContext) {
182        let (_, cx) = cx.add_window_view(|_, _| Harness { image: false });
183        cx.update(|window, cx| window.draw(cx).clear(cx));
184        assert!(cx.debug_bounds("avatar-image").is_none());
185        assert!(cx.debug_bounds("avatar-fallback").is_some());
186    }
187}