Skip to repository content317 lines · 11.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:43:38.822Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
avatar.rs
1use crate::prelude::*;
2
3use documented::Documented;
4use gpui::{AnyElement, AnyView, Hsla, ImageSource, Img, IntoElement, Styled, img};
5
6/// An element that renders a user avatar with customizable appearance options.
7///
8/// # Examples
9///
10/// ```
11/// use ui::Avatar;
12///
13/// Avatar::new("path/to/image.png")
14/// .grayscale(true)
15/// .border_color(gpui::red());
16/// ```
17#[derive(IntoElement, Documented, RegisterComponent)]
18pub struct Avatar {
19 image: Img,
20 size: Option<AbsoluteLength>,
21 border_color: Option<Hsla>,
22 indicator: Option<AnyElement>,
23}
24
25impl Avatar {
26 /// Creates a new avatar element with the specified image source.
27 pub fn new(src: impl Into<ImageSource>) -> Self {
28 Avatar {
29 image: img(src),
30 size: None,
31 border_color: None,
32 indicator: None,
33 }
34 }
35
36 /// Applies a grayscale filter to the avatar image.
37 ///
38 /// # Examples
39 ///
40 /// ```
41 /// use ui::Avatar;
42 ///
43 /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
44 /// ```
45 pub fn grayscale(mut self, grayscale: bool) -> Self {
46 self.image = self.image.grayscale(grayscale);
47 self
48 }
49
50 /// Sets the border color of the avatar.
51 ///
52 /// This might be used to match the border to the background color of
53 /// the parent element to create the illusion of cropping another
54 /// shape underneath (for example in face piles.)
55 pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
56 self.border_color = Some(color.into());
57 self
58 }
59
60 /// Size overrides the avatar size. By default they are 1rem.
61 pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
62 self.size = size.into().map(Into::into);
63 self
64 }
65
66 /// Sets the current indicator to be displayed on the avatar, if any.
67 pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
68 self.indicator = indicator.into().map(IntoElement::into_any_element);
69 self
70 }
71}
72
73impl RenderOnce for Avatar {
74 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
75 let border_width = if self.border_color.is_some() {
76 px(1.)
77 } else {
78 px(0.)
79 };
80
81 let image_size = self.size.unwrap_or_else(|| rems(1.).into());
82 let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
83
84 div()
85 .size(container_size)
86 .rounded_full()
87 .when_some(self.border_color, |this, color| {
88 this.border(border_width).border_color(color)
89 })
90 .child(
91 self.image
92 .size(image_size)
93 .rounded_full()
94 .bg(cx.theme().colors().element_disabled)
95 .with_fallback(|| {
96 h_flex()
97 .size_full()
98 .justify_center()
99 .child(
100 Icon::new(IconName::Person)
101 .color(Color::Muted)
102 .size(IconSize::Small),
103 )
104 .into_any_element()
105 }),
106 )
107 .children(self.indicator.map(|indicator| div().child(indicator)))
108 }
109}
110
111/// The audio status of an player, for use in representing
112/// their status visually on their avatar.
113#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
114pub enum AudioStatus {
115 /// The player's microphone is muted.
116 Muted,
117 /// The player's microphone is muted, and collaboration audio is disabled.
118 Deafened,
119}
120
121/// An indicator that shows the audio status of a player.
122#[derive(IntoElement)]
123pub struct AvatarAudioStatusIndicator {
124 audio_status: AudioStatus,
125 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
126}
127
128impl AvatarAudioStatusIndicator {
129 /// Creates a new `AvatarAudioStatusIndicator`
130 pub fn new(audio_status: AudioStatus) -> Self {
131 Self {
132 audio_status,
133 tooltip: None,
134 }
135 }
136
137 /// Sets the tooltip for the indicator.
138 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
139 self.tooltip = Some(Box::new(tooltip));
140 self
141 }
142}
143
144impl RenderOnce for AvatarAudioStatusIndicator {
145 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
146 let icon_size = IconSize::Indicator;
147
148 let width_in_px = icon_size.rems() * window.rem_size();
149 let padding_x = px(4.);
150
151 div()
152 .absolute()
153 .bottom(rems_from_px(-3.))
154 .right(rems_from_px(-6.))
155 .w(width_in_px + padding_x)
156 .h(icon_size.rems())
157 .child(
158 h_flex()
159 .id("muted-indicator")
160 .justify_center()
161 .px(padding_x)
162 .py(px(2.))
163 .bg(cx.theme().status().error_background)
164 .rounded_sm()
165 .child(
166 Icon::new(match self.audio_status {
167 AudioStatus::Muted => IconName::MicMute,
168 AudioStatus::Deafened => IconName::AudioOff,
169 })
170 .size(icon_size)
171 .color(Color::Error),
172 )
173 .when_some(self.tooltip, |this, tooltip| {
174 this.tooltip(move |window, cx| tooltip(window, cx))
175 }),
176 )
177 }
178}
179
180/// Represents the availability status of a collaborator.
181#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
182pub enum CollaboratorAvailability {
183 Free,
184 Busy,
185}
186
187/// Represents the availability and presence status of a collaborator.
188#[derive(IntoElement)]
189pub struct AvatarAvailabilityIndicator {
190 availability: CollaboratorAvailability,
191 avatar_size: Option<Pixels>,
192 border_color: Option<Hsla>,
193}
194
195impl AvatarAvailabilityIndicator {
196 /// Creates a new indicator
197 pub fn new(availability: CollaboratorAvailability) -> Self {
198 Self {
199 availability,
200 avatar_size: None,
201 border_color: None,
202 }
203 }
204
205 /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on.
206 pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
207 self.avatar_size = size.into();
208 self
209 }
210
211 pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
212 self.border_color = Some(color.into());
213 self
214 }
215}
216
217impl RenderOnce for AvatarAvailabilityIndicator {
218 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
219 let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size());
220
221 // HACK: non-integer sizes result in oval indicators.
222 let indicator_size = (avatar_size * 0.4).round();
223
224 let border_width = if self.border_color.is_some() {
225 px(2.)
226 } else {
227 px(0.)
228 };
229 let container_size = indicator_size + border_width * 2.;
230
231 div()
232 .absolute()
233 .bottom(-border_width)
234 .right(-border_width)
235 .size(container_size)
236 .rounded(container_size)
237 .when_some(self.border_color, |this, color| {
238 this.border(border_width).border_color(color)
239 })
240 .bg(match self.availability {
241 CollaboratorAvailability::Free => cx.theme().status().created,
242 CollaboratorAvailability::Busy => cx.theme().status().deleted,
243 })
244 }
245}
246
247// View this component preview using `workspace: open component-preview`
248impl Component for Avatar {
249 fn scope() -> ComponentScope {
250 ComponentScope::Collaboration
251 }
252
253 fn description() -> &'static str {
254 Avatar::DOCS
255 }
256
257 fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
258 let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
259
260 v_flex()
261 .gap_6()
262 .children(vec![
263 example_group(vec![
264 single_example("Default", Avatar::new(example_avatar).into_any_element()),
265 single_example(
266 "Grayscale",
267 Avatar::new(example_avatar)
268 .grayscale(true)
269 .into_any_element(),
270 ),
271 single_example(
272 "Border",
273 Avatar::new(example_avatar)
274 .border_color(cx.theme().colors().border)
275 .into_any_element(),
276 ).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."),
277 ]),
278 example_group_with_title(
279 "Indicator Styles",
280 vec![
281 single_example(
282 "Muted",
283 Avatar::new(example_avatar)
284 .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
285 .into_any_element(),
286 ).description("Indicates the collaborator's mic is muted."),
287 single_example(
288 "Deafened",
289 Avatar::new(example_avatar)
290 .indicator(AvatarAudioStatusIndicator::new(
291 AudioStatus::Deafened,
292 ))
293 .into_any_element(),
294 ).description("Indicates that both the collaborator's mic and audio are muted."),
295 single_example(
296 "Availability: Free",
297 Avatar::new(example_avatar)
298 .indicator(AvatarAvailabilityIndicator::new(
299 CollaboratorAvailability::Free,
300 ))
301 .into_any_element(),
302 ).description("Indicates that the person is free, usually meaning they are not in a call."),
303 single_example(
304 "Availability: Busy",
305 Avatar::new(example_avatar)
306 .indicator(AvatarAvailabilityIndicator::new(
307 CollaboratorAvailability::Busy,
308 ))
309 .into_any_element(),
310 ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."),
311 ],
312 ),
313 ])
314 .into_any_element()
315 }
316}
317