Detailed changes
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-off"><path d="M8.7 3A6 6 0 0 1 18 8a21.3 21.3 0 0 0 .6 5"/><path d="M17 17H3s3-2 3-9a4.67 4.67 0 0 1 .3-1.7"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="m2 2 20 20"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-ring"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M4 2C2.8 3.7 2 5.7 2 8"/><path d="M22 8c0-2.3-.8-4.3-2-6"/></svg>
@@ -1,8 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path
- fill-rule="evenodd"
- clip-rule="evenodd"
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>
@@ -190,138 +190,142 @@ where
.detach()
}
-// #[cfg(test)]
-// mod tests {
-// use std::thread;
-
-// use sqlez::domain::Domain;
-// use sqlez_macros::sql;
-// use tempdir::TempDir;
-
-// use crate::open_db;
-
-// // Test bad migration panics
-// #[gpui::test]
-// #[should_panic]
-// async fn test_bad_migration_panics() {
-// enum BadDB {}
-
-// impl Domain for BadDB {
-// fn name() -> &'static str {
-// "db_tests"
-// }
-
-// fn migrations() -> &'static [&'static str] {
-// &[
-// sql!(CREATE TABLE test(value);),
-// // failure because test already exists
-// sql!(CREATE TABLE test(value);),
-// ]
-// }
-// }
-
-// let tempdir = TempDir::new("DbTests").unwrap();
-// let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// }
-
-// /// Test that DB exists but corrupted (causing recreate)
-// #[gpui::test]
-// async fn test_db_corruption() {
-// enum CorruptedDB {}
-
-// impl Domain for CorruptedDB {
-// fn name() -> &'static str {
-// "db_tests"
-// }
-
-// fn migrations() -> &'static [&'static str] {
-// &[sql!(CREATE TABLE test(value);)]
-// }
-// }
-
-// enum GoodDB {}
-
-// impl Domain for GoodDB {
-// fn name() -> &'static str {
-// "db_tests" //Notice same name
-// }
-
-// fn migrations() -> &'static [&'static str] {
-// &[sql!(CREATE TABLE test2(value);)] //But different migration
-// }
-// }
-
-// let tempdir = TempDir::new("DbTests").unwrap();
-// {
-// let corrupt_db =
-// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// assert!(corrupt_db.persistent());
-// }
-
-// let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// assert!(
-// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
-// .unwrap()
-// .is_none()
-// );
-// }
-
-// /// Test that DB exists but corrupted (causing recreate)
-// #[gpui::test(iterations = 30)]
-// async fn test_simultaneous_db_corruption() {
-// enum CorruptedDB {}
-
-// impl Domain for CorruptedDB {
-// fn name() -> &'static str {
-// "db_tests"
-// }
-
-// fn migrations() -> &'static [&'static str] {
-// &[sql!(CREATE TABLE test(value);)]
-// }
-// }
-
-// enum GoodDB {}
-
-// impl Domain for GoodDB {
-// fn name() -> &'static str {
-// "db_tests" //Notice same name
-// }
-
-// fn migrations() -> &'static [&'static str] {
-// &[sql!(CREATE TABLE test2(value);)] //But different migration
-// }
-// }
-
-// let tempdir = TempDir::new("DbTests").unwrap();
-// {
-// // Setup the bad database
-// let corrupt_db =
-// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// assert!(corrupt_db.persistent());
-// }
-
-// // Try to connect to it a bunch of times at once
-// let mut guards = vec![];
-// for _ in 0..10 {
-// let tmp_path = tempdir.path().to_path_buf();
-// let guard = thread::spawn(move || {
-// let good_db = smol::block_on(open_db::<GoodDB>(
-// tmp_path.as_path(),
-// &util::channel::ReleaseChannel::Dev,
-// ));
-// assert!(
-// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
-// .unwrap()
-// .is_none()
-// );
-// });
-
-// guards.push(guard);
-// }
-
-// for guard in guards.into_iter() {
-// assert!(guard.join().is_ok());
-// }
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use std::thread;
+
+ use sqlez::domain::Domain;
+ use sqlez_macros::sql;
+ use tempdir::TempDir;
+
+ use crate::open_db;
+
+ // Test bad migration panics
+ #[gpui2::test]
+ #[should_panic]
+ async fn test_bad_migration_panics() {
+ enum BadDB {}
+
+ impl Domain for BadDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[
+ sql!(CREATE TABLE test(value);),
+ // failure because test already exists
+ sql!(CREATE TABLE test(value);),
+ ]
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ }
+
+ /// Test that DB exists but corrupted (causing recreate)
+ #[gpui2::test]
+ async fn test_db_corruption(cx: &mut gpui2::TestAppContext) {
+ cx.executor().allow_parking();
+
+ enum CorruptedDB {}
+
+ impl Domain for CorruptedDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
+ }
+
+ enum GoodDB {}
+
+ impl Domain for GoodDB {
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ {
+ let corrupt_db =
+ open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(corrupt_db.persistent());
+ }
+
+ let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(
+ good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
+ .unwrap()
+ .is_none()
+ );
+ }
+
+ /// Test that DB exists but corrupted (causing recreate)
+ #[gpui2::test(iterations = 30)]
+ async fn test_simultaneous_db_corruption(cx: &mut gpui2::TestAppContext) {
+ cx.executor().allow_parking();
+
+ enum CorruptedDB {}
+
+ impl Domain for CorruptedDB {
+ fn name() -> &'static str {
+ "db_tests"
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test(value);)]
+ }
+ }
+
+ enum GoodDB {}
+
+ impl Domain for GoodDB {
+ fn name() -> &'static str {
+ "db_tests" //Notice same name
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(CREATE TABLE test2(value);)] //But different migration
+ }
+ }
+
+ let tempdir = TempDir::new("DbTests").unwrap();
+ {
+ // Setup the bad database
+ let corrupt_db =
+ open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(corrupt_db.persistent());
+ }
+
+ // Try to connect to it a bunch of times at once
+ let mut guards = vec![];
+ for _ in 0..10 {
+ let tmp_path = tempdir.path().to_path_buf();
+ let guard = thread::spawn(move || {
+ let good_db = smol::block_on(open_db::<GoodDB>(
+ tmp_path.as_path(),
+ &util::channel::ReleaseChannel::Dev,
+ ));
+ assert!(
+ good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
+ .unwrap()
+ .is_none()
+ );
+ });
+
+ guards.push(guard);
+ }
+
+ for guard in guards.into_iter() {
+ assert!(guard.join().is_ok());
+ }
+ }
+}
@@ -31,32 +31,32 @@ impl KeyValueStore {
}
}
-// #[cfg(test)]
-// mod tests {
-// use crate::kvp::KeyValueStore;
-
-// #[gpui::test]
-// async fn test_kvp() {
-// let db = KeyValueStore(crate::open_test_db("test_kvp").await);
-
-// assert_eq!(db.read_kvp("key-1").unwrap(), None);
-
-// db.write_kvp("key-1".to_string(), "one".to_string())
-// .await
-// .unwrap();
-// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
-
-// db.write_kvp("key-1".to_string(), "one-2".to_string())
-// .await
-// .unwrap();
-// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
-
-// db.write_kvp("key-2".to_string(), "two".to_string())
-// .await
-// .unwrap();
-// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
-
-// db.delete_kvp("key-1".to_string()).await.unwrap();
-// assert_eq!(db.read_kvp("key-1").unwrap(), None);
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use crate::kvp::KeyValueStore;
+
+ #[gpui2::test]
+ async fn test_kvp() {
+ let db = KeyValueStore(crate::open_test_db("test_kvp").await);
+
+ assert_eq!(db.read_kvp("key-1").unwrap(), None);
+
+ db.write_kvp("key-1".to_string(), "one".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
+
+ db.write_kvp("key-1".to_string(), "one-2".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
+
+ db.write_kvp("key-2".to_string(), "two".to_string())
+ .await
+ .unwrap();
+ assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
+
+ db.delete_kvp("key-1".to_string()).await.unwrap();
+ assert_eq!(db.read_kvp("key-1").unwrap(), None);
+ }
+}
@@ -198,14 +198,19 @@ impl<V> AnyElement<V> {
pub trait Component<V> {
fn render(self) -> AnyElement<V>;
- fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+ fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where
Self: Sized,
+ U: Component<V>,
{
- if condition {
- self = then(self);
- }
- self
+ f(self)
+ }
+
+ fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| if condition { then(this) } else { this })
}
}
@@ -6,8 +6,9 @@ use crate::{
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
- SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
- Underline, UnderlineStyle, View, VisualContext, WeakView, WindowOptions, SUBPIXEL_VARIANTS,
+ SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
+ TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
+ WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Result};
use collections::HashMap;
@@ -56,6 +57,7 @@ pub enum DispatchPhase {
Capture,
}
+type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
type AnyKeyListener = Box<
dyn Fn(
@@ -187,6 +189,10 @@ pub struct Window {
default_prevented: bool,
mouse_position: Point<Pixels>,
scale_factor: f32,
+ bounds: WindowBounds,
+ bounds_observers: SubscriberSet<(), AnyObserver>,
+ active: bool,
+ activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) scene_builder: SceneBuilder,
pub(crate) dirty: bool,
pub(crate) last_blur: Option<Option<FocusId>>,
@@ -205,16 +211,34 @@ impl Window {
let mouse_position = platform_window.mouse_position();
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
+ let bounds = platform_window.bounds();
+
platform_window.on_resize(Box::new({
let mut cx = cx.to_async();
- move |content_size, scale_factor| {
+ move |_, _| {
+ handle
+ .update(&mut cx, |_, cx| cx.window_bounds_changed())
+ .log_err();
+ }
+ }));
+ platform_window.on_moved(Box::new({
+ let mut cx = cx.to_async();
+ move || {
+ handle
+ .update(&mut cx, |_, cx| cx.window_bounds_changed())
+ .log_err();
+ }
+ }));
+ platform_window.on_active_status_change(Box::new({
+ let mut cx = cx.to_async();
+ move |active| {
handle
.update(&mut cx, |_, cx| {
- cx.window.scale_factor = scale_factor;
- cx.window.scene_builder = SceneBuilder::new();
- cx.window.content_size = content_size;
- cx.window.display_id = cx.window.platform_window.display().id();
- cx.window.dirty = true;
+ cx.window.active = active;
+ cx.window
+ .activation_observers
+ .clone()
+ .retain(&(), |callback| callback(cx));
})
.log_err();
}
@@ -257,6 +281,10 @@ impl Window {
default_prevented: true,
mouse_position,
scale_factor,
+ bounds,
+ bounds_observers: SubscriberSet::new(),
+ active: false,
+ activation_observers: SubscriberSet::new(),
scene_builder: SceneBuilder::new(),
dirty: true,
last_blur: None,
@@ -534,6 +562,23 @@ impl<'a> WindowContext<'a> {
bounds
}
+ fn window_bounds_changed(&mut self) {
+ self.window.scale_factor = self.window.platform_window.scale_factor();
+ self.window.content_size = self.window.platform_window.content_size();
+ self.window.bounds = self.window.platform_window.bounds();
+ self.window.display_id = self.window.platform_window.display().id();
+ self.window.dirty = true;
+
+ self.window
+ .bounds_observers
+ .clone()
+ .retain(&(), |callback| callback(self));
+ }
+
+ pub fn window_bounds(&self) -> WindowBounds {
+ self.window.bounds
+ }
+
/// The scale factor of the display associated with the window. For example, it could
/// return 2.0 for a "retina" display, indicating that each logical pixel should actually
/// be rendered as two pixels on screen.
@@ -1726,6 +1771,28 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
+ pub fn observe_window_bounds(
+ &mut self,
+ mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
+ ) -> Subscription {
+ let view = self.view.downgrade();
+ self.window.bounds_observers.insert(
+ (),
+ Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
+ )
+ }
+
+ pub fn observe_window_activation(
+ &mut self,
+ mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
+ ) -> Subscription {
+ let view = self.view.downgrade();
+ self.window.activation_observers.insert(
+ (),
+ Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
+ )
+ }
+
pub fn on_focus_changed(
&mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
@@ -1107,74 +1107,74 @@ impl FakeLanguageServer {
}
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use gpui::TestAppContext;
-
-// #[ctor::ctor]
-// fn init_logger() {
-// if std::env::var("RUST_LOG").is_ok() {
-// env_logger::init();
-// }
-// }
-
-// #[gpui::test]
-// async fn test_fake(cx: &mut TestAppContext) {
-// let (server, mut fake) =
-// LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async());
-
-// let (message_tx, message_rx) = channel::unbounded();
-// let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
-// server
-// .on_notification::<notification::ShowMessage, _>(move |params, _| {
-// message_tx.try_send(params).unwrap()
-// })
-// .detach();
-// server
-// .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
-// diagnostics_tx.try_send(params).unwrap()
-// })
-// .detach();
-
-// let server = server.initialize(None).await.unwrap();
-// server
-// .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
-// text_document: TextDocumentItem::new(
-// Url::from_str("file://a/b").unwrap(),
-// "rust".to_string(),
-// 0,
-// "".to_string(),
-// ),
-// })
-// .unwrap();
-// assert_eq!(
-// fake.receive_notification::<notification::DidOpenTextDocument>()
-// .await
-// .text_document
-// .uri
-// .as_str(),
-// "file://a/b"
-// );
-
-// fake.notify::<notification::ShowMessage>(ShowMessageParams {
-// typ: MessageType::ERROR,
-// message: "ok".to_string(),
-// });
-// fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
-// uri: Url::from_str("file://b/c").unwrap(),
-// version: Some(5),
-// diagnostics: vec![],
-// });
-// assert_eq!(message_rx.recv().await.unwrap().message, "ok");
-// assert_eq!(
-// diagnostics_rx.recv().await.unwrap().uri.as_str(),
-// "file://b/c"
-// );
-
-// fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
-
-// drop(server);
-// fake.receive_notification::<notification::Exit>().await;
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui2::TestAppContext;
+
+ #[ctor::ctor]
+ fn init_logger() {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+ }
+
+ #[gpui2::test]
+ async fn test_fake(cx: &mut TestAppContext) {
+ let (server, mut fake) =
+ LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async());
+
+ let (message_tx, message_rx) = channel::unbounded();
+ let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
+ server
+ .on_notification::<notification::ShowMessage, _>(move |params, _| {
+ message_tx.try_send(params).unwrap()
+ })
+ .detach();
+ server
+ .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
+ diagnostics_tx.try_send(params).unwrap()
+ })
+ .detach();
+
+ let server = server.initialize(None).await.unwrap();
+ server
+ .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
+ text_document: TextDocumentItem::new(
+ Url::from_str("file://a/b").unwrap(),
+ "rust".to_string(),
+ 0,
+ "".to_string(),
+ ),
+ })
+ .unwrap();
+ assert_eq!(
+ fake.receive_notification::<notification::DidOpenTextDocument>()
+ .await
+ .text_document
+ .uri
+ .as_str(),
+ "file://a/b"
+ );
+
+ fake.notify::<notification::ShowMessage>(ShowMessageParams {
+ typ: MessageType::ERROR,
+ message: "ok".to_string(),
+ });
+ fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
+ uri: Url::from_str("file://b/c").unwrap(),
+ version: Some(5),
+ diagnostics: vec![],
+ });
+ assert_eq!(message_rx.recv().await.unwrap().message, "ok");
+ assert_eq!(
+ diagnostics_rx.recv().await.unwrap().uri.as_str(),
+ "file://b/c"
+ );
+
+ fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
+
+ drop(server);
+ fake.receive_notification::<notification::Exit>().await;
+ }
+}
@@ -77,7 +77,7 @@ fn main() {
WindowOptions {
bounds: WindowBounds::Fixed(Bounds {
origin: Default::default(),
- size: size(px(1700.), px(980.)).into(),
+ size: size(px(1500.), px(780.)).into(),
}),
..Default::default()
},
@@ -64,6 +64,7 @@ pub struct ThemeColors {
pub element_selected: Hsla,
pub element_disabled: Hsla,
pub element_placeholder: Hsla,
+ pub element_drop_target: Hsla,
pub ghost_element: Hsla,
pub ghost_element_hover: Hsla,
pub ghost_element_active: Hsla,
@@ -83,6 +84,8 @@ pub struct ThemeColors {
pub title_bar: Hsla,
pub toolbar: Hsla,
pub tab_bar: Hsla,
+ pub tab_inactive: Hsla,
+ pub tab_active: Hsla,
pub editor: Hsla,
pub editor_subheader: Hsla,
pub editor_active_line: Hsla,
@@ -9,6 +9,10 @@ use crate::{
ColorScale,
};
+fn neutral() -> ColorScaleSet {
+ slate()
+}
+
impl Default for SystemColors {
fn default() -> Self {
Self {
@@ -24,16 +28,16 @@ impl Default for StatusColors {
fn default() -> Self {
Self {
conflict: red().dark().step_11(),
- created: gpui2::black(),
- deleted: gpui2::black(),
- error: gpui2::black(),
- hidden: gpui2::black(),
- ignored: gpui2::black(),
- info: gpui2::black(),
- modified: gpui2::black(),
- renamed: gpui2::black(),
- success: gpui2::black(),
- warning: gpui2::black(),
+ 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(),
}
}
}
@@ -41,12 +45,12 @@ impl Default for StatusColors {
impl Default for GitStatusColors {
fn default() -> Self {
Self {
- conflict: gpui2::rgba(0xdec184ff).into(),
- created: gpui2::rgba(0xa1c181ff).into(),
- deleted: gpui2::rgba(0xd07277ff).into(),
- ignored: gpui2::rgba(0x555a63ff).into(),
- modified: gpui2::rgba(0x74ade8ff).into(),
- renamed: gpui2::rgba(0xdec184ff).into(),
+ 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(),
}
}
}
@@ -82,54 +86,57 @@ impl SyntaxTheme {
pub fn default_light() -> Self {
Self {
highlights: vec![
+ ("attribute".into(), cyan().light().step_11().into()),
+ ("boolean".into(), tomato().light().step_11().into()),
+ ("comment".into(), neutral().light().step_11().into()),
+ ("comment.doc".into(), iris().light().step_12().into()),
+ ("constant".into(), red().light().step_7().into()),
+ ("constructor".into(), red().light().step_7().into()),
+ ("embedded".into(), red().light().step_7().into()),
+ ("emphasis".into(), red().light().step_7().into()),
+ ("emphasis.strong".into(), red().light().step_7().into()),
+ ("enum".into(), red().light().step_7().into()),
+ ("function".into(), red().light().step_7().into()),
+ ("hint".into(), red().light().step_7().into()),
+ ("keyword".into(), orange().light().step_11().into()),
+ ("label".into(), red().light().step_7().into()),
+ ("link_text".into(), red().light().step_7().into()),
+ ("link_uri".into(), red().light().step_7().into()),
+ ("number".into(), red().light().step_7().into()),
+ ("operator".into(), red().light().step_7().into()),
+ ("predictive".into(), red().light().step_7().into()),
+ ("preproc".into(), red().light().step_7().into()),
+ ("primary".into(), red().light().step_7().into()),
+ ("property".into(), red().light().step_7().into()),
+ ("punctuation".into(), neutral().light().step_11().into()),
(
- "string.special.symbol".into(),
- gpui2::rgba(0xad6e26ff).into(),
+ "punctuation.bracket".into(),
+ neutral().light().step_11().into(),
+ ),
+ (
+ "punctuation.delimiter".into(),
+ neutral().light().step_11().into(),
),
- ("hint".into(), gpui2::rgba(0x9294beff).into()),
- ("link_uri".into(), gpui2::rgba(0x3882b7ff).into()),
- ("type".into(), gpui2::rgba(0x3882b7ff).into()),
- ("string.regex".into(), gpui2::rgba(0xad6e26ff).into()),
- ("constant".into(), gpui2::rgba(0x669f59ff).into()),
- ("function".into(), gpui2::rgba(0x5b79e3ff).into()),
- ("string.special".into(), gpui2::rgba(0xad6e26ff).into()),
- ("punctuation.bracket".into(), gpui2::rgba(0x4d4f52ff).into()),
- ("variable".into(), gpui2::rgba(0x383a41ff).into()),
- ("punctuation".into(), gpui2::rgba(0x383a41ff).into()),
- ("property".into(), gpui2::rgba(0xd3604fff).into()),
- ("string".into(), gpui2::rgba(0x649f57ff).into()),
- ("predictive".into(), gpui2::rgba(0x9b9ec6ff).into()),
- ("attribute".into(), gpui2::rgba(0x5c78e2ff).into()),
- ("number".into(), gpui2::rgba(0xad6e25ff).into()),
- ("constructor".into(), gpui2::rgba(0x5c78e2ff).into()),
- ("embedded".into(), gpui2::rgba(0x383a41ff).into()),
- ("title".into(), gpui2::rgba(0xd3604fff).into()),
- ("tag".into(), gpui2::rgba(0x5c78e2ff).into()),
- ("boolean".into(), gpui2::rgba(0xad6e25ff).into()),
(
"punctuation.list_marker".into(),
- gpui2::rgba(0xd3604fff).into(),
+ blue().light().step_11().into(),
),
- ("variant".into(), gpui2::rgba(0x5b79e3ff).into()),
- ("emphasis".into(), gpui2::rgba(0x5c78e2ff).into()),
- ("link_text".into(), gpui2::rgba(0x5b79e3ff).into()),
- ("comment".into(), gpui2::rgba(0xa2a3a7ff).into()),
- ("punctuation.special".into(), gpui2::rgba(0xb92b46ff).into()),
- ("emphasis.strong".into(), gpui2::rgba(0xad6e25ff).into()),
- ("primary".into(), gpui2::rgba(0x383a41ff).into()),
+ ("punctuation.special".into(), red().light().step_7().into()),
+ ("string".into(), jade().light().step_11().into()),
+ ("string.escape".into(), red().light().step_7().into()),
+ ("string.regex".into(), tomato().light().step_11().into()),
+ ("string.special".into(), red().light().step_7().into()),
(
- "punctuation.delimiter".into(),
- gpui2::rgba(0x4d4f52ff).into(),
+ "string.special.symbol".into(),
+ red().light().step_7().into(),
),
- ("label".into(), gpui2::rgba(0x5c78e2ff).into()),
- ("keyword".into(), gpui2::rgba(0xa449abff).into()),
- ("string.escape".into(), gpui2::rgba(0x7c7e86ff).into()),
- ("text.literal".into(), gpui2::rgba(0x649f57ff).into()),
- ("variable.special".into(), gpui2::rgba(0xad6e25ff).into()),
- ("comment.doc".into(), gpui2::rgba(0x7c7e86ff).into()),
- ("enum".into(), gpui2::rgba(0xd3604fff).into()),
- ("operator".into(), gpui2::rgba(0x3882b7ff).into()),
- ("preproc".into(), gpui2::rgba(0x383a41ff).into()),
+ ("tag".into(), red().light().step_7().into()),
+ ("text.literal".into(), red().light().step_7().into()),
+ ("title".into(), red().light().step_7().into()),
+ ("type".into(), red().light().step_7().into()),
+ ("variable".into(), red().light().step_7().into()),
+ ("variable.special".into(), red().light().step_7().into()),
+ ("variant".into(), red().light().step_7().into()),
],
}
}
@@ -137,54 +144,54 @@ impl SyntaxTheme {
pub fn default_dark() -> Self {
Self {
highlights: vec![
- ("keyword".into(), gpui2::rgba(0xb477cfff).into()),
- ("comment.doc".into(), gpui2::rgba(0x878e98ff).into()),
- ("variant".into(), gpui2::rgba(0x73ade9ff).into()),
- ("property".into(), gpui2::rgba(0xd07277ff).into()),
- ("function".into(), gpui2::rgba(0x73ade9ff).into()),
- ("type".into(), gpui2::rgba(0x6eb4bfff).into()),
- ("tag".into(), gpui2::rgba(0x74ade8ff).into()),
- ("string.escape".into(), gpui2::rgba(0x878e98ff).into()),
- ("punctuation.bracket".into(), gpui2::rgba(0xb2b9c6ff).into()),
- ("hint".into(), gpui2::rgba(0x5a6f89ff).into()),
- ("punctuation".into(), gpui2::rgba(0xacb2beff).into()),
- ("comment".into(), gpui2::rgba(0x5d636fff).into()),
- ("emphasis".into(), gpui2::rgba(0x74ade8ff).into()),
- ("punctuation.special".into(), gpui2::rgba(0xb1574bff).into()),
- ("link_uri".into(), gpui2::rgba(0x6eb4bfff).into()),
- ("string.regex".into(), gpui2::rgba(0xbf956aff).into()),
- ("constructor".into(), gpui2::rgba(0x73ade9ff).into()),
- ("operator".into(), gpui2::rgba(0x6eb4bfff).into()),
- ("constant".into(), gpui2::rgba(0xdfc184ff).into()),
- ("string.special".into(), gpui2::rgba(0xbf956aff).into()),
- ("emphasis.strong".into(), gpui2::rgba(0xbf956aff).into()),
+ ("attribute".into(), cyan().dark().step_11().into()),
+ ("boolean".into(), tomato().dark().step_11().into()),
+ ("comment".into(), neutral().dark().step_11().into()),
+ ("comment.doc".into(), iris().dark().step_12().into()),
+ ("constant".into(), red().dark().step_7().into()),
+ ("constructor".into(), red().dark().step_7().into()),
+ ("embedded".into(), red().dark().step_7().into()),
+ ("emphasis".into(), red().dark().step_7().into()),
+ ("emphasis.strong".into(), red().dark().step_7().into()),
+ ("enum".into(), red().dark().step_7().into()),
+ ("function".into(), red().dark().step_7().into()),
+ ("hint".into(), red().dark().step_7().into()),
+ ("keyword".into(), orange().dark().step_11().into()),
+ ("label".into(), red().dark().step_7().into()),
+ ("link_text".into(), red().dark().step_7().into()),
+ ("link_uri".into(), red().dark().step_7().into()),
+ ("number".into(), red().dark().step_7().into()),
+ ("operator".into(), red().dark().step_7().into()),
+ ("predictive".into(), red().dark().step_7().into()),
+ ("preproc".into(), red().dark().step_7().into()),
+ ("primary".into(), red().dark().step_7().into()),
+ ("property".into(), red().dark().step_7().into()),
+ ("punctuation".into(), neutral().dark().step_11().into()),
(
- "string.special.symbol".into(),
- gpui2::rgba(0xbf956aff).into(),
+ "punctuation.bracket".into(),
+ neutral().dark().step_11().into(),
),
- ("primary".into(), gpui2::rgba(0xacb2beff).into()),
- ("preproc".into(), gpui2::rgba(0xc8ccd4ff).into()),
- ("string".into(), gpui2::rgba(0xa1c181ff).into()),
(
"punctuation.delimiter".into(),
- gpui2::rgba(0xb2b9c6ff).into(),
+ neutral().dark().step_11().into(),
),
- ("embedded".into(), gpui2::rgba(0xc8ccd4ff).into()),
- ("enum".into(), gpui2::rgba(0xd07277ff).into()),
- ("variable.special".into(), gpui2::rgba(0xbf956aff).into()),
- ("text.literal".into(), gpui2::rgba(0xa1c181ff).into()),
- ("attribute".into(), gpui2::rgba(0x74ade8ff).into()),
- ("link_text".into(), gpui2::rgba(0x73ade9ff).into()),
- ("title".into(), gpui2::rgba(0xd07277ff).into()),
- ("predictive".into(), gpui2::rgba(0x5a6a87ff).into()),
- ("number".into(), gpui2::rgba(0xbf956aff).into()),
- ("label".into(), gpui2::rgba(0x74ade8ff).into()),
- ("variable".into(), gpui2::rgba(0xc8ccd4ff).into()),
- ("boolean".into(), gpui2::rgba(0xbf956aff).into()),
(
"punctuation.list_marker".into(),
- gpui2::rgba(0xd07277ff).into(),
+ blue().dark().step_11().into(),
),
+ ("punctuation.special".into(), red().dark().step_7().into()),
+ ("string".into(), jade().dark().step_11().into()),
+ ("string.escape".into(), red().dark().step_7().into()),
+ ("string.regex".into(), tomato().dark().step_11().into()),
+ ("string.special".into(), red().dark().step_7().into()),
+ ("string.special.symbol".into(), red().dark().step_7().into()),
+ ("tag".into(), red().dark().step_7().into()),
+ ("text.literal".into(), red().dark().step_7().into()),
+ ("title".into(), red().dark().step_7().into()),
+ ("type".into(), red().dark().step_7().into()),
+ ("variable".into(), red().dark().step_7().into()),
+ ("variable.special".into(), red().dark().step_7().into()),
+ ("variant".into(), red().dark().step_7().into()),
],
}
}
@@ -192,82 +199,92 @@ impl SyntaxTheme {
impl ThemeColors {
pub fn default_light() -> Self {
+ let system = SystemColors::default();
+
Self {
- border: gpui2::white(),
- border_variant: gpui2::white(),
- border_focused: gpui2::white(),
- border_transparent: gpui2::white(),
- elevated_surface: gpui2::white(),
- surface: gpui2::white(),
- background: gpui2::white(),
- element: gpui2::white(),
- element_hover: gpui2::white(),
- element_active: gpui2::white(),
- element_selected: gpui2::white(),
- element_disabled: gpui2::white(),
- element_placeholder: gpui2::white(),
- ghost_element: gpui2::white(),
- ghost_element_hover: gpui2::white(),
- ghost_element_active: gpui2::white(),
- ghost_element_selected: gpui2::white(),
- ghost_element_disabled: gpui2::white(),
- text: gpui2::white(),
- text_muted: gpui2::white(),
- text_placeholder: gpui2::white(),
- text_disabled: gpui2::white(),
- text_accent: gpui2::white(),
- icon: gpui2::white(),
- icon_muted: gpui2::white(),
- icon_disabled: gpui2::white(),
- icon_placeholder: gpui2::white(),
- icon_accent: gpui2::white(),
- status_bar: gpui2::white(),
- title_bar: gpui2::white(),
- toolbar: gpui2::white(),
- tab_bar: gpui2::white(),
- editor: gpui2::white(),
- editor_subheader: gpui2::white(),
- editor_active_line: gpui2::white(),
+ border: neutral().light().step_6(),
+ border_variant: neutral().light().step_5(),
+ border_focused: blue().light().step_5(),
+ border_transparent: system.transparent,
+ elevated_surface: neutral().light().step_2(),
+ surface: neutral().light().step_2(),
+ background: neutral().light().step_1(),
+ element: neutral().light().step_3(),
+ element_hover: neutral().light().step_4(),
+ element_active: neutral().light().step_5(),
+ element_selected: neutral().light().step_5(),
+ element_disabled: neutral().light_alpha().step_3(),
+ element_placeholder: neutral().light().step_11(),
+ element_drop_target: blue().light_alpha().step_2(),
+ ghost_element: system.transparent,
+ ghost_element_hover: neutral().light().step_4(),
+ ghost_element_active: neutral().light().step_5(),
+ ghost_element_selected: neutral().light().step_5(),
+ ghost_element_disabled: neutral().light_alpha().step_3(),
+ text: neutral().light().step_12(),
+ text_muted: neutral().light().step_11(),
+ text_placeholder: neutral().light().step_10(),
+ text_disabled: neutral().light().step_9(),
+ text_accent: blue().light().step_11(),
+ icon: neutral().light().step_11(),
+ icon_muted: neutral().light().step_10(),
+ icon_disabled: neutral().light().step_9(),
+ icon_placeholder: neutral().light().step_10(),
+ icon_accent: blue().light().step_11(),
+ status_bar: neutral().light().step_2(),
+ title_bar: neutral().light().step_2(),
+ toolbar: neutral().light().step_1(),
+ tab_bar: neutral().light().step_2(),
+ tab_active: neutral().light().step_1(),
+ tab_inactive: neutral().light().step_2(),
+ editor: neutral().light().step_1(),
+ editor_subheader: neutral().light().step_2(),
+ editor_active_line: neutral().light_alpha().step_3(),
}
}
pub fn default_dark() -> Self {
+ let system = SystemColors::default();
+
Self {
- border: gpui2::rgba(0x464b57ff).into(),
- border_variant: gpui2::rgba(0x464b57ff).into(),
- border_focused: gpui2::rgba(0x293b5bff).into(),
- border_transparent: gpui2::rgba(0x00000000).into(),
- elevated_surface: gpui2::rgba(0x3b414dff).into(),
- surface: gpui2::rgba(0x2f343eff).into(),
- background: gpui2::rgba(0x3b414dff).into(),
- element: gpui2::rgba(0x3b414dff).into(),
- element_hover: gpui2::rgba(0xffffff1e).into(),
- element_active: gpui2::rgba(0xffffff28).into(),
- element_selected: gpui2::rgba(0x18243dff).into(),
- element_disabled: gpui2::rgba(0x00000000).into(),
- element_placeholder: gpui2::black(),
- ghost_element: gpui2::rgba(0x00000000).into(),
- ghost_element_hover: gpui2::rgba(0xffffff14).into(),
- ghost_element_active: gpui2::rgba(0xffffff1e).into(),
- ghost_element_selected: gpui2::rgba(0x18243dff).into(),
- ghost_element_disabled: gpui2::rgba(0x00000000).into(),
- text: gpui2::rgba(0xc8ccd4ff).into(),
- text_muted: gpui2::rgba(0x838994ff).into(),
- text_placeholder: gpui2::rgba(0xd07277ff).into(),
- text_disabled: gpui2::rgba(0x555a63ff).into(),
- text_accent: gpui2::rgba(0x74ade8ff).into(),
- icon: gpui2::black(),
- icon_muted: gpui2::rgba(0x838994ff).into(),
- icon_disabled: gpui2::black(),
- icon_placeholder: gpui2::black(),
- icon_accent: gpui2::black(),
- status_bar: gpui2::rgba(0x3b414dff).into(),
- title_bar: gpui2::rgba(0x3b414dff).into(),
- toolbar: gpui2::rgba(0x282c33ff).into(),
- tab_bar: gpui2::rgba(0x2f343eff).into(),
- editor: gpui2::rgba(0x282c33ff).into(),
- editor_subheader: gpui2::rgba(0x2f343eff).into(),
- editor_active_line: gpui2::rgba(0x2f343eff).into(),
+ border: neutral().dark().step_6(),
+ border_variant: neutral().dark().step_5(),
+ border_focused: blue().dark().step_5(),
+ border_transparent: system.transparent,
+ elevated_surface: neutral().dark().step_2(),
+ surface: neutral().dark().step_2(),
+ background: neutral().dark().step_1(),
+ element: neutral().dark().step_3(),
+ element_hover: neutral().dark().step_4(),
+ element_active: neutral().dark().step_5(),
+ element_selected: neutral().dark().step_5(),
+ element_disabled: neutral().dark_alpha().step_3(),
+ element_placeholder: neutral().dark().step_11(),
+ element_drop_target: blue().dark_alpha().step_2(),
+ ghost_element: system.transparent,
+ ghost_element_hover: neutral().dark().step_4(),
+ ghost_element_active: neutral().dark().step_5(),
+ ghost_element_selected: neutral().dark().step_5(),
+ ghost_element_disabled: neutral().dark_alpha().step_3(),
+ text: neutral().dark().step_12(),
+ text_muted: neutral().dark().step_11(),
+ text_placeholder: neutral().dark().step_10(),
+ text_disabled: neutral().dark().step_9(),
+ text_accent: blue().dark().step_11(),
+ icon: neutral().dark().step_11(),
+ icon_muted: neutral().dark().step_10(),
+ icon_disabled: neutral().dark().step_9(),
+ icon_placeholder: neutral().dark().step_10(),
+ icon_accent: blue().dark().step_11(),
+ status_bar: neutral().dark().step_2(),
+ title_bar: neutral().dark().step_2(),
+ toolbar: neutral().dark().step_1(),
+ tab_bar: neutral().dark().step_2(),
+ tab_active: neutral().dark().step_1(),
+ tab_inactive: neutral().dark().step_2(),
+ editor: neutral().dark().step_1(),
+ editor_subheader: neutral().dark().step_2(),
+ editor_active_line: neutral().dark_alpha().step_3(),
}
}
}
@@ -70,6 +70,18 @@ impl ThemeVariant {
&self.styles.syntax
}
+ /// Returns the [`StatusColors`] for the theme.
+ #[inline(always)]
+ pub fn status(&self) -> &StatusColors {
+ &self.styles.status
+ }
+
+ /// Returns the [`GitStatusColors`] for the theme.
+ #[inline(always)]
+ pub fn git(&self) -> &GitStatusColors {
+ &self.styles.git
+ }
+
/// Returns the color for the syntax node with the given name.
#[inline(always)]
pub fn syntax_color(&self, name: &str) -> Hsla {
@@ -1,4 +1,4 @@
-use gpui2::{div, relative, Div};
+use gpui2::{div, px, relative, Div};
use crate::settings::user_settings;
use crate::{
@@ -15,12 +15,20 @@ pub enum ListItemVariant {
Inset,
}
+pub enum ListHeaderMeta {
+ // TODO: These should be IconButtons
+ Tools(Vec<Icon>),
+ // TODO: This should be a button
+ Button(Label),
+ Text(Label),
+}
+
#[derive(Component)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
+ meta: Option<ListHeaderMeta>,
variant: ListItemVariant,
- state: InteractionState,
toggleable: Toggleable,
}
@@ -29,9 +37,9 @@ impl ListHeader {
Self {
label: label.into(),
left_icon: None,
+ meta: None,
variant: ListItemVariant::default(),
- state: InteractionState::default(),
- toggleable: Toggleable::Toggleable(ToggleState::Toggled),
+ toggleable: Toggleable::NotToggleable,
}
}
@@ -50,8 +58,8 @@ impl ListHeader {
self
}
- pub fn state(mut self, state: InteractionState) -> Self {
- self.state = state;
+ pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
+ self.meta = meta;
self
}
@@ -74,34 +82,36 @@ impl ListHeader {
}
}
- fn label_color(&self) -> LabelColor {
- match self.state {
- InteractionState::Disabled => LabelColor::Disabled,
- _ => Default::default(),
- }
- }
-
- fn icon_color(&self) -> IconColor {
- match self.state {
- InteractionState::Disabled => IconColor::Disabled,
- _ => Default::default(),
- }
- }
-
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = self.toggleable.is_toggled();
let disclosure_control = self.disclosure_control();
+ let meta = match self.meta {
+ Some(ListHeaderMeta::Tools(icons)) => div().child(
+ h_stack()
+ .gap_2()
+ .items_center()
+ .children(icons.into_iter().map(|i| {
+ IconElement::new(i)
+ .color(IconColor::Muted)
+ .size(IconSize::Small)
+ })),
+ ),
+ Some(ListHeaderMeta::Button(label)) => div().child(label),
+ Some(ListHeaderMeta::Text(label)) => div().child(label),
+ None => div(),
+ };
+
h_stack()
- .flex_1()
.w_full()
.bg(cx.theme().colors().surface)
- .when(self.state == InteractionState::Focused, |this| {
- this.border()
- .border_color(cx.theme().colors().border_focused)
- })
+ // TODO: Add focus state
+ // .when(self.state == InteractionState::Focused, |this| {
+ // this.border()
+ // .border_color(cx.theme().colors().border_focused)
+ // })
.relative()
.child(
div()
@@ -109,22 +119,28 @@ impl ListHeader {
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.flex()
.flex_1()
+ .items_center()
+ .justify_between()
.w_full()
.gap_1()
- .items_center()
.child(
- div()
- .flex()
+ h_stack()
.gap_1()
- .items_center()
- .children(self.left_icon.map(|i| {
- IconElement::new(i)
- .color(IconColor::Muted)
- .size(IconSize::Small)
- }))
- .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+ .child(
+ div()
+ .flex()
+ .gap_1()
+ .items_center()
+ .children(self.left_icon.map(|i| {
+ IconElement::new(i)
+ .color(IconColor::Muted)
+ .size(IconSize::Small)
+ }))
+ .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+ )
+ .child(disclosure_control),
)
- .child(disclosure_control),
+ .child(meta),
)
}
}
@@ -473,42 +489,63 @@ impl<V: 'static> ListDetailsEntry<V> {
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let settings = user_settings(cx);
- let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
- true => (
- cx.theme().colors().ghost_element,
- cx.theme().colors().ghost_element_hover,
- cx.theme().colors().ghost_element_active,
- ),
- false => (
- cx.theme().colors().element,
- cx.theme().colors().element_hover,
- cx.theme().colors().element_active,
- ),
- };
+ let (item_bg, item_bg_hover, item_bg_active) = (
+ cx.theme().colors().ghost_element,
+ cx.theme().colors().ghost_element_hover,
+ cx.theme().colors().ghost_element_active,
+ );
let label_color = match self.seen {
true => LabelColor::Muted,
false => LabelColor::Default,
};
- v_stack()
+ div()
.relative()
.group("")
.bg(item_bg)
- .px_1()
- .py_1_5()
+ .px_2()
+ .py_1p5()
.w_full()
- .line_height(relative(1.2))
- .child(Label::new(self.label.clone()).color(label_color))
- .children(
- self.meta
- .map(|meta| Label::new(meta).color(LabelColor::Muted)),
- )
+ .z_index(1)
+ .when(!self.seen, |this| {
+ this.child(
+ div()
+ .absolute()
+ .left(px(3.0))
+ .top_3()
+ .rounded_full()
+ .border_2()
+ .border_color(cx.theme().colors().surface)
+ .w(px(9.0))
+ .h(px(9.0))
+ .z_index(2)
+ .bg(cx.theme().status().info),
+ )
+ })
.child(
- h_stack()
+ v_stack()
+ .w_full()
+ .line_height(relative(1.2))
.gap_1()
- .justify_end()
- .children(self.actions.unwrap_or_default()),
+ .child(
+ div()
+ .w_5()
+ .h_5()
+ .rounded_full()
+ .bg(cx.theme().colors().icon_accent),
+ )
+ .child(Label::new(self.label.clone()).color(label_color))
+ .children(
+ self.meta
+ .map(|meta| Label::new(meta).color(LabelColor::Muted)),
+ )
+ .child(
+ h_stack()
+ .gap_1()
+ .justify_end()
+ .children(self.actions.unwrap_or_default()),
+ ),
)
}
}
@@ -522,7 +559,7 @@ impl ListSeparator {
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().h_px().w_full().bg(cx.theme().colors().border)
+ div().h_px().w_full().bg(cx.theme().colors().border_variant)
}
}
@@ -564,14 +601,15 @@ impl<V: 'static> List<V> {
let is_toggled = Toggleable::is_toggled(&self.toggleable);
let list_content = match (self.items.is_empty(), is_toggled) {
- (_, false) => div(),
(false, _) => div().children(self.items),
- (true, _) => {
+ (true, false) => div(),
+ (true, true) => {
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
}
};
v_stack()
+ .w_full()
.py_1()
.children(self.header.map(|header| header.toggleable(self.toggleable)))
.child(list_content)
@@ -1,5 +1,10 @@
-use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
-use crate::{List, ListHeader};
+use crate::utils::naive_format_distance_from_now;
+use crate::{
+ h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon,
+ IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
+ UnreadIndicator,
+};
+use crate::{ClickHandler, ListHeader};
#[derive(Component)]
pub struct NotificationsPanel {
@@ -16,31 +21,348 @@ impl NotificationsPanel {
.id(self.id.clone())
.flex()
.flex_col()
- .w_full()
- .h_full()
+ .size_full()
.bg(cx.theme().colors().surface)
.child(
- div()
- .id("header")
- .w_full()
- .flex()
- .flex_col()
+ ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
+ Icon::AtSign,
+ Icon::BellOff,
+ Icon::MailOpen,
+ ]))),
+ )
+ .child(ListSeparator::new())
+ .child(
+ v_stack()
+ .id("notifications-panel-scroll-view")
+ .py_1()
.overflow_y_scroll()
+ .flex_1()
+ .child(
+ div()
+ .mx_2()
+ .p_1()
+ // TODO: Add cursor style
+ // .cursor(Cursor::IBeam)
+ .bg(cx.theme().colors().element)
+ .border()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Label::new("Search...")
+ .color(LabelColor::Placeholder)
+ .line_height_style(LineHeightStyle::UILabel),
+ ),
+ )
+ .child(v_stack().px_1().children(static_new_notification_items_2())),
+ )
+ }
+}
+
+pub enum ButtonOrIconButton<V: 'static> {
+ Button(Button<V>),
+ IconButton(IconButton<V>),
+}
+
+impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
+ fn from(value: Button<V>) -> Self {
+ Self::Button(value)
+ }
+}
+
+impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
+ fn from(value: IconButton<V>) -> Self {
+ Self::IconButton(value)
+ }
+}
+
+pub struct NotificationAction<V: 'static> {
+ button: ButtonOrIconButton<V>,
+ tooltip: SharedString,
+ /// Shows after action is chosen
+ ///
+ /// For example, if the action is "Accept" the taken message could be:
+ ///
+ /// - `(None,"Accepted")` - "Accepted"
+ ///
+ /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
+ taken_message: (Option<Icon>, SharedString),
+}
+
+impl<V: 'static> NotificationAction<V> {
+ pub fn new(
+ button: impl Into<ButtonOrIconButton<V>>,
+ tooltip: impl Into<SharedString>,
+ (icon, taken_message): (Option<Icon>, impl Into<SharedString>),
+ ) -> Self {
+ Self {
+ button: button.into(),
+ tooltip: tooltip.into(),
+ taken_message: (icon, taken_message.into()),
+ }
+ }
+}
+
+pub enum ActorOrIcon {
+ Actor(PublicActor),
+ Icon(Icon),
+}
+
+pub struct NotificationMeta<V: 'static> {
+ items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
+}
+
+struct NotificationHandlers<V: 'static> {
+ click: Option<ClickHandler<V>>,
+}
+
+impl<V: 'static> Default for NotificationHandlers<V> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+}
+
+#[derive(Component)]
+pub struct Notification<V: 'static> {
+ id: ElementId,
+ slot: ActorOrIcon,
+ message: SharedString,
+ date_received: NaiveDateTime,
+ meta: Option<NotificationMeta<V>>,
+ actions: Option<[NotificationAction<V>; 2]>,
+ unread: bool,
+ new: bool,
+ action_taken: Option<NotificationAction<V>>,
+ handlers: NotificationHandlers<V>,
+}
+
+impl<V> Notification<V> {
+ fn new(
+ id: ElementId,
+ message: SharedString,
+ date_received: NaiveDateTime,
+ slot: ActorOrIcon,
+ click_action: Option<ClickHandler<V>>,
+ ) -> Self {
+ let handlers = if click_action.is_some() {
+ NotificationHandlers {
+ click: click_action,
+ }
+ } else {
+ NotificationHandlers::default()
+ };
+
+ Self {
+ id,
+ date_received,
+ message,
+ meta: None,
+ slot,
+ actions: None,
+ unread: true,
+ new: false,
+ action_taken: None,
+ handlers,
+ }
+ }
+
+ /// Creates a new notification with an actor slot.
+ ///
+ /// Requires a click action.
+ pub fn new_actor_message(
+ id: impl Into<ElementId>,
+ message: impl Into<SharedString>,
+ date_received: NaiveDateTime,
+ actor: PublicActor,
+ click_action: ClickHandler<V>,
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message.into(),
+ date_received,
+ ActorOrIcon::Actor(actor),
+ Some(click_action),
+ )
+ }
+
+ /// Creates a new notification with an icon slot.
+ ///
+ /// Requires a click action.
+ pub fn new_icon_message(
+ id: impl Into<ElementId>,
+ message: impl Into<SharedString>,
+ date_received: NaiveDateTime,
+ icon: Icon,
+ click_action: ClickHandler<V>,
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message.into(),
+ date_received,
+ ActorOrIcon::Icon(icon),
+ Some(click_action),
+ )
+ }
+
+ /// Creates a new notification with an actor slot
+ /// and a Call To Action row.
+ ///
+ /// Cannot take a click action due to required actions.
+ pub fn new_actor_with_actions(
+ id: impl Into<ElementId>,
+ message: impl Into<SharedString>,
+ date_received: NaiveDateTime,
+ actor: PublicActor,
+ actions: [NotificationAction<V>; 2],
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message.into(),
+ date_received,
+ ActorOrIcon::Actor(actor),
+ None,
+ )
+ .actions(actions)
+ }
+
+ /// Creates a new notification with an icon slot
+ /// and a Call To Action row.
+ ///
+ /// Cannot take a click action due to required actions.
+ pub fn new_icon_with_actions(
+ id: impl Into<ElementId>,
+ message: impl Into<SharedString>,
+ date_received: NaiveDateTime,
+ icon: Icon,
+ actions: [NotificationAction<V>; 2],
+ ) -> Self {
+ Self::new(
+ id.into(),
+ message.into(),
+ date_received,
+ ActorOrIcon::Icon(icon),
+ None,
+ )
+ .actions(actions)
+ }
+
+ fn on_click(mut self, handler: ClickHandler<V>) -> Self {
+ self.handlers.click = Some(handler);
+ self
+ }
+
+ pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
+ self.actions = Some(actions);
+ self
+ }
+
+ pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
+ self.meta = Some(meta);
+ self
+ }
+
+ fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
+ if let Some(meta) = &self.meta {
+ h_stack().children(
+ meta.items
+ .iter()
+ .map(|(icon, text, _)| {
+ let mut meta_el = div();
+ if let Some(icon) = icon {
+ meta_el = meta_el.child(IconElement::new(icon.clone()));
+ }
+ meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
+ })
+ .collect::<Vec<_>>(),
+ )
+ } else {
+ div()
+ }
+ }
+
+ fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
+ match &self.slot {
+ ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
+ ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
+ }
+ }
+
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ div()
+ .relative()
+ .id(self.id.clone())
+ .p_1()
+ .flex()
+ .flex_col()
+ .w_full()
+ .children(
+ Some(
+ div()
+ .absolute()
+ .left(px(3.0))
+ .top_3()
+ .z_index(2)
+ .child(UnreadIndicator::new()),
+ )
+ .filter(|_| self.unread),
+ )
+ .child(
+ v_stack()
+ .z_index(1)
+ .gap_1()
+ .w_full()
.child(
- List::new(static_new_notification_items())
- .header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
- .toggle(ToggleState::Toggled),
+ h_stack()
+ .w_full()
+ .gap_2()
+ .child(self.render_slot(cx))
+ .child(div().flex_1().child(Label::new(self.message.clone()))),
)
.child(
- List::new(static_read_notification_items())
- .header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
- .empty_message("No new notifications")
- .toggle(ToggleState::Toggled),
+ h_stack()
+ .justify_between()
+ .child(
+ h_stack()
+ .gap_1()
+ .child(
+ Label::new(naive_format_distance_from_now(
+ self.date_received,
+ true,
+ true,
+ ))
+ .color(LabelColor::Muted),
+ )
+ .child(self.render_meta_items(cx)),
+ )
+ .child(match (self.actions, self.action_taken) {
+ // Show nothing
+ (None, _) => div(),
+ // Show the taken_message
+ (Some(_), Some(action_taken)) => h_stack()
+ .children(action_taken.taken_message.0.map(|icon| {
+ IconElement::new(icon).color(crate::IconColor::Muted)
+ }))
+ .child(
+ Label::new(action_taken.taken_message.1.clone())
+ .color(LabelColor::Muted),
+ ),
+ // Show the actions
+ (Some(actions), None) => {
+ h_stack().children(actions.map(|action| match action.button {
+ ButtonOrIconButton::Button(button) => {
+ Component::render(button)
+ }
+ ButtonOrIconButton::IconButton(icon_button) => {
+ Component::render(icon_button)
+ }
+ }))
+ }
+ }),
),
)
}
}
+use chrono::NaiveDateTime;
+use gpui2::{px, Styled};
#[cfg(feature = "stories")]
pub use stories::*;
@@ -98,16 +98,14 @@ impl<V: 'static> Panel<V> {
v_stack()
.id(self.id.clone())
.flex_initial()
- .when(
- self.current_side == PanelSide::Left || self.current_side == PanelSide::Right,
- |this| this.h_full().w(current_size),
- )
- .when(self.current_side == PanelSide::Left, |this| this.border_r())
- .when(self.current_side == PanelSide::Right, |this| {
- this.border_l()
+ .map(|this| match self.current_side {
+ PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
+ PanelSide::Bottom => this,
})
- .when(self.current_side == PanelSide::Bottom, |this| {
- this.border_b().w_full().h(current_size)
+ .map(|this| match self.current_side {
+ PanelSide::Left => this.border_r(),
+ PanelSide::Right => this.border_l(),
+ PanelSide::Bottom => this.border_b().w_full().h(current_size),
})
.bg(cx.theme().colors().surface)
.border_color(cx.theme().colors().border)
@@ -1,6 +1,6 @@
use crate::prelude::*;
use crate::{Icon, IconColor, IconElement, Label, LabelColor};
-use gpui2::{black, red, Div, ElementId, Render, View, VisualContext};
+use gpui2::{red, Div, ElementId, Render, View, VisualContext};
#[derive(Component, Clone)]
pub struct Tab {
@@ -108,13 +108,13 @@ impl Tab {
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
- true => (
- cx.theme().colors().ghost_element,
+ false => (
+ cx.theme().colors().tab_inactive,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
- false => (
- cx.theme().colors().element,
+ true => (
+ cx.theme().colors().tab_active,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
@@ -127,7 +127,7 @@ impl Tab {
div()
.id(self.id.clone())
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
- .drag_over::<TabDragState>(|d| d.bg(black()))
+ .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().element_drop_target))
.on_drop(|_view, state: View<TabDragState>, cx| {
eprintln!("{:?}", state.read(cx));
})
@@ -144,7 +144,7 @@ impl Tab {
.px_1()
.flex()
.items_center()
- .gap_1()
+ .gap_1p5()
.children(has_fs_conflict.then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(crate::IconSize::Small)
@@ -27,6 +27,7 @@ impl TabBar {
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
div()
+ .group("tab_bar")
.id(self.id.clone())
.w_full()
.flex()
@@ -34,6 +35,7 @@ impl TabBar {
// Left Side
.child(
div()
+ .relative()
.px_1()
.flex()
.flex_none()
@@ -41,6 +43,7 @@ impl TabBar {
// Nav Buttons
.child(
div()
+ .right_0()
.flex()
.items_center()
.gap_px()
@@ -67,10 +70,15 @@ impl TabBar {
// Right Side
.child(
div()
+ // We only use absolute here since we don't
+ // have opacity or `hidden()` yet
+ .absolute()
+ .neg_top_7()
.px_1()
.flex()
.flex_none()
.gap_2()
+ .group_hover("tab_bar", |this| this.top_0())
// Nav Buttons
.child(
div()
@@ -2,6 +2,7 @@ mod avatar;
mod button;
mod details;
mod icon;
+mod indicator;
mod input;
mod label;
mod player;
@@ -12,6 +13,7 @@ pub use avatar::*;
pub use button::*;
pub use details::*;
pub use icon::*;
+pub use indicator::*;
pub use input::*;
pub use label::*;
pub use player::*;
@@ -26,23 +26,21 @@ pub enum IconColor {
impl IconColor {
pub fn color(self, cx: &WindowContext) -> Hsla {
- let theme_colors = cx.theme().colors();
-
match self {
- IconColor::Default => theme_colors.icon,
- IconColor::Muted => theme_colors.icon_muted,
- IconColor::Disabled => theme_colors.icon_disabled,
- IconColor::Placeholder => theme_colors.icon_placeholder,
- IconColor::Accent => theme_colors.icon_accent,
- IconColor::Error => gpui2::red(),
- IconColor::Warning => gpui2::red(),
- IconColor::Success => gpui2::red(),
- IconColor::Info => gpui2::red(),
+ IconColor::Default => cx.theme().colors().icon,
+ IconColor::Muted => cx.theme().colors().icon_muted,
+ IconColor::Disabled => cx.theme().colors().icon_disabled,
+ IconColor::Placeholder => cx.theme().colors().icon_placeholder,
+ IconColor::Accent => cx.theme().colors().icon_accent,
+ IconColor::Error => cx.theme().status().error,
+ IconColor::Warning => cx.theme().status().warning,
+ IconColor::Success => cx.theme().status().success,
+ IconColor::Info => cx.theme().status().info,
}
}
}
-#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
+#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
pub enum Icon {
Ai,
ArrowLeft,
@@ -51,6 +49,7 @@ pub enum Icon {
AudioOff,
AudioOn,
Bolt,
+ Check,
ChevronDown,
ChevronLeft,
ChevronRight,
@@ -69,7 +68,6 @@ pub enum Icon {
Folder,
FolderOpen,
FolderX,
- #[default]
Hash,
InlayHint,
MagicWand,
@@ -91,6 +89,11 @@ pub enum Icon {
XCircle,
Copilot,
Envelope,
+ Bell,
+ BellOff,
+ BellRing,
+ MailOpen,
+ AtSign,
}
impl Icon {
@@ -103,6 +106,7 @@ impl Icon {
Icon::AudioOff => "icons/speaker-off.svg",
Icon::AudioOn => "icons/speaker-loud.svg",
Icon::Bolt => "icons/bolt.svg",
+ Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
Icon::ChevronRight => "icons/chevron_right.svg",
@@ -142,6 +146,11 @@ impl Icon {
Icon::XCircle => "icons/error.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::Envelope => "icons/feedback.svg",
+ Icon::Bell => "icons/bell.svg",
+ Icon::BellOff => "icons/bell-off.svg",
+ Icon::BellRing => "icons/bell-ring.svg",
+ Icon::MailOpen => "icons/mail-open.svg",
+ Icon::AtSign => "icons/at-sign.svg",
}
}
}
@@ -0,0 +1,23 @@
+use gpui2::px;
+
+use crate::prelude::*;
+
+#[derive(Component)]
+pub struct UnreadIndicator;
+
+impl UnreadIndicator {
+ pub fn new() -> Self {
+ Self
+ }
+
+ fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ div()
+ .rounded_full()
+ .border_2()
+ .border_color(cx.theme().colors().surface)
+ .w(px(9.0))
+ .h(px(9.0))
+ .z_index(2)
+ .bg(cx.theme().status().info)
+ }
+}
@@ -94,14 +94,13 @@ impl Input {
.active(|style| style.bg(input_active_bg))
.flex()
.items_center()
- .child(
- div()
- .flex()
- .items_center()
- .text_sm()
- .when(self.value.is_empty(), |this| this.child(placeholder_label))
- .when(!self.value.is_empty(), |this| this.child(label)),
- )
+ .child(div().flex().items_center().text_sm().map(|this| {
+ if self.value.is_empty() {
+ this.child(placeholder_label)
+ } else {
+ this.child(label)
+ }
+ }))
}
}
@@ -21,11 +21,11 @@ impl LabelColor {
match self {
Self::Default => cx.theme().colors().text,
Self::Muted => cx.theme().colors().text_muted,
- Self::Created => gpui2::red(),
- Self::Modified => gpui2::red(),
- Self::Deleted => gpui2::red(),
+ Self::Created => cx.theme().status().created,
+ Self::Modified => cx.theme().status().modified,
+ Self::Deleted => cx.theme().status().deleted,
Self::Disabled => cx.theme().colors().text_disabled,
- Self::Hidden => gpui2::red(),
+ Self::Hidden => cx.theme().status().hidden,
Self::Placeholder => cx.theme().colors().text_placeholder,
Self::Accent => cx.theme().colors().text_accent,
}
@@ -79,8 +79,7 @@ impl Label {
this.relative().child(
div()
.absolute()
- .top_px()
- .my_auto()
+ .top_1_2()
.w_full()
.h_px()
.bg(LabelColor::Hidden.hsla(cx)),
@@ -23,6 +23,7 @@ mod elevation;
pub mod prelude;
pub mod settings;
mod static_data;
+pub mod utils;
pub use components::*;
pub use elements::*;
@@ -10,6 +10,24 @@ pub use theme2::ActiveTheme;
use gpui2::Hsla;
use strum::EnumIter;
+/// Represents a person with a Zed account's public profile.
+/// All data in this struct should be considered public.
+pub struct PublicActor {
+ pub username: SharedString,
+ pub avatar: SharedString,
+ pub is_contact: bool,
+}
+
+impl PublicActor {
+ pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
+ Self {
+ username: username.into(),
+ avatar: avatar.into(),
+ is_contact: false,
+ }
+ }
+}
+
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum FileSystemStatus {
#[default]
@@ -1,17 +1,20 @@
use std::path::PathBuf;
use std::str::FromStr;
+use std::sync::Arc;
+use chrono::DateTime;
use gpui2::{AppContext, ViewContext};
use rand::Rng;
use theme2::ActiveTheme;
use crate::{
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
- HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
- Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus,
- PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
+ HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
+ Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
+ PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
};
use crate::{HighlightedText, ListDetailsEntry};
+use crate::{ListItem, NotificationAction};
pub fn static_tabs_example() -> Vec<Tab> {
vec![
@@ -325,27 +328,227 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
]
}
-pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
+pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
vec![
- ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
- .meta("4 people in stream."),
- ListDetailsEntry::new("nathansobo accepted your contact request."),
+ Notification::new_icon_message(
+ "notif-1",
+ "You were mentioned in a note.",
+ DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z")
+ .unwrap()
+ .naive_local(),
+ Icon::AtSign,
+ Arc::new(|_, _| {}),
+ ),
+ Notification::new_actor_with_actions(
+ "notif-2",
+ "as-cii sent you a contact request.",
+ DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+ [
+ NotificationAction::new(
+ Button::new("Decline"),
+ "Decline Request",
+ (Some(Icon::XCircle), "Declined"),
+ ),
+ NotificationAction::new(
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ "Accept Request",
+ (Some(Icon::Check), "Accepted"),
+ ),
+ ],
+ ),
+ Notification::new_icon_message(
+ "notif-3",
+ "You were mentioned #design.",
+ DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ Icon::MessageBubbles,
+ Arc::new(|_, _| {}),
+ ),
+ Notification::new_actor_with_actions(
+ "notif-4",
+ "as-cii sent you a contact request.",
+ DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+ [
+ NotificationAction::new(
+ Button::new("Decline"),
+ "Decline Request",
+ (Some(Icon::XCircle), "Declined"),
+ ),
+ NotificationAction::new(
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ "Accept Request",
+ (Some(Icon::Check), "Accepted"),
+ ),
+ ],
+ ),
+ Notification::new_icon_message(
+ "notif-5",
+ "You were mentioned in a note.",
+ DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ Icon::AtSign,
+ Arc::new(|_, _| {}),
+ ),
+ Notification::new_actor_with_actions(
+ "notif-6",
+ "as-cii sent you a contact request.",
+ DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+ [
+ NotificationAction::new(
+ Button::new("Decline"),
+ "Decline Request",
+ (Some(Icon::XCircle), "Declined"),
+ ),
+ NotificationAction::new(
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ "Accept Request",
+ (Some(Icon::Check), "Accepted"),
+ ),
+ ],
+ ),
+ Notification::new_icon_message(
+ "notif-7",
+ "You were mentioned in a note.",
+ DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ Icon::AtSign,
+ Arc::new(|_, _| {}),
+ ),
+ Notification::new_actor_with_actions(
+ "notif-8",
+ "as-cii sent you a contact request.",
+ DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+ [
+ NotificationAction::new(
+ Button::new("Decline"),
+ "Decline Request",
+ (Some(Icon::XCircle), "Declined"),
+ ),
+ NotificationAction::new(
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ "Accept Request",
+ (Some(Icon::Check), "Accepted"),
+ ),
+ ],
+ ),
+ Notification::new_icon_message(
+ "notif-9",
+ "You were mentioned in a note.",
+ DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z")
+ .unwrap()
+ .naive_local(),
+ Icon::AtSign,
+ Arc::new(|_, _| {}),
+ ),
+ Notification::new_actor_with_actions(
+ "notif-10",
+ "as-cii sent you a contact request.",
+ DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
+ .unwrap()
+ .naive_local(),
+ PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
+ [
+ NotificationAction::new(
+ Button::new("Decline"),
+ "Decline Request",
+ (Some(Icon::XCircle), "Declined"),
+ ),
+ NotificationAction::new(
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ "Accept Request",
+ (Some(Icon::Check), "Accepted"),
+ ),
+ ],
+ ),
]
- .into_iter()
- .map(From::from)
- .collect()
}
-pub fn static_read_notification_items<V: 'static>() -> Vec<ListItem<V>> {
+pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
vec![
- ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
- Button::new("Decline"),
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- ]),
- ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
- .seen(true)
- .meta("This stream has ended."),
- ListDetailsEntry::new("as-cii accepted your contact request."),
+ ListItem::Header(ListSubHeader::new("New")),
+ ListItem::Details(
+ ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
+ .meta("4 people in stream."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "nathansobo accepted your contact request.",
+ )),
+ ListItem::Header(ListSubHeader::new("Earlier")),
+ ListItem::Details(
+ ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
+ Button::new("Decline"),
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ ]),
+ ),
+ ListItem::Details(
+ ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "as-cii accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "osiewicz accepted your contact request.",
+ )),
+ ListItem::Details(ListDetailsEntry::new(
+ "ConradIrwin accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "nathansobo accepted your contact request.",
+ )),
+ ListItem::Header(ListSubHeader::new("Earlier")),
+ ListItem::Details(
+ ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
+ Button::new("Decline"),
+ Button::new("Accept").variant(crate::ButtonVariant::Filled),
+ ]),
+ ),
+ ListItem::Details(
+ ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "as-cii accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
+ ),
+ ListItem::Details(ListDetailsEntry::new(
+ "osiewicz accepted your contact request.",
+ )),
+ ListItem::Details(ListDetailsEntry::new(
+ "ConradIrwin accepted your contact request.",
+ )),
+ ListItem::Details(
+ ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ),
]
.into_iter()
.map(From::from)
@@ -0,0 +1,3 @@
+mod format_distance;
+
+pub use format_distance::*;
@@ -0,0 +1,231 @@
+use chrono::NaiveDateTime;
+
+/// Calculates the distance in seconds between two NaiveDateTime objects.
+/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
+///
+/// ## Arguments
+///
+/// * `date` - A NaiveDateTime object representing the date of interest
+/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
+fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
+ let duration = date.signed_duration_since(base_date);
+ -duration.num_seconds()
+}
+
+/// Generates a string describing the time distance between two dates in a human-readable way.
+fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
+ let suffix = if distance < 0 { " from now" } else { " ago" };
+
+ let d = distance.abs();
+
+ let minutes = d / 60;
+ let hours = d / 3600;
+ let days = d / 86400;
+ let months = d / 2592000;
+ let years = d / 31536000;
+
+ let string = if d < 5 && include_seconds {
+ "less than 5 seconds".to_string()
+ } else if d < 10 && include_seconds {
+ "less than 10 seconds".to_string()
+ } else if d < 20 && include_seconds {
+ "less than 20 seconds".to_string()
+ } else if d < 40 && include_seconds {
+ "half a minute".to_string()
+ } else if d < 60 && include_seconds {
+ "less than a minute".to_string()
+ } else if d < 90 && include_seconds {
+ "1 minute".to_string()
+ } else if d < 30 {
+ "less than a minute".to_string()
+ } else if d < 90 {
+ "1 minute".to_string()
+ } else if d < 2700 {
+ format!("{} minutes", minutes)
+ } else if d < 5400 {
+ "about 1 hour".to_string()
+ } else if d < 86400 {
+ format!("about {} hours", hours)
+ } else if d < 172800 {
+ "1 day".to_string()
+ } else if d < 2592000 {
+ format!("{} days", days)
+ } else if d < 5184000 {
+ "about 1 month".to_string()
+ } else if d < 7776000 {
+ "about 2 months".to_string()
+ } else if d < 31540000 {
+ format!("{} months", months)
+ } else if d < 39425000 {
+ "about 1 year".to_string()
+ } else if d < 55195000 {
+ "over 1 year".to_string()
+ } else if d < 63080000 {
+ "almost 2 years".to_string()
+ } else {
+ let years = d / 31536000;
+ let remaining_months = (d % 31536000) / 2592000;
+
+ if remaining_months < 3 {
+ format!("about {} years", years)
+ } else if remaining_months < 9 {
+ format!("over {} years", years)
+ } else {
+ format!("almost {} years", years + 1)
+ }
+ };
+
+ if add_suffix {
+ return format!("{}{}", string, suffix);
+ } else {
+ string
+ }
+}
+
+/// Get the time difference between two dates into a relative human readable string.
+///
+/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
+///
+/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
+///
+/// # Arguments
+///
+/// * `date` - The NaiveDateTime to compare.
+/// * `base_date` - The NaiveDateTime to compare against.
+/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
+/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
+///
+/// # Example
+///
+/// ```rust
+/// use chrono::DateTime;
+/// use ui2::utils::naive_format_distance;
+///
+/// fn time_between_moon_landings() -> String {
+/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
+/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local();
+/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false))
+/// }
+/// ```
+///
+/// Output: `"There was about 3 years between the first and last crewed moon landings."`
+pub fn naive_format_distance(
+ date: NaiveDateTime,
+ base_date: NaiveDateTime,
+ include_seconds: bool,
+ add_suffix: bool,
+) -> String {
+ let distance = distance_in_seconds(date, base_date);
+
+ distance_string(distance, include_seconds, add_suffix)
+}
+
+/// Get the time difference between a date and now as relative human readable string.
+///
+/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
+///
+/// # Arguments
+///
+/// * `datetime` - The NaiveDateTime to compare with the current time.
+/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
+/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
+///
+/// # Example
+///
+/// ```rust
+/// use chrono::DateTime;
+/// use ui2::utils::naive_format_distance_from_now;
+///
+/// fn time_since_first_moon_landing() -> String {
+/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
+/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false))
+/// }
+/// ```
+///
+/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
+pub fn naive_format_distance_from_now(
+ datetime: NaiveDateTime,
+ include_seconds: bool,
+ add_suffix: bool,
+) -> String {
+ let now = chrono::offset::Local::now().naive_local();
+
+ naive_format_distance(datetime, now, include_seconds, add_suffix)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use chrono::NaiveDateTime;
+
+ #[test]
+ fn test_naive_format_distance() {
+ let date =
+ NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
+ let base_date =
+ NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
+
+ assert_eq!(
+ "about 2 hours",
+ naive_format_distance(date, base_date, false, false)
+ );
+ }
+
+ #[test]
+ fn test_naive_format_distance_with_suffix() {
+ let date =
+ NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
+ let base_date =
+ NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
+
+ assert_eq!(
+ "about 2 hours from now",
+ naive_format_distance(date, base_date, false, true)
+ );
+ }
+
+ #[test]
+ fn test_naive_format_distance_from_now() {
+ let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
+ .expect("Invalid NaiveDateTime for date");
+
+ assert_eq!(
+ "over 54 years ago",
+ naive_format_distance_from_now(date, false, true)
+ );
+ }
+
+ #[test]
+ fn test_naive_format_distance_string() {
+ assert_eq!(distance_string(3, false, false), "less than a minute");
+ assert_eq!(distance_string(7, false, false), "less than a minute");
+ assert_eq!(distance_string(13, false, false), "less than a minute");
+ assert_eq!(distance_string(21, false, false), "less than a minute");
+ assert_eq!(distance_string(45, false, false), "1 minute");
+ assert_eq!(distance_string(61, false, false), "1 minute");
+ assert_eq!(distance_string(1920, false, false), "32 minutes");
+ assert_eq!(distance_string(3902, false, false), "about 1 hour");
+ assert_eq!(distance_string(18002, false, false), "about 5 hours");
+ assert_eq!(distance_string(86470, false, false), "1 day");
+ assert_eq!(distance_string(345880, false, false), "4 days");
+ assert_eq!(distance_string(2764800, false, false), "about 1 month");
+ assert_eq!(distance_string(5184000, false, false), "about 2 months");
+ assert_eq!(distance_string(10368000, false, false), "4 months");
+ assert_eq!(distance_string(34694000, false, false), "about 1 year");
+ assert_eq!(distance_string(47310000, false, false), "over 1 year");
+ assert_eq!(distance_string(61503000, false, false), "almost 2 years");
+ assert_eq!(distance_string(160854000, false, false), "about 5 years");
+ assert_eq!(distance_string(236550000, false, false), "over 7 years");
+ assert_eq!(distance_string(249166000, false, false), "almost 8 years");
+ }
+
+ #[test]
+ fn test_naive_format_distance_string_include_seconds() {
+ assert_eq!(distance_string(3, true, false), "less than 5 seconds");
+ assert_eq!(distance_string(7, true, false), "less than 10 seconds");
+ assert_eq!(distance_string(13, true, false), "less than 20 seconds");
+ assert_eq!(distance_string(21, true, false), "half a minute");
+ assert_eq!(distance_string(45, true, false), "less than a minute");
+ assert_eq!(distance_string(61, true, false), "1 minute");
+ }
+}