Add `TitleBar` component and wire up to the workspace

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs           |   1 
crates/storybook2/src/stories/components/title_bar.rs |  26 +
crates/storybook2/src/story_selector.rs               |   2 
crates/ui2/src/components.rs                          |   2 
crates/ui2/src/components/title_bar.rs                | 119 +++++++
crates/ui2/src/components/workspace.rs                |  14 
crates/ui2/src/static_data.rs                         | 194 ++++++++++++
7 files changed, 349 insertions(+), 9 deletions(-)

Detailed changes

crates/storybook2/src/stories/components/title_bar.rs 🔗

@@ -0,0 +1,26 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::TitleBar;
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct TitleBarStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> TitleBarStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TitleBar<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TitleBar::new(cx))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -46,6 +46,7 @@ pub enum ComponentStory {
     Tab,
     TabBar,
     Terminal,
+    TitleBar,
     Toolbar,
     TrafficLights,
     Workspace,
@@ -68,6 +69,7 @@ impl ComponentStory {
             Self::Tab => components::tab::TabStory::new().into_any(),
             Self::TabBar => components::tab_bar::TabBarStory::new().into_any(),
             Self::Terminal => components::terminal::TerminalStory::new().into_any(),
+            Self::TitleBar => components::title_bar::TitleBarStory::new().into_any(),
             Self::Toolbar => components::toolbar::ToolbarStory::new().into_any(),
             Self::TrafficLights => components::traffic_lights::TrafficLightsStory::new().into_any(),
             Self::Workspace => components::workspace::WorkspaceStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -14,6 +14,7 @@ mod status_bar;
 mod tab;
 mod tab_bar;
 mod terminal;
+mod title_bar;
 mod toolbar;
 mod traffic_lights;
 mod workspace;
@@ -34,6 +35,7 @@ pub use status_bar::*;
 pub use tab::*;
 pub use tab_bar::*;
 pub use terminal::*;
+pub use title_bar::*;
 pub use toolbar::*;
 pub use traffic_lights::*;
 pub use workspace::*;

crates/ui2/src/components/title_bar.rs 🔗

@@ -0,0 +1,119 @@
+use std::marker::PhantomData;
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+
+use crate::prelude::*;
+use crate::{
+    theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, PlayerWithCallStatus,
+    ToolDivider, TrafficLights,
+};
+
+#[derive(Clone)]
+pub struct Livestream {
+    pub players: Vec<PlayerWithCallStatus>,
+    pub channel: Option<String>, // projects
+                                 // windows
+}
+
+#[derive(Element)]
+pub struct TitleBar<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+    /// If the window is active from the OS's perspective.
+    is_active: Arc<AtomicBool>,
+    livestream: Option<Livestream>,
+}
+
+impl<S: 'static + Send + Sync + Clone> TitleBar<S> {
+    pub fn new(cx: &mut ViewContext<S>) -> Self {
+        let is_active = Arc::new(AtomicBool::new(true));
+        let active = is_active.clone();
+
+        // cx.observe_window_activation(move |_, is_active, cx| {
+        //     active.store(is_active, std::sync::atomic::Ordering::SeqCst);
+        //     cx.notify();
+        // })
+        // .detach();
+
+        Self {
+            state_type: PhantomData,
+            is_active,
+            livestream: None,
+        }
+    }
+
+    pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
+        self.livestream = livestream;
+        self
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+        // let has_focus = cx.window_is_active();
+        let has_focus = true;
+
+        let player_list = if let Some(livestream) = &self.livestream {
+            livestream.players.clone().into_iter()
+        } else {
+            vec![].into_iter()
+        };
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .h_full()
+                    .gap_4()
+                    .px_2()
+                    .child(TrafficLights::new().window_has_focus(has_focus))
+                    // === Project Info === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(Button::new("zed"))
+                            .child(Button::new("nate/gpui2-ui-components")),
+                    )
+                    .children(player_list.map(|p| PlayerStack::new(p)))
+                    .child(IconButton::new(Icon::Plus)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(IconButton::new(Icon::FolderX))
+                            .child(IconButton::new(Icon::Close)),
+                    )
+                    .child(ToolDivider::new())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(IconButton::new(Icon::Mic))
+                            .child(IconButton::new(Icon::AudioOn))
+                            .child(IconButton::new(Icon::Screen).color(IconColor::Accent)),
+                    )
+                    .child(
+                        div().px_2().flex().items_center().child(
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .shape(Shape::RoundedRectangle),
+                        ),
+                    ),
+            )
+    }
+}

crates/ui2/src/components/workspace.rs 🔗

@@ -6,9 +6,9 @@ use gpui3::{relative, rems, Size};
 
 use crate::prelude::*;
 use crate::{
-    hello_world_rust_editor_with_status_example, theme, v_stack, ChatMessage, ChatPanel,
-    EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection,
-    StatusBar, Terminal,
+    hello_world_rust_editor_with_status_example, random_players_with_call_status, theme, v_stack,
+    ChatMessage, ChatPanel, EditorPane, Livestream, Pane, PaneGroup, Panel, PanelAllowedSides,
+    PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
 };
 
 #[derive(Element)]
@@ -102,10 +102,10 @@ impl<S: 'static + Send + Sync + Clone> WorkspaceElement<S> {
             .items_start()
             .text_color(theme.lowest.base.default.foreground)
             .fill(theme.lowest.base.default.background)
