Detailed changes
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -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"
}
}
},
@@ -7264,7 +7264,7 @@ mod tests {
}
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
- gpui::Element::boxed(gpui::elements::Empty)
+ gpui::Element::boxed(gpui::elements::Empty::new())
}
}
}
@@ -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)
@@ -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()
@@ -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::*;
@@ -162,6 +162,7 @@ pub struct StatusBarSidebarButtons {
pub group_left: ContainerStyle,
pub group_right: ContainerStyle,
pub item: Interactive<SidebarItem>,
+ pub badge: ContainerStyle,
}
#[derive(Deserialize, Default)]
@@ -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<T> SidebarItemHandle for ViewHandle<T>
+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<AnyViewHandle> 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<dyn SidebarItemHandle>,
+ _observation: Subscription,
}
pub struct SidebarButtons {
@@ -58,13 +85,18 @@ impl Sidebar {
}
}
- pub fn add_item(
+ pub fn add_item<T: SidebarItem>(
&mut self,
icon_path: &'static str,
- view: AnyViewHandle,
+ view: ViewHandle<T>,
cx: &mut ViewContext<Self>,
) {
- 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<Self>) -> 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::<Vec<_>>();
Flex::row()
- .with_children(items.iter().enumerate().map(|(ix, item)| {
- MouseEventHandler::new::<Self, _, _>(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::<Self, _, _>(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()
@@ -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) {
@@ -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"),
}
}
}
@@ -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"),