ui/components/
facepile.rs1use crate::component_prelude::*;
2use crate::prelude::*;
3use gpui::{AnyElement, StyleRefinement};
4use smallvec::SmallVec;
5
6use super::Avatar;
7
8#[derive(IntoElement, Documented, RegisterComponent)]
31pub struct Facepile {
32 base: Div,
33 faces: SmallVec<[AnyElement; 2]>,
34 max_visible: Option<usize>,
35}
36
37impl Facepile {
38 pub fn empty() -> Self {
40 Self::new(SmallVec::new())
41 }
42
43 pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self {
45 Self {
46 base: div(),
47 faces,
48 max_visible: None,
49 }
50 }
51
52 pub fn max_visible(mut self, max: usize) -> Self {
55 self.max_visible = Some(max);
56 self
57 }
58}
59
60impl ParentElement for Facepile {
61 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
62 self.faces.extend(elements);
63 }
64}
65
66impl Facepile {
68 fn style(&mut self) -> &mut StyleRefinement {
69 self.base.style()
70 }
71
72 gpui::padding_style_methods!({
73 visibility: pub
74 });
75}
76
77impl RenderOnce for Facepile {
78 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
79 let total = self.faces.len();
80 let mut faces = self.faces;
81
82 if let Some(max) = self.max_visible {
85 if total > max {
86 let overflow = total - max;
87 faces.truncate(max);
88 faces.push(
89 div()
90 .size_6()
91 .flex()
92 .items_center()
93 .justify_center()
94 .rounded_full()
95 .border_2()
96 .border_color(semantic::surface(cx))
97 .bg(semantic::elevated_surface(cx))
98 .child(
99 Label::new(format!("+{overflow}"))
100 .size(LabelSize::XSmall)
101 .color(Color::Custom(semantic::text_muted(cx))),
102 )
103 .into_any_element(),
104 );
105 }
106 }
107
108 self.base
110 .flex()
111 .flex_row_reverse()
112 .items_center()
113 .justify_start()
114 .children(
115 faces
116 .into_iter()
117 .enumerate()
118 .rev()
119 .map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
120 )
121 }
122}
123
124pub const EXAMPLE_FACES: [&str; 6] = [
125 "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
126 "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
127 "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
128 "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
129 "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
130 "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
131];
132
133impl Component for Facepile {
134 fn scope() -> ComponentScope {
135 ComponentScope::Collaboration
136 }
137
138 fn description() -> Option<&'static str> {
139 Some(
140 "Displays a collection of avatars or initials in a compact format. Often used to represent active collaborators or a subset of contributors.",
141 )
142 }
143
144 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
145 Some(
146 v_flex()
147 .gap_6()
148 .children(vec![example_group_with_title(
149 "Facepile Examples",
150 vec![
151 single_example(
152 "Default",
153 Facepile::new(
154 EXAMPLE_FACES
155 .iter()
156 .map(|&url| Avatar::new(url).into_any_element())
157 .collect(),
158 )
159 .into_any_element(),
160 ),
161 single_example(
162 "Custom Size",
163 Facepile::new(
164 EXAMPLE_FACES
165 .iter()
166 .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
167 .collect(),
168 )
169 .into_any_element(),
170 ),
171 single_example(
172 "Overflow Count",
173 Facepile::new(
174 EXAMPLE_FACES
175 .iter()
176 .map(|&url| Avatar::new(url).into_any_element())
177 .collect(),
178 )
179 .max_visible(3)
180 .into_any_element(),
181 )
182 .description("Caps visible faces and shows a `+N` overflow indicator."),
183 ],
184 )])
185 .into_any_element(),
186 )
187 }
188}