-            // .child(TitleBar::new(cx).set_livestream(Some(Livestream {
-            //     players: random_players_with_call_status(7),
-            //     channel: Some("gpui2-ui".to_string()),
-            // })))
+            .child(TitleBar::new(cx).set_livestream(Some(Livestream {
+                players: random_players_with_call_status(7),
+                channel: Some("gpui2-ui".to_string()),
+            })))
             .child(
                 div()
                     .flex_1()

crates/ui2/src/static_data.rs 🔗

@@ -1,10 +1,13 @@
 use std::path::PathBuf;
 use std::str::FromStr;
 
+use rand::Rng;
+
 use crate::{
     Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
-    HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListItem, Player, Symbol,
-    Tab, Theme, ToggleState,
+    HighlightedLine, HighlightedText, Icon, Label, LabelColor, ListEntry, ListItem, Livestream,
+    MicStatus, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab,
+    Theme, ToggleState, VideoStatus,
 };
 
 pub fn static_tabs_example<S: 'static + Send + Sync + Clone>() -> Vec<Tab<S>> {
@@ -130,6 +133,193 @@ pub fn static_players() -> Vec<Player> {
     ]
 }
 
+#[derive(Debug)]
+pub struct PlayerData {
+    pub url: String,
+    pub name: String,
+}
+
+pub fn static_player_data() -> Vec<PlayerData> {
+    vec![
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            name: "iamnbutler".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            name: "maxbrunsfeld".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            name: "as-cii".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
+            name: "nathansobo".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "ForLoveOfCats".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
+            name: "SomeoneToIgnore".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
+            name: "JosephTLyons".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
+            name: "osiewicz".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
+            name: "KCaverly".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "maxdeviant".into(),
+        },
+    ]
+}
+
+pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
+    let mut players = Vec::new();
+    for data in player_data {
+        players.push(Player::new(players.len(), data.url, data.name));
+    }
+    players
+}
+
+pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
+    Player::new(1, data[0].url.clone(), data[0].name.clone())
+}
+
+pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
+    Player::new(2, data[1].url.clone(), data[1].name.clone())
+}
+
+pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
+    Player::new(3, data[2].url.clone(), data[2].name.clone())
+}
+
+pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
+    Player::new(4, data[3].url.clone(), data[3].name.clone())
+}
+
+pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
+    Player::new(5, data[4].url.clone(), data[4].name.clone())
+}
+
+pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
+    Player::new(6, data[5].url.clone(), data[5].name.clone())
+}
+
+pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
+    Player::new(7, data[6].url.clone(), data[6].name.clone())
+}
+
+pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
+    Player::new(8, data[7].url.clone(), data[7].name.clone())
+}
+
+pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
+    Player::new(9, data[8].url.clone(), data[8].name.clone())
+}
+
+pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
+    Player::new(10, data[9].url.clone(), data[9].name.clone())
+}
+
+pub fn static_livestream() -> Livestream {
+    Livestream {
+        players: random_players_with_call_status(7),
+        channel: Some("gpui2-ui".to_string()),
+    }
+}
+
+pub fn populate_player_call_status(
+    player: Player,
+    followers: Option<Vec<Player>>,
+) -> PlayerCallStatus {
+    let mut rng = rand::thread_rng();
+    let in_current_project: bool = rng.gen();
+    let disconnected: bool = rng.gen();
+    let voice_activity: f32 = rng.gen();
+    let mic_status = if rng.gen_bool(0.5) {
+        MicStatus::Muted
+    } else {
+        MicStatus::Unmuted
+    };
+    let video_status = if rng.gen_bool(0.5) {
+        VideoStatus::On
+    } else {
+        VideoStatus::Off
+    };
+    let screen_share_status = if rng.gen_bool(0.5) {
+        ScreenShareStatus::Shared
+    } else {
+        ScreenShareStatus::NotShared
+    };
+    PlayerCallStatus {
+        mic_status,
+        voice_activity,
+        video_status,
+        screen_share_status,
+        in_current_project,
+        disconnected,
+        following: None,
+        followers,
+    }
+}
+
+pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
+    let players = create_static_players(static_player_data());
+    let mut player_status = vec![];
+    for i in 0..number_of_players {
+        let followers = if i == 0 {
+            Some(vec![
+                players[1].clone(),
+                players[3].clone(),
+                players[5].clone(),
+                players[6].clone(),
+            ])
+        } else if i == 1 {
+            Some(vec![players[2].clone(), players[6].clone()])
+        } else {
+            None
+        };
+        let call_status = populate_player_call_status(players[i].clone(), followers);
+        player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
+    }
+    player_status
+}
+
+pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
+    let players = static_players();
+    let mut player_0_status = PlayerCallStatus::new();
+    let player_1_status = PlayerCallStatus::new();
+    let player_2_status = PlayerCallStatus::new();
+    let mut player_3_status = PlayerCallStatus::new();
+    let mut player_4_status = PlayerCallStatus::new();
+
+    player_0_status.screen_share_status = ScreenShareStatus::Shared;
+    player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
+
+    player_3_status.voice_activity = 0.5;
+    player_4_status.mic_status = MicStatus::Muted;
+    player_4_status.in_current_project = false;
+
+    vec![
+        PlayerWithCallStatus::new(players[0].clone(), player_0_status),
+        PlayerWithCallStatus::new(players[1].clone(), player_1_status),
+        PlayerWithCallStatus::new(players[2].clone(), player_2_status),
+        PlayerWithCallStatus::new(players[3].clone(), player_3_status),
+        PlayerWithCallStatus::new(players[4].clone(), player_4_status),
+    ]
+}
+
 pub fn static_project_panel_project_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
     vec![
         ListEntry::new(Label::new("zed"))