Update UI elements and implement user settings for customization

Nate Butler created

Change summary

crates/gpui3/src/styled.rs                  |  14 ++
crates/ui2/src/components/list.rs           |  12 -
crates/ui2/src/components/panel.rs          |   8 
crates/ui2/src/components/title_bar.rs      |  10 +
crates/ui2/src/components/traffic_lights.rs |   3 
crates/ui2/src/elements/button.rs           |   9 +
crates/ui2/src/lib.rs                       |   1 
crates/ui2/src/prelude.rs                   |  36 +++++--
crates/ui2/src/settings.rs                  | 102 +++++++++++++++++++++++
9 files changed, 163 insertions(+), 32 deletions(-)

Detailed changes

crates/gpui3/src/styled.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    self as gpui3, hsla, point, px, relative, rems, AlignItems, Display, Fill, FlexDirection, Hsla,
-    JustifyContent, Length, Position, SharedString, StyleRefinement,
+    self as gpui3, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, Display, Fill,
+    FlexDirection, Hsla, JustifyContent, Length, Position, Rems, SharedString, StyleRefinement,
 };
 use crate::{BoxShadow, TextStyleRefinement};
 use smallvec::smallvec;
@@ -350,6 +350,16 @@ pub trait Styled {
         self
     }
 
+    fn text_size(mut self, size: Rems) -> Self
+    where
+        Self: Sized,
+    {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_size = Some(size);
+        self
+    }
+
     fn text_xs(mut self) -> Self
     where
         Self: Sized,

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

@@ -3,10 +3,10 @@ use std::marker::PhantomData;
 use gpui3::{div, Div};
 
 use crate::prelude::*;
+use crate::settings::user_settings;
 use crate::theme::theme;
 use crate::{
-    h_stack, token, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor,
-    LabelSize,
+    h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor, LabelSize,
 };
 
 #[derive(Clone, Copy, Default, Debug, PartialEq)]
@@ -94,7 +94,6 @@ impl<S: 'static + Send + Sync + Clone> ListHeader<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let theme = theme(cx);
-        let token = token();
         let system_color = SystemColor::new();
         let color = ThemeColor::new(cx);
 
