From f33d41af636a4db4b51b0d2a64b68ffcad69190c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 7 Oct 2023 12:02:42 -0400 Subject: [PATCH] Add `Facepile` and `PlayerStack` components --- crates/storybook2/src/stories/components.rs | 1 + .../src/stories/components/facepile.rs | 35 +++++ crates/storybook2/src/story_selector.rs | 2 + crates/ui2/src/components.rs | 4 + crates/ui2/src/components/facepile.rs | 33 +++++ crates/ui2/src/components/player_stack.rs | 72 ++++++++++ crates/ui2/src/elements.rs | 2 + crates/ui2/src/elements/player.rs | 133 ++++++++++++++++++ crates/ui2/src/static_data.rs | 34 ++++- 9 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 crates/storybook2/src/stories/components/facepile.rs create mode 100644 crates/ui2/src/components/facepile.rs create mode 100644 crates/ui2/src/components/player_stack.rs create mode 100644 crates/ui2/src/elements/player.rs diff --git a/crates/storybook2/src/stories/components.rs b/crates/storybook2/src/stories/components.rs index f097ea4727..78f1982eff 100644 --- a/crates/storybook2/src/stories/components.rs +++ b/crates/storybook2/src/stories/components.rs @@ -2,6 +2,7 @@ pub mod assistant_panel; pub mod breadcrumb; pub mod buffer; pub mod chat_panel; +pub mod facepile; pub mod panel; pub mod project_panel; pub mod tab; diff --git a/crates/storybook2/src/stories/components/facepile.rs b/crates/storybook2/src/stories/components/facepile.rs new file mode 100644 index 0000000000..54599e5e89 --- /dev/null +++ b/crates/storybook2/src/stories/components/facepile.rs @@ -0,0 +1,35 @@ +use std::marker::PhantomData; + +use ui::prelude::*; +use ui::{static_players, Facepile}; + +use crate::story::Story; + +#[derive(Element)] +pub struct FacepileStory { + state_type: PhantomData, +} + +impl FacepileStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let players = static_players(); + + Story::container(cx) + .child(Story::title_for::<_, Facepile>(cx)) + .child(Story::label(cx, "Default")) + .child( + div() + .flex() + .gap_3() + .child(Facepile::new(players.clone().into_iter().take(1))) + .child(Facepile::new(players.clone().into_iter().take(2))) + .child(Facepile::new(players.clone().into_iter().take(3))), + ) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 761d4d78d6..0ae51bf47f 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -40,6 +40,7 @@ pub enum ComponentStory { Breadcrumb, Buffer, ChatPanel, + Facepile, Panel, ProjectPanel, Tab, @@ -61,6 +62,7 @@ impl ComponentStory { Self::Buffer => components::buffer::BufferStory::new().into_any(), Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::new().into_any(), Self::ChatPanel => components::chat_panel::ChatPanelStory::new().into_any(), + Self::Facepile => components::facepile::FacepileStory::new().into_any(), Self::Panel => components::panel::PanelStory::new().into_any(), Self::ProjectPanel => components::project_panel::ProjectPanelStory::new().into_any(), Self::Tab => components::tab::TabStory::new().into_any(), diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 2a58ec5f1e..f0fa249b75 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -3,10 +3,12 @@ mod breadcrumb; mod buffer; mod chat_panel; mod editor_pane; +mod facepile; mod icon_button; mod list; mod panel; mod panes; +mod player_stack; mod project_panel; mod status_bar; mod tab; @@ -21,10 +23,12 @@ pub use breadcrumb::*; pub use buffer::*; pub use chat_panel::*; pub use editor_pane::*; +pub use facepile::*; pub use icon_button::*; pub use list::*; pub use panel::*; pub use panes::*; +pub use player_stack::*; pub use project_panel::*; pub use status_bar::*; pub use tab::*; diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs new file mode 100644 index 0000000000..a19d8d432d --- /dev/null +++ b/crates/ui2/src/components/facepile.rs @@ -0,0 +1,33 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{theme, Avatar, Player}; + +#[derive(Element)] +pub struct Facepile { + state_type: PhantomData, + players: Vec, +} + +impl Facepile { + pub fn new>(players: P) -> Self { + Self { + state_type: PhantomData, + players: players.collect(), + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let theme = theme(cx); + let player_count = self.players.len(); + let player_list = self.players.iter().enumerate().map(|(ix, player)| { + let isnt_last = ix < player_count - 1; + + div() + // TODO: Blocked on negative margins. + // .when(isnt_last, |div| div.neg_mr_1()) + .child(Avatar::new(player.avatar_src().to_string())) + }); + div().p_1().flex().items_center().children(player_list) + } +} diff --git a/crates/ui2/src/components/player_stack.rs b/crates/ui2/src/components/player_stack.rs new file mode 100644 index 0000000000..415a2b911d --- /dev/null +++ b/crates/ui2/src/components/player_stack.rs @@ -0,0 +1,72 @@ +use std::marker::PhantomData; + +use crate::prelude::*; +use crate::{Avatar, Facepile, PlayerWithCallStatus}; + +#[derive(Element)] +pub struct PlayerStack { + state_type: PhantomData, + player_with_call_status: PlayerWithCallStatus, +} + +impl PlayerStack { + pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self { + Self { + state_type: PhantomData, + player_with_call_status, + } + } + + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let system_color = SystemColor::new(); + let player = self.player_with_call_status.get_player(); + self.player_with_call_status.get_call_status(); + + let followers = self + .player_with_call_status + .get_call_status() + .followers + .as_ref() + .map(|followers| followers.clone()); + + // if we have no followers return a slightly different element + // if mic_status == muted add a red ring to avatar + + div() + .h_full() + .flex() + .flex_col() + .gap_px() + .justify_center() + .child( + div().flex().justify_center().w_full().child( + div() + .w_4() + .h_0p5() + .rounded_sm() + .fill(player.cursor_color(cx)), + ), + ) + .child( + div() + .flex() + .items_center() + .justify_center() + .h_6() + .pl_1() + .rounded_lg() + .fill(if followers.is_none() { + system_color.transparent + } else { + player.selection_color(cx) + }) + .child(Avatar::new(player.avatar_src().to_string())) + .children(followers.map(|followers| { + div() + // TODO: Blocked on negative margins. + // .neg_ml_2() + .child(Facepile::new(followers.into_iter())) + })), + ) + } +} diff --git a/crates/ui2/src/elements.rs b/crates/ui2/src/elements.rs index 6f9d54e48a..81a306d3e4 100644 --- a/crates/ui2/src/elements.rs +++ b/crates/ui2/src/elements.rs @@ -3,6 +3,7 @@ mod button; mod icon; mod input; mod label; +mod player; mod stack; mod tool_divider; @@ -11,5 +12,6 @@ pub use button::*; pub use icon::*; pub use input::*; pub use label::*; +pub use player::*; pub use stack::*; pub use tool_divider::*; diff --git a/crates/ui2/src/elements/player.rs b/crates/ui2/src/elements/player.rs new file mode 100644 index 0000000000..605db8f43e --- /dev/null +++ b/crates/ui2/src/elements/player.rs @@ -0,0 +1,133 @@ +use gpui3::{Hsla, ViewContext}; + +use crate::theme; + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum PlayerStatus { + #[default] + Offline, + Online, + InCall, + Away, + DoNotDisturb, + Invisible, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum MicStatus { + Muted, + #[default] + Unmuted, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum VideoStatus { + On, + #[default] + Off, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum ScreenShareStatus { + Shared, + #[default] + NotShared, +} + +#[derive(Clone)] +pub struct PlayerCallStatus { + pub mic_status: MicStatus, + /// Indicates if the player is currently speaking + /// And the intensity of the volume coming through + /// + /// 0.0 - 1.0 + pub voice_activity: f32, + pub video_status: VideoStatus, + pub screen_share_status: ScreenShareStatus, + pub in_current_project: bool, + pub disconnected: bool, + pub following: Option>, + pub followers: Option>, +} + +impl PlayerCallStatus { + pub fn new() -> Self { + Self { + mic_status: MicStatus::default(), + voice_activity: 0., + video_status: VideoStatus::default(), + screen_share_status: ScreenShareStatus::default(), + in_current_project: true, + disconnected: false, + following: None, + followers: None, + } + } +} + +#[derive(PartialEq, Clone)] +pub struct Player { + index: usize, + avatar_src: String, + username: String, + status: PlayerStatus, +} + +#[derive(Clone)] +pub struct PlayerWithCallStatus { + player: Player, + call_status: PlayerCallStatus, +} + +impl PlayerWithCallStatus { + pub fn new(player: Player, call_status: PlayerCallStatus) -> Self { + Self { + player, + call_status, + } + } + + pub fn get_player(&self) -> &Player { + &self.player + } + + pub fn get_call_status(&self) -> &PlayerCallStatus { + &self.call_status + } +} + +impl Player { + pub fn new(index: usize, avatar_src: String, username: String) -> Self { + Self { + index, + avatar_src, + username, + status: Default::default(), + } + } + + pub fn set_status(mut self, status: PlayerStatus) -> Self { + self.status = status; + self + } + + pub fn cursor_color(&self, cx: &mut ViewContext) -> Hsla { + let theme = theme(cx); + let index = self.index % 8; + theme.players[self.index].cursor + } + + pub fn selection_color(&self, cx: &mut ViewContext) -> Hsla { + let theme = theme(cx); + let index = self.index % 8; + theme.players[self.index].selection + } + + pub fn avatar_src(&self) -> &str { + &self.avatar_src + } + + pub fn index(&self) -> usize { + self.index + } +} diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 0ce67b8a76..8b83434d87 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use crate::{ Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor, - HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListItem, Symbol, Tab, - Theme, ToggleState, + HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListItem, Player, Symbol, + Tab, Theme, ToggleState, }; pub fn static_tabs_example() -> Vec> { @@ -100,6 +100,36 @@ pub fn static_tabs_3() -> Vec> { vec![Tab::new().git_status(GitStatus::Created).current(true)] } +pub fn static_players() -> Vec { + vec![ + Player::new( + 0, + "https://avatars.githubusercontent.com/u/1714999?v=4".into(), + "nathansobo".into(), + ), + Player::new( + 1, + "https://avatars.githubusercontent.com/u/326587?v=4".into(), + "maxbrunsfeld".into(), + ), + Player::new( + 2, + "https://avatars.githubusercontent.com/u/482957?v=4".into(), + "as-cii".into(), + ), + Player::new( + 3, + "https://avatars.githubusercontent.com/u/1714999?v=4".into(), + "iamnbutler".into(), + ), + Player::new( + 4, + "https://avatars.githubusercontent.com/u/1486634?v=4".into(), + "maxdeviant".into(), + ), + ] +} + pub fn static_project_panel_project_items() -> Vec> { vec![ ListEntry::new(Label::new("zed"))