diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index e8d03c60e0f158e22ef75586dfb4dc3f4fd1c1a2..acb5315ddaac226dee41a1ff86103c2acd284440 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -341,6 +341,19 @@ "icon_color": "#efecf4", "background": "#5852605c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#26232a" + }, + "background": "#576ddb" } } }, diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index de7de76670211674b16084e2014c4f95eb7f62a4..5d75efa22a70967326832825ff8fc4730e518dab 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -341,6 +341,19 @@ "icon_color": "#19171c", "background": "#8b87922e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#e2dfe7" + }, + "background": "#576ddb" } } }, diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 78fa14aa5de5eec18ad5c12a98fdddcce7ee90a4..393b5b20d84547a12cc9671082bc45643f419117 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -341,6 +341,19 @@ "icon_color": "#ffffff", "background": "#2b2b2b" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#1c1c1c" + }, + "background": "#2472f2" } } }, diff --git a/assets/themes/light.json b/assets/themes/light.json index 61923bfec5bbc9038c3b1b0af1ca8d0ae284f731..851886982514a8930cc75eab3d9891bebb241638 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -341,6 +341,19 @@ "icon_color": "#000000", "background": "#e3e3e3" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#f8f8f8" + }, + "background": "#484bed" } } }, diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 9865c125865fe791111c336c3a70c200e993094d..6ce85a9ee88dacbc5ee9ece35c635e0c4e69dcb7 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -341,6 +341,19 @@ "icon_color": "#fdf6e3", "background": "#586e755c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#073642" + }, + "background": "#268bd2" } } }, diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index c61d354d35f8e7f3f29b3ee0f359f20b4aae6745..a3bc6b8597743483c73fc2379613f836669510b9 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -341,6 +341,19 @@ "icon_color": "#002b36", "background": "#93a1a12e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#eee8d5" + }, + "background": "#268bd2" } } }, diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 907ec58cc0cf2000e9cb69fac9bff22bc010523e..68657b31c2a35fccfdf65b7f314f6cda8c19d9cf 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -341,6 +341,19 @@ "icon_color": "#f5f7ff", "background": "#5e66875c" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#293256" + }, + "background": "#3d8fd1" } } }, diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 3ae43250f0328ffbad67f7d632f4fa795c5e375e..18e4b99363633f4980c97140ff30f1d30184767d 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -341,6 +341,19 @@ "icon_color": "#202746", "background": "#979db42e" } + }, + "badge": { + "corner_radius": 3, + "padding": 2, + "margin": { + "bottom": -1, + "right": -1 + }, + "border": { + "width": 1, + "color": "#dfe2f1" + }, + "background": "#3d8fd1" } } }, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0a34e75b3a7dc6a31fe5841dfa788bac56dd0aa0..8cd4b6387c640291f47a9e7ac826bbc0b767e585 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -7264,7 +7264,7 @@ mod tests { } fn render(&mut self, _: &mut gpui::RenderContext) -> gpui::ElementBox { - gpui::Element::boxed(gpui::elements::Empty) + gpui::Element::boxed(gpui::elements::Empty::new()) } } } diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 333de8c3d5de35ccd73c4c4a3d1c4ab8843f8127..003f3885b192ffece610e9f17a368e89cd2b446b 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -10,14 +10,14 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, impl_actions, platform::CursorStyle, - Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, + RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use serde::Deserialize; use settings::Settings; use std::sync::Arc; use theme::IconButton; -use workspace::{AppState, JoinProject, Workspace}; +use workspace::{sidebar::SidebarItem, AppState, JoinProject, Workspace}; impl_actions!( contacts_panel, @@ -599,6 +599,16 @@ impl ContactsPanel { } } +impl SidebarItem for ContactsPanel { + fn should_show_badge(&self, cx: &AppContext) -> bool { + !self + .user_store + .read(cx) + .incoming_contact_requests() + .is_empty() + } +} + fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { Svg::new(svg_path) .with_color(style.color) diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index 90b21231639665aec813967e9595d2673437d777..afe24127b58ef6eadc4acf20801d95011ca1036b 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -8,11 +8,18 @@ use crate::{ }; use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; -pub struct Empty; +pub struct Empty { + collapsed: bool, +} impl Empty { pub fn new() -> Self { - Self + Self { collapsed: false } + } + + pub fn collapsed(mut self) -> Self { + self.collapsed = true; + self } } @@ -25,12 +32,12 @@ impl Element for Empty { constraint: SizeConstraint, _: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let x = if constraint.max.x().is_finite() { + let x = if constraint.max.x().is_finite() && !self.collapsed { constraint.max.x() } else { constraint.min.x() }; - let y = if constraint.max.y().is_finite() { + let y = if constraint.max.y().is_finite() && !self.collapsed { constraint.max.y() } else { constraint.min.y() diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 61c97f281d327f01f1cad86bb81dac7bca92bc0f..639d7b44d9c7f1d89d6af6486842a258765ab3d1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -900,6 +900,12 @@ impl Entity for ProjectPanel { type Event = Event; } +impl workspace::sidebar::SidebarItem for ProjectPanel { + fn should_show_badge(&self, _: &AppContext) -> bool { + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index aeb656828ec228ff502431e4f5b7f0535e258feb..5575dce9e7af0b73ac1d566d7a061bdd2af05188 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -162,6 +162,7 @@ pub struct StatusBarSidebarButtons { pub group_left: ContainerStyle, pub group_right: ContainerStyle, pub item: Interactive, + pub badge: ContainerStyle, } #[derive(Deserialize, Default)] diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index c9cbcbb4fb073367c04ca63b9d951310a0ecaed4..366c74e43f7a3bcd31dd44cf6ee0465b07f66558 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -1,13 +1,40 @@ +use crate::StatusItemView; use gpui::{ - elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, Entity, RenderContext, View, - ViewContext, ViewHandle, + elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity, + RenderContext, Subscription, View, ViewContext, ViewHandle, }; use serde::Deserialize; use settings::Settings; use std::{cell::RefCell, rc::Rc}; use theme::Theme; -use crate::StatusItemView; +pub trait SidebarItem: View { + fn should_show_badge(&self, cx: &AppContext) -> bool; +} + +pub trait SidebarItemHandle { + fn should_show_badge(&self, cx: &AppContext) -> bool; + fn to_any(&self) -> AnyViewHandle; +} + +impl SidebarItemHandle for ViewHandle +where + T: SidebarItem, +{ + fn should_show_badge(&self, cx: &AppContext) -> bool { + self.read(cx).should_show_badge(cx) + } + + fn to_any(&self) -> AnyViewHandle { + self.into() + } +} + +impl Into for &dyn SidebarItemHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} pub struct Sidebar { side: Side, @@ -23,10 +50,10 @@ pub enum Side { Right, } -#[derive(Clone)] struct Item { icon_path: &'static str, - view: AnyViewHandle, + view: Rc, + _observation: Subscription, } pub struct SidebarButtons { @@ -58,13 +85,18 @@ impl Sidebar { } } - pub fn add_item( + pub fn add_item( &mut self, icon_path: &'static str, - view: AnyViewHandle, + view: ViewHandle, cx: &mut ViewContext, ) { - self.items.push(Item { icon_path, view }); + let subscription = cx.observe(&view, |_, _, cx| cx.notify()); + self.items.push(Item { + icon_path, + view: Rc::new(view), + _observation: subscription, + }); cx.notify() } @@ -82,10 +114,10 @@ impl Sidebar { cx.notify(); } - pub fn active_item(&self) -> Option<&AnyViewHandle> { + pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> { self.active_item_ix .and_then(|ix| self.items.get(ix)) - .map(|item| &item.view) + .map(|item| item.view.as_ref()) } fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { @@ -185,34 +217,62 @@ impl View for SidebarButtons { .sidebar_buttons; let sidebar = self.sidebar.read(cx); let item_style = theme.item; + let badge_style = theme.badge; let active_ix = sidebar.active_item_ix; let side = sidebar.side; let group_style = match side { Side::Left => theme.group_left, Side::Right => theme.group_right, }; - let items = sidebar.items.clone(); + let items = sidebar + .items + .iter() + .map(|item| (item.icon_path, item.view.clone())) + .collect::>(); Flex::row() - .with_children(items.iter().enumerate().map(|(ix, item)| { - MouseEventHandler::new::(ix, cx, move |state, _| { - let style = item_style.style_for(state, Some(ix) == active_ix); - Svg::new(item.icon_path) - .with_color(style.icon_color) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) + .with_children( + items + .into_iter() + .enumerate() + .map(|(ix, (icon_path, item_view))| { + MouseEventHandler::new::(ix, cx, move |state, cx| { + let is_active = Some(ix) == active_ix; + let style = item_style.style_for(state, is_active); + Stack::new() + .with_child( + Svg::new(icon_path).with_color(style.icon_color).boxed(), + ) + .with_children(if !is_active && item_view.should_show_badge(cx) { + Some( + Empty::new() + .collapsed() + .contained() + .with_style(badge_style) + .aligned() + .bottom() + .right() + .boxed(), + ) + } else { + None + }) + .constrained() + .with_width(style.icon_size) + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| { + cx.dispatch_action(ToggleSidebarItem { + side, + item_index: ix, + }) + }) .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(ToggleSidebarItem { - side, - item_index: ix, - }) - }) - .boxed() - })) + }), + ) .contained() .with_style(group_style) .boxed() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b077b82518225e0b81173fbca74bd37c0ef309f7..68ecfa8903638eb26f180c6f10c1536aeaad9977 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1102,7 +1102,7 @@ impl Workspace { }; let active_item = sidebar.update(cx, |sidebar, cx| { sidebar.toggle_item(action.item_index, cx); - sidebar.active_item().cloned() + sidebar.active_item().map(|item| item.to_any()) }); if let Some(active_item) = active_item { cx.focus(active_item); @@ -1123,7 +1123,7 @@ impl Workspace { }; let active_item = sidebar.update(cx, |sidebar, cx| { sidebar.activate_item(action.item_index, cx); - sidebar.active_item().cloned() + sidebar.active_item().map(|item| item.to_any()) }); if let Some(active_item) = active_item { if active_item.is_focused(cx) { diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index 621b77639ea5f7a89f78d3756811fff7d41b6101..c7b7c6a0a35b3138e77d648b042344f98c507ff2 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -1,8 +1,8 @@ import Theme from "../themes/theme"; import { backgroundColor, border, iconColor, text } from "./components"; +import { workspaceBackground } from "./workspace"; export default function statusBar(theme: Theme) { - const statusContainer = { cornerRadius: 6, padding: { top: 3, bottom: 3, left: 6, right: 6 } @@ -100,6 +100,13 @@ export default function statusBar(theme: Theme) { iconColor: iconColor(theme, "active"), background: backgroundColor(theme, 300, "active"), } + }, + badge: { + cornerRadius: 3, + padding: 2, + margin: { bottom: -1, right: -1 }, + border: { width: 1, color: workspaceBackground(theme) }, + background: iconColor(theme, "feature"), } } } diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 1d4b78944fbb1945eb84baab1108162c290fe1f8..326b07b9eef4eed9ba98b116d56eaeb422ff3f54 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -2,11 +2,15 @@ import Theme from "../themes/theme"; import { backgroundColor, border, iconColor, shadow, text } from "./components"; import statusBar from "./statusBar"; +export function workspaceBackground(theme: Theme) { + return backgroundColor(theme, 300) +} + export default function workspace(theme: Theme) { const tab = { height: 32, - background: backgroundColor(theme, 300), + background: workspaceBackground(theme), iconClose: iconColor(theme, "muted"), iconCloseActive: iconColor(theme, "active"), iconConflict: iconColor(theme, "warning"),