@@ -166,7 +165,6 @@ impl<S: 'static + Send + Sync + Clone> ListSubHeader<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let theme = theme(cx);
-        let token = token();
 
         h_stack().flex_1().w_full().relative().py_1().child(
             div()
@@ -351,7 +349,6 @@ impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
         cx: &mut ViewContext<S>,
     ) -> Option<impl Element<ViewState = S>> {
         let theme = theme(cx);
-        let token = token();
 
         let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
             IconElement::new(Icon::ChevronDown)
@@ -374,9 +371,9 @@ impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let theme = theme(cx);
-        let token = token();
         let system_color = SystemColor::new();
         let color = ThemeColor::new(cx);
+        let setting = user_settings();
 
         let left_content = match self.left_content {
             Some(LeftContent::Icon(i)) => Some(
@@ -408,7 +405,7 @@ impl<S: 'static + Send + Sync + Clone> ListEntry<S> {
                     // .ml(rems(0.75 * self.indent_level as f32))
                     .children((0..self.indent_level).map(|_| {
                         div()
-                            .w(token.list_indent_depth)
+                            .w(setting.list_indent_depth())
                             .h_full()
                             .flex()
                             .justify_center()
@@ -484,7 +481,6 @@ impl<S: 'static + Send + Sync + Clone> List<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let theme = theme(cx);
-        let token = token();
         let is_toggleable = self.toggleable != Toggleable::NotToggleable;
         let is_toggled = Toggleable::is_toggled(&self.toggleable);
 

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

@@ -3,8 +3,9 @@ use std::marker::PhantomData;
 use gpui3::{AbsoluteLength, AnyElement};
 use smallvec::SmallVec;
 
+use crate::settings::user_settings;
+use crate::v_stack;
 use crate::{prelude::*, theme};
-use crate::{token, v_stack};
 
 #[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
 pub enum PanelAllowedSides {
@@ -53,14 +54,14 @@ pub struct Panel<S: 'static + Send + Sync> {
 
 impl<S: 'static + Send + Sync> Panel<S> {
     pub fn new(scroll_state: ScrollState) -> Self {
-        let token = token();
+        let setting = user_settings();
 
         Self {
             state_type: PhantomData,
             scroll_state,
             current_side: PanelSide::default(),
             allowed_sides: PanelAllowedSides::default(),
-            initial_width: token.default_panel_size,
+            initial_width: setting.default_panel_size(),
             width: None,
             children: SmallVec::new(),
         }
@@ -96,7 +97,6 @@ impl<S: 'static + Send + Sync> Panel<S> {
     }
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
-        let token = token();
         let theme = theme(cx);
 
         let panel_base;

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

@@ -4,6 +4,7 @@ use std::sync::Arc;
 use gpui3::{view, Context, View};
 
 use crate::prelude::*;
+use crate::settings::user_settings;
 use crate::{
     random_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, MicStatus,
     PlayerStack, PlayerWithCallStatus, ScreenShareStatus, ToolDivider, TrafficLights,
@@ -93,6 +94,9 @@ impl TitleBar {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element<ViewState = Self> {
         let theme = theme(cx);
+        let color = ThemeColor::new(cx);
+        let setting = user_settings();
+
         // let has_focus = cx.window_is_active();
         let has_focus = true;
 
@@ -107,8 +111,7 @@ impl TitleBar {
             .items_center()
             .justify_between()
             .w_full()
-            .h_8()
-            .bg(theme.lowest.base.default.background)
+            .bg(color.background)
             .child(
                 div()
                     .flex()
@@ -123,6 +126,9 @@ impl TitleBar {
                             .flex()
                             .items_center()
                             .gap_1()
+                            .when(setting.titlebar_show_project_owner(), |this| {
+                                this.child(Button::new("iamnbutler"))
+                            })
                             .child(Button::new("zed"))
                             .child(Button::new("nate/gpui2-ui-components")),
                     )

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

@@ -1,7 +1,7 @@
 use std::marker::PhantomData;
 
 use crate::prelude::*;
-use crate::{theme, token, SystemColor};
+use crate::{theme, SystemColor};
 
 #[derive(Clone, Copy)]
 enum TrafficLightColor {
@@ -62,7 +62,6 @@ impl<S: 'static + Send + Sync> TrafficLights<S> {
 
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let theme = theme(cx);
-        let token = token();
 
         div()
             .flex()

crates/ui2/src/elements/button.rs 🔗

@@ -1,9 +1,10 @@
 use std::marker::PhantomData;
 use std::sync::Arc;
 
-use gpui3::{DefiniteLength, Hsla, Interactive, MouseButton, WindowContext};
+use gpui3::{rems, DefiniteLength, Hsla, Interactive, MouseButton, WindowContext};
 
 use crate::prelude::*;
+use crate::settings::user_settings;
 use crate::{h_stack, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
 
 #[derive(Default, PartialEq, Clone, Copy)]
@@ -148,11 +149,11 @@ impl<S: 'static + Send + Sync + Clone> Button<S> {
     fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
         let icon_color = self.icon_color();
         let border_color = self.border_color(cx);
+        let setting = user_settings();
 
         let mut el = h_stack()
-            .h_6()
-            .px_1()
-            .items_center()
+            .p_1()
+            .text_size(rems(1.125 * setting.ui_scale()))
             .rounded_md()
             .border()
             .border_color(border_color)

crates/ui2/src/lib.rs 🔗

@@ -4,6 +4,7 @@ mod components;
 mod element_ext;
 mod elements;
 pub mod prelude;
+mod settings;
 mod static_data;
 mod theme;
 

crates/ui2/src/prelude.rs 🔗

@@ -9,34 +9,28 @@ use gpui3::{hsla, rems, rgb, AbsoluteLength, Hsla};
 use strum::EnumIter;
 
 #[derive(Clone, Copy)]
-pub struct Token {
+pub struct FakeSettings {
     pub list_indent_depth: AbsoluteLength,
     pub default_panel_size: AbsoluteLength,
-    pub state_hover_background: Hsla,
-    pub state_active_background: Hsla,
 }
 
-impl Default for Token {
+impl Default for FakeSettings {
     fn default() -> Self {
         Self {
             list_indent_depth: rems(0.3).into(),
             default_panel_size: AbsoluteLength::Rems(rems(16.)),
-            state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
-            state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
         }
     }
 }
 
-pub fn token() -> Token {
-    Token::default()
-}
-
 #[derive(Default)]
 pub struct SystemColor {
     pub transparent: Hsla,
     pub mac_os_traffic_light_red: Hsla,
     pub mac_os_traffic_light_yellow: Hsla,
     pub mac_os_traffic_light_green: Hsla,
+    pub state_hover_background: Hsla,
+    pub state_active_background: Hsla,
 }
 
 impl SystemColor {
@@ -46,6 +40,8 @@ impl SystemColor {
             mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
             mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
             mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
+            state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
+            state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
         }
     }
     pub fn color(&self) -> Hsla {
@@ -62,6 +58,8 @@ pub struct ThemeColor {
     /// The background color of an elevated surface, like a modal, tooltip or toast.
     pub elevated_surface: Hsla,
     pub surface: Hsla,
+    /// Window background color
+    pub background: Hsla,
     /// Default background for elements like filled buttons,
     /// text fields, checkboxes, radio buttons, etc.
     /// - TODO: Map to step 3.
@@ -99,6 +97,7 @@ impl ThemeColor {
             border_transparent: system_color.transparent,
             elevated_surface: theme.middle.base.default.background,
             surface: theme.middle.base.default.background,
+            background: theme.lowest.base.default.background,
             filled_element: theme.lowest.base.default.background,
             filled_element_hover: theme.lowest.base.hovered.background,
             filled_element_active: theme.lowest.base.active.background,
@@ -251,6 +250,23 @@ pub enum DisclosureControlVisibility {
     Always,
 }
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DisclosureControlStyle {
+    /// Shows the disclosure control only when hovered where possible.
+    ///
+    /// More compact, but not available everywhere.
+    ChevronOnHover,
+    /// Shows an icon where possible, otherwise shows a chevron.
+    ///
+    /// For example, in a file tree a folder or file icon is shown
+    /// instead of a chevron
+    Icon,
+    /// Always shows a chevron.
+    Chevron,
+    /// Completely hides the disclosure control where possible.
+    None,
+}
+
 #[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
 pub enum InteractionState {
     #[default]

crates/ui2/src/settings.rs 🔗

@@ -0,0 +1,102 @@
+use gpui3::{rems, AbsoluteLength};
+
+use crate::DisclosureControlStyle;
+
+// This is a fake static example of user settings overriding the default settings
+pub fn user_settings() -> Settings {
+    let mut settings = Settings::new();
+    settings.list_indent_depth = Some(rems(0.5).into());
+    settings
+}
+
+#[derive(Clone, Copy)]
+pub struct TitlebarSettings {
+    pub show_project_owner: Option<bool>,
+    pub show_git_status: Option<bool>,
+    pub show_git_controls: Option<bool>,
+}
+
+impl Default for TitlebarSettings {
+    fn default() -> Self {
+        Self {
+            show_project_owner: Some(true),
+            show_git_status: Some(true),
+            show_git_controls: Some(true),
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct Settings {
+    pub default_panel_size: Option<AbsoluteLength>,
+    pub list_disclosure_style: Option<DisclosureControlStyle>,
+    pub list_indent_depth: Option<AbsoluteLength>,
+    pub titlebar: TitlebarSettings,
+    pub ui_scale: Option<f32>,
+}
+
+// These should be merged into settings
+impl Settings {
+    pub fn new() -> Self {
+        Self {
+            titlebar: TitlebarSettings::default(),
+            list_disclosure_style: None,
+            list_indent_depth: None,
+            default_panel_size: None,
+            ui_scale: None,
+        }
+    }
+
+    pub fn titlebar_show_project_owner(&self) -> bool {
+        self.titlebar.show_project_owner.unwrap_or(
+            Settings::default()
+                .titlebar
+                .show_project_owner
+                .expect("titlebar_show_project_owner default not set."),
+        )
+    }
+
+    pub fn list_disclosure_style(&self) -> DisclosureControlStyle {
+        self.list_disclosure_style.unwrap_or(
+            Settings::default()
+                .list_disclosure_style
+                .expect("list_disclosure_style default not set."),
+        )
+    }
+
+    pub fn list_indent_depth(&self) -> AbsoluteLength {
+        self.list_indent_depth.unwrap_or(
+            Settings::default()
+                .list_indent_depth
+                .expect("list_indent_depth default not set."),
+        )
+    }
+
+    pub fn default_panel_size(&self) -> AbsoluteLength {
+        self.default_panel_size.unwrap_or(
+            Settings::default()
+                .default_panel_size
+                .expect("default_panel_size default not set."),
+        )
+    }
+
+    pub fn ui_scale(&self) -> f32 {
+        self.ui_scale.unwrap_or(
+            Settings::default()
+                .ui_scale
+                .expect("ui_scale default not set."),
+        )
+    }
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        Self {
+            titlebar: TitlebarSettings::default(),
+            list_disclosure_style: Some(DisclosureControlStyle::ChevronOnHover),
+            list_indent_depth: Some(rems(0.3).into()),
+            default_panel_size: Some(rems(16.).into()),
+            ui_scale: Some(1.),
+        }
+    }
+}