Show badge when there are pending contact requests

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-05-11 17:39:03 +02:00
parent c71b264786
commit 933a1f2cd6
17 changed files with 241 additions and 42 deletions

View file

@ -341,6 +341,19 @@
"icon_color": "#efecf4", "icon_color": "#efecf4",
"background": "#5852605c" "background": "#5852605c"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#26232a"
},
"background": "#576ddb"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#19171c", "icon_color": "#19171c",
"background": "#8b87922e" "background": "#8b87922e"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#e2dfe7"
},
"background": "#576ddb"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#ffffff", "icon_color": "#ffffff",
"background": "#2b2b2b" "background": "#2b2b2b"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#1c1c1c"
},
"background": "#2472f2"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#000000", "icon_color": "#000000",
"background": "#e3e3e3" "background": "#e3e3e3"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#f8f8f8"
},
"background": "#484bed"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#fdf6e3", "icon_color": "#fdf6e3",
"background": "#586e755c" "background": "#586e755c"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#073642"
},
"background": "#268bd2"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#002b36", "icon_color": "#002b36",
"background": "#93a1a12e" "background": "#93a1a12e"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#eee8d5"
},
"background": "#268bd2"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#f5f7ff", "icon_color": "#f5f7ff",
"background": "#5e66875c" "background": "#5e66875c"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#293256"
},
"background": "#3d8fd1"
} }
} }
}, },

View file

@ -341,6 +341,19 @@
"icon_color": "#202746", "icon_color": "#202746",
"background": "#979db42e" "background": "#979db42e"
} }
},
"badge": {
"corner_radius": 3,
"padding": 2,
"margin": {
"bottom": -1,
"right": -1
},
"border": {
"width": 1,
"color": "#dfe2f1"
},
"background": "#3d8fd1"
} }
} }
}, },

View file

@ -7264,7 +7264,7 @@ mod tests {
} }
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox { fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
gpui::Element::boxed(gpui::elements::Empty) gpui::Element::boxed(gpui::elements::Empty::new())
} }
} }
} }

View file

