Update default theme player colors and add players story (#3263)

Nate Butler created

[[PR Description]]

- Update the default theme player colors for `Zed Pro Moonlight` and
`Zed Pro Daylight`
- Adds the ability to create stories in the `theme2` crate


![image](https://github.com/zed-industries/zed/assets/1714999/61fca222-1512-43b1-a229-fae1080d07fa)

You can see them by running:
- `cargo run -p storybook2 -- components/players --theme "Zed Pro
Daylight"`
- `cargo run -p storybook2 -- components/players --theme`

The player colors crisscross back and forth on the color wheel so that
the colors are as distinct as possible.


![image](https://github.com/zed-industries/zed/assets/1714999/255c9fd2-34da-4f75-9aad-0dd02f7009bf)

We do have room to add additional players if needed. Just let me know if
we feel like the default 8 aren't cutting it.


Release Notes:

- N/A

Change summary

Cargo.lock                              |   1 
crates/storybook2/src/story_selector.rs |   2 
crates/theme2/Cargo.toml                |   3 
crates/theme2/src/colors.rs             |  29 ----
crates/theme2/src/default_colors.rs     | 159 +++++++++++++++++-------
crates/theme2/src/default_theme.rs      |   6 
crates/theme2/src/players.rs            | 170 ++++++++++++++++++++++++++
crates/theme2/src/story.rs              |  38 ++++++
crates/theme2/src/theme2.rs             |   7 +
9 files changed, 338 insertions(+), 77 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9005,6 +9005,7 @@ dependencies = [
  "fs2",
  "gpui2",
  "indexmap 1.9.3",
+ "itertools 0.11.0",
  "parking_lot 0.11.2",
  "refineable",
  "schemars",

crates/storybook2/src/story_selector.rs 🔗

@@ -38,6 +38,7 @@ pub enum ComponentStory {
     Palette,
     Panel,
     ProjectPanel,
+    Players,
     RecentProjects,
     Scroll,
     Tab,
@@ -79,6 +80,7 @@ impl ComponentStory {
             Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(),
             Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(),
             Self::Palette => cx.build_view(|cx| ui::PaletteStory).into(),
+            Self::Players => cx.build_view(|_| theme2::PlayerStory).into(),
             Self::Panel => cx.build_view(|cx| ui::PanelStory).into(),
             Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(),
             Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(),

crates/theme2/Cargo.toml 🔗

@@ -5,6 +5,8 @@ edition = "2021"
 publish = false
 
 [features]
+default = ["stories"]
+stories = ["dep:itertools"]
 test-support = [
     "gpui/test-support",
     "fs/test-support",
@@ -30,6 +32,7 @@ settings = { package = "settings2", path = "../settings2" }
 toml.workspace = true
 uuid.workspace = true
 util = { path = "../util" }
+itertools = { version = "0.11.0", optional = true }
 
 [dev-dependencies]
 gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

crates/theme2/src/colors.rs 🔗

@@ -3,7 +3,7 @@ use std::sync::Arc;
 use gpui::Hsla;
 use refineable::Refineable;
 
-use crate::SyntaxTheme;
+use crate::{PlayerColors, SyntaxTheme};
 
 #[derive(Clone)]
 pub struct SystemColors {
@@ -13,33 +13,6 @@ pub struct SystemColors {
     pub mac_os_traffic_light_green: Hsla,
 }
 
-#[derive(Debug, Clone, Copy)]
-pub struct PlayerColor {
-    pub cursor: Hsla,
-    pub background: Hsla,
-    pub selection: Hsla,
-}
-
-#[derive(Clone)]
-pub struct PlayerColors(pub Vec<PlayerColor>);
-
-impl PlayerColors {
-    pub fn local(&self) -> PlayerColor {
-        // todo!("use a valid color");
-        *self.0.first().unwrap()
-    }
-
-    pub fn absent(&self) -> PlayerColor {
-        // todo!("use a valid color");
-        *self.0.last().unwrap()
-    }
-
-    pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor {
-        let len = self.0.len() - 1;
-        self.0[(participant_index as usize % len) + 1]
-    }
-}
-
 #[derive(Refineable, Clone, Debug)]
 #[refineable(debug)]
 pub struct StatusColors {

crates/theme2/src/default_colors.rs 🔗

@@ -3,12 +3,106 @@ use std::num::ParseIntError;
 use gpui::{hsla, Hsla, Rgba};
 
 use crate::{
-    colors::{GitStatusColors, PlayerColor, PlayerColors, StatusColors, SystemColors, ThemeColors},
+    colors::{GitStatusColors, StatusColors, SystemColors, ThemeColors},
     scale::{ColorScaleSet, ColorScales},
     syntax::SyntaxTheme,
-    ColorScale,
+    ColorScale, PlayerColor, PlayerColors,
 };
 
+impl Default for PlayerColors {
+    fn default() -> Self {
+        Self(vec![
+            PlayerColor {
+                cursor: blue().dark().step_9(),
+                background: blue().dark().step_5(),
+                selection: blue().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: orange().dark().step_9(),
+                background: orange().dark().step_5(),
+                selection: orange().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: pink().dark().step_9(),
+                background: pink().dark().step_5(),
+                selection: pink().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: lime().dark().step_9(),
+                background: lime().dark().step_5(),
+                selection: lime().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: purple().dark().step_9(),
+                background: purple().dark().step_5(),
+                selection: purple().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: amber().dark().step_9(),
+                background: amber().dark().step_5(),
+                selection: amber().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: jade().dark().step_9(),
+                background: jade().dark().step_5(),
+                selection: jade().dark().step_3(),
+            },
+            PlayerColor {
+                cursor: red().dark().step_9(),
+                background: red().dark().step_5(),
+                selection: red().dark().step_3(),
+            },
+        ])
+    }
+}
+
+impl PlayerColors {
+    pub fn default_light() -> Self {
+        Self(vec![
+            PlayerColor {
+                cursor: blue().light().step_9(),
+                background: blue().light().step_4(),
+                selection: blue().light().step_3(),
+            },
+            PlayerColor {
+                cursor: orange().light().step_9(),
+                background: orange().light().step_4(),
+                selection: orange().light().step_3(),
+            },
+            PlayerColor {
+                cursor: pink().light().step_9(),
+                background: pink().light().step_4(),
+                selection: pink().light().step_3(),
+            },
+            PlayerColor {
+                cursor: lime().light().step_9(),
+                background: lime().light().step_4(),
+                selection: lime().light().step_3(),
+            },
+            PlayerColor {
+                cursor: purple().light().step_9(),
+                background: purple().light().step_4(),
+                selection: purple().light().step_3(),
+            },
+            PlayerColor {
+                cursor: amber().light().step_9(),
+                background: amber().light().step_4(),
+                selection: amber().light().step_3(),
+            },
+            PlayerColor {
+                cursor: jade().light().step_9(),
+                background: jade().light().step_4(),
+                selection: jade().light().step_3(),
+            },
+            PlayerColor {
+                cursor: red().light().step_9(),
+                background: red().light().step_4(),
+                selection: red().light().step_3(),
+            },
+        ])
+    }
+}
+
 fn neutral() -> ColorScaleSet {
     slate()
 }
@@ -27,17 +121,17 @@ impl Default for SystemColors {
 impl Default for StatusColors {
     fn default() -> Self {
         Self {
-            conflict: red().dark().step_11(),
-            created: grass().dark().step_11(),
-            deleted: red().dark().step_11(),
-            error: red().dark().step_11(),
-            hidden: neutral().dark().step_11(),
-            ignored: neutral().dark().step_11(),
-            info: blue().dark().step_11(),
-            modified: yellow().dark().step_11(),
-            renamed: blue().dark().step_11(),
-            success: grass().dark().step_11(),
-            warning: yellow().dark().step_11(),
+            conflict: red().dark().step_9(),
+            created: grass().dark().step_9(),
+            deleted: red().dark().step_9(),
+            error: red().dark().step_9(),
+            hidden: neutral().dark().step_9(),
+            ignored: neutral().dark().step_9(),
+            info: blue().dark().step_9(),
+            modified: yellow().dark().step_9(),
+            renamed: blue().dark().step_9(),
+            success: grass().dark().step_9(),
+            warning: yellow().dark().step_9(),
         }
     }
 }
@@ -45,43 +139,16 @@ impl Default for StatusColors {
 impl Default for GitStatusColors {
     fn default() -> Self {
         Self {
-            conflict: orange().dark().step_11(),
-            created: grass().dark().step_11(),
-            deleted: red().dark().step_11(),
-            ignored: neutral().dark().step_11(),
-            modified: yellow().dark().step_11(),
-            renamed: blue().dark().step_11(),
+            conflict: orange().dark().step_9(),
+            created: grass().dark().step_9(),
+            deleted: red().dark().step_9(),
+            ignored: neutral().dark().step_9(),
+            modified: yellow().dark().step_9(),
+            renamed: blue().dark().step_9(),
         }
     }
 }
 
-impl Default for PlayerColors {
-    fn default() -> Self {
-        Self(vec![
-            PlayerColor {
-                cursor: hsla(0.0, 0.0, 0.0, 1.0),
-                background: hsla(0.0, 0.0, 0.0, 1.0),
-                selection: hsla(0.0, 0.0, 0.0, 1.0),
-            },
-            PlayerColor {
-                cursor: hsla(0.0, 0.0, 0.0, 1.0),
-                background: hsla(0.0, 0.0, 0.0, 1.0),
-                selection: hsla(0.0, 0.0, 0.0, 1.0),
-            },
-            PlayerColor {
-                cursor: hsla(0.0, 0.0, 0.0, 1.0),
-                background: hsla(0.0, 0.0, 0.0, 1.0),
-                selection: hsla(0.0, 0.0, 0.0, 1.0),
-            },
-            PlayerColor {
-                cursor: hsla(0.0, 0.0, 0.0, 1.0),
-                background: hsla(0.0, 0.0, 0.0, 1.0),
-                selection: hsla(0.0, 0.0, 0.0, 1.0),
-            },
-        ])
-    }
-}
-
 impl SyntaxTheme {
     pub fn default_light() -> Self {
         Self {

crates/theme2/src/default_theme.rs 🔗

@@ -1,8 +1,8 @@
 use std::sync::Arc;
 
 use crate::{
-    colors::{GitStatusColors, PlayerColors, StatusColors, SystemColors, ThemeColors, ThemeStyles},
-    default_color_scales, Appearance, SyntaxTheme, Theme, ThemeFamily,
+    colors::{GitStatusColors, StatusColors, SystemColors, ThemeColors, ThemeStyles},
+    default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily,
 };
 
 fn zed_pro_daylight() -> Theme {
@@ -15,7 +15,7 @@ fn zed_pro_daylight() -> Theme {
             colors: ThemeColors::default_light(),
             status: StatusColors::default(),
             git: GitStatusColors::default(),
-            player: PlayerColors::default(),
+            player: PlayerColors::default_light(),
             syntax: Arc::new(SyntaxTheme::default_light()),
         },
     }

crates/theme2/src/players.rs 🔗

@@ -0,0 +1,170 @@
+use gpui::Hsla;
+
+#[derive(Debug, Clone, Copy)]
+pub struct PlayerColor {
+    pub cursor: Hsla,
+    pub background: Hsla,
+    pub selection: Hsla,
+}
+
+/// A collection of colors that are used to color players in the editor.
+///
+/// The first color is always the local player's color, usually a blue.
+///
+/// The rest of the default colors crisscross back and forth on the
+/// color wheel so that the colors are as distinct as possible.
+#[derive(Clone)]
+pub struct PlayerColors(pub Vec<PlayerColor>);
+
+impl PlayerColors {
+    pub fn local(&self) -> PlayerColor {
+        // todo!("use a valid color");
+        *self.0.first().unwrap()
+    }
+
+    pub fn absent(&self) -> PlayerColor {
+        // todo!("use a valid color");
+        *self.0.last().unwrap()
+    }
+
+    pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor {
+        let len = self.0.len() - 1;
+        self.0[(participant_index as usize % len) + 1]
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use super::*;
+    use crate::{ActiveTheme, Story};
+    use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext};
+
+    pub struct PlayerStory;
+
+    impl Render for PlayerStory {
+        type Element = Div<Self>;
+
+        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+            Story::container(cx).child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .gap_4()
+                    .child(Story::title_for::<_, PlayerColors>(cx))
+                    .child(Story::label(cx, "Player Colors"))
+                    .child(
+                        div()
+                            .flex()
+                            .flex_col()
+                            .gap_1()
+                            .child(
+                                div().flex().gap_1().children(
+                                    cx.theme().players().0.clone().iter_mut().map(|player| {
+                                        div().w_8().h_8().rounded_md().bg(player.cursor)
+                                    }),
+                                ),
+                            )
+                            .child(div().flex().gap_1().children(
+                                cx.theme().players().0.clone().iter_mut().map(|player| {
+                                    div().w_8().h_8().rounded_md().bg(player.background)
+                                }),
+                            ))
+                            .child(div().flex().gap_1().children(
+                                cx.theme().players().0.clone().iter_mut().map(|player| {
+                                    div().w_8().h_8().rounded_md().bg(player.selection)
+                                }),
+                            )),
+                    )
+                    .child(Story::label(cx, "Avatar Rings"))
+                    .child(div().flex().gap_1().children(
+                        cx.theme().players().0.clone().iter_mut().map(|player| {
+                            div()
+                                .my_1()
+                                .rounded_full()
+                                .border_2()
+                                .border_color(player.cursor)
+                                .child(
+                                    img()
+                                        .rounded_full()
+                                        .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                        .size_6()
+                                        .bg(gpui::red()),
+                                )
+                        }),
+                    ))
+                    .child(Story::label(cx, "Player Backgrounds"))
+                    .child(div().flex().gap_1().children(
+                        cx.theme().players().0.clone().iter_mut().map(|player| {
+                            div()
+                                .my_1()
+                                .rounded_xl()
+                                .flex()
+                                .items_center()
+                                .h_8()
+                                .py_0p5()
+                                .px_1p5()
+                                .bg(player.background)
+                                .child(
+                                div().relative().neg_mx_1().rounded_full().z_index(3)
+                                    .border_2()
+                                    .border_color(player.background)
+                                    .size(px(28.))
+                                    .child(
+                                    img()
+                                        .rounded_full()
+                                        .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                        .size(px(24.))
+                                        .bg(gpui::red()),
+                                ),
+                            ).child(
+                            div().relative().neg_mx_1().rounded_full().z_index(2)
+                                .border_2()
+                                .border_color(player.background)
+                                .size(px(28.))
+                                .child(
+                                img()
+                                    .rounded_full()
+                                    .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                    .size(px(24.))
+                                    .bg(gpui::red()),
+                            ),
+                        ).child(
+                        div().relative().neg_mx_1().rounded_full().z_index(1)
+                            .border_2()
+                            .border_color(player.background)
+                            .size(px(28.))
+                            .child(
+                            img()
+                                .rounded_full()
+                                .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .size(px(24.))
+                                .bg(gpui::red()),
+                        ),
+                    )
+                        }),
+                    ))
+                    .child(Story::label(cx, "Player Selections"))
+                    .child(div().flex().flex_col().gap_px().children(
+                        cx.theme().players().0.clone().iter_mut().map(|player| {
+                            div()
+                                .flex()
+                                .child(
+                                    div()
+                                        .flex()
+                                        .flex_none()
+                                        .rounded_sm()
+                                        .px_0p5()
+                                        .text_color(cx.theme().colors().text)
+                                        .bg(player.selection)
+                                        .child("The brown fox jumped over the lazy dog."),
+                                )
+                                .child(div().flex_1())
+                        }),
+                    )),
+            )
+        }
+    }
+}

crates/theme2/src/story.rs 🔗

@@ -0,0 +1,38 @@
+use gpui::{div, Component, Div, ParentElement, Styled, ViewContext};
+
+use crate::ActiveTheme;
+
+pub struct Story {}
+
+impl Story {
+    pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .pt_2()
+            .px_4()
+            .font("Zed Mono")
+            .bg(cx.theme().colors().background)
+    }
+
+    pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
+        div()
+            .text_xl()
+            .text_color(cx.theme().colors().text)
+            .child(title.to_owned())
+    }
+
+    pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
+        Self::title(cx, std::any::type_name::<T>())
+    }
+
+    pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
+        div()
+            .mt_4()
+            .mb_2()
+            .text_xs()
+            .text_color(cx.theme().colors().text)
+            .child(label.to_owned())
+    }
+}

crates/theme2/src/theme2.rs 🔗

@@ -1,6 +1,7 @@
 mod colors;
 mod default_colors;
 mod default_theme;
+mod players;
 mod registry;
 mod scale;
 mod settings;
@@ -14,6 +15,7 @@ use ::settings::Settings;
 pub use colors::*;
 pub use default_colors::*;
 pub use default_theme::*;
+pub use players::*;
 pub use registry::*;
 pub use scale::*;
 pub use settings::*;
@@ -120,3 +122,8 @@ pub struct DiagnosticStyle {
     pub hint: Hsla,
     pub ignored: Hsla,
 }
+
+#[cfg(feature = "stories")]
+mod story;
+#[cfg(feature = "stories")]
+pub use story::*;