Skip to main content

ui/components/
facepile.rs

1use crate::component_prelude::*;
2use crate::prelude::*;
3use gpui::{AnyElement, StyleRefinement};
4use smallvec::SmallVec;
5
6use super::Avatar;
7
8/// An element that displays a collection of (usually) faces stacked
9/// horizontally, with the left-most face on top, visually descending
10/// from left to right.
11///
12/// Facepiles are used to display a group of people or things,
13/// such as a list of participants in a collaboration session.
14///
15/// # Examples
16///
17/// ## Default
18///
19/// A default, horizontal facepile.
20///
21/// ```
22/// use gpui::IntoElement;
23/// use ui::{Avatar, Facepile, EXAMPLE_FACES};
24///
25/// let facepile = Facepile::new(
26///     EXAMPLE_FACES.iter().take(3).map(|&url|
27///         Avatar::new(url).into_any_element()).collect()
28/// );
29/// ```
30#[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    /// Creates a new empty facepile.
39    pub fn empty() -> Self {
40        Self::new(SmallVec::new())
41    }
42
43    /// Creates a new facepile with the given faces.
44    pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self {
45        Self {
46            base: div(),
47            faces,
48            max_visible: None,
49        }
50    }
51
52    /// Caps the number of visible faces, replacing the rest with a `+N`
53    /// overflow indicator.
54    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
66// Style methods.
67impl 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 capped, trim to `max_visible` and append a `+N` overflow badge
83        // (rendered on top, at the front of the stack).
84        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        // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
109        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}