boltz-ui 0.2.21

High-level reusable GPUI UI components (Label, Button, Input, etc.).
Documentation
use crate::component_prelude::*;
use crate::prelude::*;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;

use super::Avatar;

/// An element that displays a collection of (usually) faces stacked
/// horizontally, with the left-most face on top, visually descending
/// from left to right.
///
/// Facepiles are used to display a group of people or things,
/// such as a list of participants in a collaboration session.
///
/// # Examples
///
/// ## Default
///
/// A default, horizontal facepile.
///
/// ```
/// use gpui::IntoElement;
/// use ui::{Avatar, Facepile, EXAMPLE_FACES};
///
/// let facepile = Facepile::new(
///     EXAMPLE_FACES.iter().take(3).map(|&url|
///         Avatar::new(url).into_any_element()).collect()
/// );
/// ```
#[derive(IntoElement, Documented, RegisterComponent)]
pub struct Facepile {
    base: Div,
    faces: SmallVec<[AnyElement; 2]>,
    max_visible: Option<usize>,
}

impl Facepile {
    /// Creates a new empty facepile.
    pub fn empty() -> Self {
        Self::new(SmallVec::new())
    }

    /// Creates a new facepile with the given faces.
    pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self {
        Self {
            base: div(),
            faces,
            max_visible: None,
        }
    }

    /// Caps the number of visible faces, replacing the rest with a `+N`
    /// overflow indicator.
    pub fn max_visible(mut self, max: usize) -> Self {
        self.max_visible = Some(max);
        self
    }
}

impl ParentElement for Facepile {
    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
        self.faces.extend(elements);
    }
}

// Style methods.
impl Facepile {
    fn style(&mut self) -> &mut StyleRefinement {
        self.base.style()
    }

    gpui::padding_style_methods!({
        visibility: pub
    });
}

impl RenderOnce for Facepile {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let total = self.faces.len();
        let mut faces = self.faces;

        // If capped, trim to `max_visible` and append a `+N` overflow badge
        // (rendered on top, at the front of the stack).
        if let Some(max) = self.max_visible {
            if total > max {
                let overflow = total - max;
                faces.truncate(max);
                faces.push(
                    div()
                        .size_6()
                        .flex()
                        .items_center()
                        .justify_center()
                        .rounded_full()
                        .border_2()
                        .border_color(semantic::surface(cx))
                        .bg(semantic::elevated_surface(cx))
                        .child(
                            Label::new(format!("+{overflow}"))
                                .size(LabelSize::XSmall)
                                .color(Color::Custom(semantic::text_muted(cx))),
                        )
                        .into_any_element(),
                );
            }
        }

        // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
        self.base
            .flex()
            .flex_row_reverse()
            .items_center()
            .justify_start()
            .children(
                faces
                    .into_iter()
                    .enumerate()
                    .rev()
                    .map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
            )
    }
}

pub const EXAMPLE_FACES: [&str; 6] = [
    "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
    "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
    "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
    "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
    "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
    "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
];

impl Component for Facepile {
    fn scope() -> ComponentScope {
        ComponentScope::Collaboration
    }

    fn description() -> Option<&'static str> {
        Some(
            "Displays a collection of avatars or initials in a compact format. Often used to represent active collaborators or a subset of contributors.",
        )
    }

    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
        Some(
            v_flex()
                .gap_6()
                .children(vec![example_group_with_title(
                    "Facepile Examples",
                    vec![
                        single_example(
                            "Default",
                            Facepile::new(
                                EXAMPLE_FACES
                                    .iter()
                                    .map(|&url| Avatar::new(url).into_any_element())
                                    .collect(),
                            )
                            .into_any_element(),
                        ),
                        single_example(
                            "Custom Size",
                            Facepile::new(
                                EXAMPLE_FACES
                                    .iter()
                                    .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
                                    .collect(),
                            )
                            .into_any_element(),
                        ),
                        single_example(
                            "Overflow Count",
                            Facepile::new(
                                EXAMPLE_FACES
                                    .iter()
                                    .map(|&url| Avatar::new(url).into_any_element())
                                    .collect(),
                            )
                            .max_visible(3)
                            .into_any_element(),
                        )
                        .description("Caps visible faces and shows a `+N` overflow indicator."),
                    ],
                )])
                .into_any_element(),
        )
    }
}