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#[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#[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#[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}