@ -10,14 +10,14 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_actions,
platform::CursorStyle, platform::CursorStyle,
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use theme::IconButton; use theme::IconButton;
use workspace::{AppState, JoinProject, Workspace}; use workspace::{sidebar::SidebarItem, AppState, JoinProject, Workspace};
impl_actions!( impl_actions!(
contacts_panel, 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 { fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
Svg::new(svg_path) Svg::new(svg_path)
.with_color(style.color) .with_color(style.color)

View file

@ -8,11 +8,18 @@ use crate::{
}; };
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint}; use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
pub struct Empty; pub struct Empty {
collapsed: bool,
}
impl Empty { impl Empty {
pub fn new() -> Self { 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, constraint: SizeConstraint,
_: &mut LayoutContext, _: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) { ) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() { let x = if constraint.max.x().is_finite() && !self.collapsed {
constraint.max.x() constraint.max.x()
} else { } else {
constraint.min.x() constraint.min.x()
}; };
let y = if constraint.max.y().is_finite() { let y = if constraint.max.y().is_finite() && !self.collapsed {
constraint.max.y() constraint.max.y()
} else { } else {
constraint.min.y() constraint.min.y()

View file

@ -900,6 +900,12 @@ impl Entity for ProjectPanel {
type Event = Event; type Event = Event;
} }
impl workspace::sidebar::SidebarItem for ProjectPanel {
fn should_show_badge(&self, _: &AppContext) -> bool {
false
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -162,6 +162,7 @@ pub struct StatusBarSidebarButtons {
pub group_left: ContainerStyle, pub group_left: ContainerStyle,
pub group_right: ContainerStyle, pub group_right: ContainerStyle,
pub item: Interactive<SidebarItem>, pub item: Interactive<SidebarItem>,
pub badge: ContainerStyle,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]

View file

@ -1,13 +1,40 @@
use crate::StatusItemView;
use gpui::{ use gpui::{
elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, Entity, RenderContext, View, elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
ViewContext, ViewHandle, RenderContext, Subscription, View, ViewContext, ViewHandle,
}; };
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use theme::Theme; 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 { pub struct Sidebar {
side: Side, side: Side,
@ -23,10 +50,10 @@ pub enum Side {
Right, Right,
} }
#[derive(Clone)]
struct Item { struct Item {
icon_path: &'static str, icon_path: &'static str,
view: AnyViewHandle, view: Rc<dyn SidebarItemHandle>,
_observation: Subscription,
} }
pub struct SidebarButtons { pub struct SidebarButtons {
@ -58,13 +85,18 @@ impl Sidebar {
} }
} }
pub fn add_item( pub fn add_item<T: SidebarItem>(
&mut self, &mut self,
icon_path: &'static str, icon_path: &'static str,
view: AnyViewHandle, view: ViewHandle<T>,
cx: &mut ViewContext<Self>, 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() cx.notify()
} }
@ -82,10 +114,10 @@ impl Sidebar {
cx.notify(); cx.notify();
} }
pub fn active_item(&self) -> Option<&AnyViewHandle> { pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> {
self.active_item_ix self.active_item_ix
.and_then(|ix| self.items.get(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 { fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
@ -185,20 +217,47 @@ impl View for SidebarButtons {
.sidebar_buttons; .sidebar_buttons;
let sidebar = self.sidebar.read(cx); let sidebar = self.sidebar.read(cx);
let item_style = theme.item; let item_style = theme.item;
let badge_style = theme.badge;
let active_ix = sidebar.active_item_ix; let active_ix = sidebar.active_item_ix;
let side = sidebar.side; let side = sidebar.side;
let group_style = match side { let group_style = match side {
Side::Left => theme.group_left, Side::Left => theme.group_left,
Side::Right => theme.group_right, 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() Flex::row()
.with_children(items.iter().enumerate().map(|(ix, item)| { .with_children(
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, _| { items
let style = item_style.style_for(state, Some(ix) == active_ix); .into_iter()
Svg::new(item.icon_path) .enumerate()
.with_color(style.icon_color) .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() .constrained()
.with_width(style.icon_size)
.with_height(style.icon_size) .with_height(style.icon_size)
.contained() .contained()
.with_style(style.container) .with_style(style.container)
@ -212,7 +271,8 @@ impl View for SidebarButtons {
}) })
}) })
.boxed() .boxed()
})) }),
)
.contained() .contained()
.with_style(group_style) .with_style(group_style)
.boxed() .boxed()

View file

@ -1102,7 +1102,7 @@ impl Workspace {
}; };
let active_item = sidebar.update(cx, |sidebar, cx| { let active_item = sidebar.update(cx, |sidebar, cx| {
sidebar.toggle_item(action.item_index, 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 { if let Some(active_item) = active_item {
cx.focus(active_item); cx.focus(active_item);
@ -1123,7 +1123,7 @@ impl Workspace {
}; };
let active_item = sidebar.update(cx, |sidebar, cx| { let active_item = sidebar.update(cx, |sidebar, cx| {
sidebar.activate_item(action.item_index, 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 let Some(active_item) = active_item {
if active_item.is_focused(cx) { if active_item.is_focused(cx) {

View file

@ -1,8 +1,8 @@
import Theme from "../themes/theme"; import Theme from "../themes/theme";
import { backgroundColor, border, iconColor, text } from "./components"; import { backgroundColor, border, iconColor, text } from "./components";
import { workspaceBackground } from "./workspace";
export default function statusBar(theme: Theme) { export default function statusBar(theme: Theme) {
const statusContainer = { const statusContainer = {
cornerRadius: 6, cornerRadius: 6,
padding: { top: 3, bottom: 3, left: 6, right: 6 } padding: { top: 3, bottom: 3, left: 6, right: 6 }
@ -100,6 +100,13 @@ export default function statusBar(theme: Theme) {
iconColor: iconColor(theme, "active"), iconColor: iconColor(theme, "active"),
background: backgroundColor(theme, 300, "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"),
} }
} }
} }

View file

@ -2,11 +2,15 @@ import Theme from "../themes/theme";
import { backgroundColor, border, iconColor, shadow, text } from "./components"; import { backgroundColor, border, iconColor, shadow, text } from "./components";
import statusBar from "./statusBar"; import statusBar from "./statusBar";
export function workspaceBackground(theme: Theme) {
return backgroundColor(theme, 300)
}
export default function workspace(theme: Theme) { export default function workspace(theme: Theme) {
const tab = { const tab = {
height: 32, height: 32,
background: backgroundColor(theme, 300), background: workspaceBackground(theme),
iconClose: iconColor(theme, "muted"), iconClose: iconColor(theme, "muted"),
iconCloseActive: iconColor(theme, "active"), iconCloseActive: iconColor(theme, "active"),
iconConflict: iconColor(theme, "warning"), iconConflict: iconColor(theme, "warning"),