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;
Marshall Bowers created
crates/storybook2/src/stories/components.rs | 1
crates/storybook2/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(-)
@@ -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;
@@ -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<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync> FacepileStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ let players = static_players();
+
+ Story::container(cx)
+ .child(Story::title_for::<_, Facepile<S>>(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))),
+ )
+ }
+}
@@ -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(),
@@ -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::*;
@@ -0,0 +1,33 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{theme, Avatar, Player};
+
+#[derive(Element)]
+pub struct Facepile<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+ players: Vec<Player>,
+}
+
+impl<S: 'static + Send + Sync> Facepile<S> {
+ pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
+ Self {
+ state_type: PhantomData,
+ players: players.collect(),
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ 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)
+ }
+}
@@ -0,0 +1,72 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{Avatar, Facepile, PlayerWithCallStatus};
+
+#[derive(Element)]
+pub struct PlayerStack<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+ player_with_call_status: PlayerWithCallStatus,
+}
+
+impl<S: 'static + Send + Sync> PlayerStack<S> {
+ pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
+ Self {
+ state_type: PhantomData,
+ player_with_call_status,
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+ 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()))
+ })),
+ )
+ }
+}
@@ -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::*;
@@ -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<Vec<Player>>,
+ pub followers: Option<Vec<Player>>,
+}
+
+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<S: 'static>(&self, cx: &mut ViewContext<S>) -> Hsla {
+ let theme = theme(cx);
+ let index = self.index % 8;
+ theme.players[self.index].cursor
+ }
+
+ pub fn selection_color<S: 'static>(&self, cx: &mut ViewContext<S>) -> 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
+ }
+}
@@ -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<S: 'static + Send + Sync + Clone>() -> Vec<Tab<S>> {
@@ -100,6 +100,36 @@ pub fn static_tabs_3<S: 'static + Send + Sync + Clone>() -> Vec<Tab<S>> {
vec![Tab::new().git_status(GitStatus::Created).current(true)]
}
+pub fn static_players() -> Vec<Player> {
+ 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<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
vec![
ListEntry::new(Label::new("zed"))