diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 550eda882b..c1d2457ed4 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -6,26 +6,27 @@ jobs: discord_release: runs-on: ubuntu-latest steps: - - name: Get release URL - id: get-release-url - run: | - if [ "${{ github.event.release.prerelease }}" == "true" ]; then - URL="https://zed.dev/releases/preview/latest" - else - URL="https://zed.dev/releases/stable/latest" - fi - echo "::set-output name=URL::$URL" - - name: Get content - uses: 2428392/gh-truncate-string-action@v1.2.0 - id: get-content - with: - stringToTruncate: | - 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! + - name: Get release URL + id: get-release-url + run: | + if [ "${{ github.event.release.prerelease }}" == "true" ]; then + URL="https://zed.dev/releases/preview/latest" + else + URL="https://zed.dev/releases/stable/latest" + fi + echo "::set-output name=URL::$URL" + - name: Get content + uses: 2428392/gh-truncate-string-action@v1.3.0 + id: get-content + with: + stringToTruncate: | + 📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released! - ${{ github.event.release.body }} - maxLength: 2000 - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v5.3.0 - with: - webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} - content: ${{ steps.get-content.outputs.string }} + ${{ github.event.release.body }} + maxLength: 2000 + truncationSymbol: "..." + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v5.3.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + content: ${{ steps.get-content.outputs.string }} diff --git a/Cargo.lock b/Cargo.lock index 161e1e4b14..f91f574b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1603,7 +1603,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.27.0" +version = "0.28.0" dependencies = [ "anyhow", "async-trait", diff --git a/assets/icons/at-sign.svg b/assets/icons/at-sign.svg new file mode 100644 index 0000000000..5adac38f62 --- /dev/null +++ b/assets/icons/at-sign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell-off.svg b/assets/icons/bell-off.svg new file mode 100644 index 0000000000..db1021f2d3 --- /dev/null +++ b/assets/icons/bell-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell-ring.svg b/assets/icons/bell-ring.svg new file mode 100644 index 0000000000..da51fdc5be --- /dev/null +++ b/assets/icons/bell-ring.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index ea1c6dd42e..4c7d5472db 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,8 +1 @@ - - - + \ No newline at end of file diff --git a/assets/icons/mail-open.svg b/assets/icons/mail-open.svg new file mode 100644 index 0000000000..b63915bd73 --- /dev/null +++ b/assets/icons/mail-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/ai/src/test.rs b/crates/ai/src/test.rs index d4165f3cca..3f331da117 100644 --- a/crates/ai/src/test.rs +++ b/crates/ai/src/test.rs @@ -153,10 +153,17 @@ impl FakeCompletionProvider { pub fn send_completion(&self, completion: impl Into) { let mut tx = self.last_completion_tx.lock(); - tx.as_mut().unwrap().try_send(completion.into()).unwrap(); + + println!("COMPLETION TX: {:?}", &tx); + + let a = tx.as_mut().unwrap(); + a.try_send(completion.into()).unwrap(); + + // tx.as_mut().unwrap().try_send(completion.into()).unwrap(); } pub fn finish_completion(&self) { + println!("FINISHING COMPLETION"); self.last_completion_tx.lock().take().unwrap(); } } @@ -181,8 +188,10 @@ impl CompletionProvider for FakeCompletionProvider { &self, _prompt: Box, ) -> BoxFuture<'static, anyhow::Result>>> { + println!("COMPLETING"); let (tx, rx) = mpsc::channel(1); *self.last_completion_tx.lock() = Some(tx); + println!("TX: {:?}", *self.last_completion_tx.lock()); async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed() } fn box_clone(&self) -> Box { diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 03eb3c238f..6ab96093a7 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -142,7 +142,7 @@ pub struct AssistantPanel { zoomed: bool, has_focus: bool, toolbar: ViewHandle, - completion_provider: Box, + completion_provider: Arc, api_key_editor: Option>, languages: Arc, fs: Arc, @@ -204,7 +204,7 @@ impl AssistantPanel { let semantic_index = SemanticIndex::global(cx); // Defaulting currently to GPT4, allow for this to be set via config. - let completion_provider = Box::new(OpenAICompletionProvider::new( + let completion_provider = Arc::new(OpenAICompletionProvider::new( "gpt-4", cx.background().clone(), )); @@ -259,7 +259,13 @@ impl AssistantPanel { cx: &mut ViewContext, ) { let this = if let Some(this) = workspace.panel::(cx) { - if this.update(cx, |assistant, _| assistant.has_credentials()) { + if this.update(cx, |assistant, cx| { + if !assistant.has_credentials() { + assistant.load_credentials(cx); + }; + + assistant.has_credentials() + }) { this } else { workspace.focus_panel::(cx); @@ -320,13 +326,10 @@ impl AssistantPanel { }; let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let provider = Arc::new(OpenAICompletionProvider::new( - "gpt-4", - cx.background().clone(), - )); + let provider = self.completion_provider.clone(); // Retrieve Credentials Authenticates the Provider - // provider.retrieve_credentials(cx); + provider.retrieve_credentials(cx); let codegen = cx.add_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) @@ -1439,7 +1442,7 @@ struct Conversation { pending_save: Task>, path: Option, _subscriptions: Vec, - completion_provider: Box, + completion_provider: Arc, } impl Entity for Conversation { @@ -1450,7 +1453,7 @@ impl Conversation { fn new( language_registry: Arc, cx: &mut ModelContext, - completion_provider: Box, + completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { @@ -1544,7 +1547,7 @@ impl Conversation { None => Some(Uuid::new_v4().to_string()), }; let model = saved_conversation.model; - let completion_provider: Box = Box::new( + let completion_provider: Arc = Arc::new( OpenAICompletionProvider::new(model.full_name(), cx.background().clone()), ); completion_provider.retrieve_credentials(cx); @@ -2201,7 +2204,7 @@ struct ConversationEditor { impl ConversationEditor { fn new( - completion_provider: Box, + completion_provider: Arc, language_registry: Arc, fs: Arc, workspace: WeakViewHandle, @@ -3406,7 +3409,7 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3535,7 +3538,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3633,7 +3636,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); @@ -3716,7 +3719,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); init(cx); let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Box::new(FakeCompletionProvider::new()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index f62c91fcb7..25c9deef7f 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -367,6 +367,8 @@ fn strip_invalid_spans_from_codeblock( #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; @@ -437,6 +439,7 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); + println!("CHUNK: {:?}", &chunk); provider.send_completion(chunk); new_text = suffix; deterministic.run_until_parked(); @@ -569,6 +572,7 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); + println!("{:?}", &chunk); provider.send_completion(chunk); new_text = suffix; deterministic.run_until_parked(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 987c295407..dea6e09245 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.27.0" +version = "0.28.0" publish = false [[bin]] diff --git a/crates/db2/src/db2.rs b/crates/db2/src/db2.rs index fe79dfbb0c..e052d59d12 100644 --- a/crates/db2/src/db2.rs +++ b/crates/db2/src/db2.rs @@ -190,138 +190,142 @@ where .detach() } -// #[cfg(test)] -// mod tests { -// use std::thread; +#[cfg(test)] +mod tests { + use std::thread; -// use sqlez::domain::Domain; -// use sqlez_macros::sql; -// use tempdir::TempDir; + use sqlez::domain::Domain; + use sqlez_macros::sql; + use tempdir::TempDir; -// use crate::open_db; + use crate::open_db; -// // Test bad migration panics -// #[gpui::test] -// #[should_panic] -// async fn test_bad_migration_panics() { -// enum BadDB {} + // 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" -// } + 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);), -// ] -// } -// } + 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::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// } + let tempdir = TempDir::new("DbTests").unwrap(); + let _bad_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + } -// /// Test that DB exists but corrupted (causing recreate) -// #[gpui::test] -// async fn test_db_corruption() { -// enum CorruptedDB {} + /// Test that DB exists but corrupted (causing recreate) + #[gpui2::test] + async fn test_db_corruption(cx: &mut gpui2::TestAppContext) { + cx.executor().allow_parking(); -// impl Domain for CorruptedDB { -// fn name() -> &'static str { -// "db_tests" -// } + enum CorruptedDB {} -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test(value);)] -// } -// } + impl Domain for CorruptedDB { + fn name() -> &'static str { + "db_tests" + } -// enum GoodDB {} + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } + } -// impl Domain for GoodDB { -// fn name() -> &'static str { -// "db_tests" //Notice same name -// } + enum GoodDB {} -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test2(value);)] //But different migration -// } -// } + impl Domain for GoodDB { + fn name() -> &'static str { + "db_tests" //Notice same name + } -// let tempdir = TempDir::new("DbTests").unwrap(); -// { -// let corrupt_db = -// open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!(corrupt_db.persistent()); -// } + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } + } -// let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!( -// good_db.select_row::("SELECT * FROM test2").unwrap()() -// .unwrap() -// .is_none() -// ); -// } + let tempdir = TempDir::new("DbTests").unwrap(); + { + let corrupt_db = + open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(corrupt_db.persistent()); + } -// /// Test that DB exists but corrupted (causing recreate) -// #[gpui::test(iterations = 30)] -// async fn test_simultaneous_db_corruption() { -// enum CorruptedDB {} + let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!( + good_db.select_row::("SELECT * FROM test2").unwrap()() + .unwrap() + .is_none() + ); + } -// impl Domain for CorruptedDB { -// fn name() -> &'static str { -// "db_tests" -// } + /// 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(); -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test(value);)] -// } -// } + enum CorruptedDB {} -// enum GoodDB {} + impl Domain for CorruptedDB { + fn name() -> &'static str { + "db_tests" + } -// impl Domain for GoodDB { -// fn name() -> &'static str { -// "db_tests" //Notice same name -// } + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } + } -// fn migrations() -> &'static [&'static str] { -// &[sql!(CREATE TABLE test2(value);)] //But different migration -// } -// } + enum GoodDB {} -// let tempdir = TempDir::new("DbTests").unwrap(); -// { -// // Setup the bad database -// let corrupt_db = -// open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; -// assert!(corrupt_db.persistent()); -// } + impl Domain for GoodDB { + fn name() -> &'static str { + "db_tests" //Notice same name + } -// // 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::( -// tmp_path.as_path(), -// &util::channel::ReleaseChannel::Dev, -// )); -// assert!( -// good_db.select_row::("SELECT * FROM test2").unwrap()() -// .unwrap() -// .is_none() -// ); -// }); + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } + } -// guards.push(guard); -// } + let tempdir = TempDir::new("DbTests").unwrap(); + { + // Setup the bad database + let corrupt_db = + open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(corrupt_db.persistent()); + } -// for guard in guards.into_iter() { -// assert!(guard.join().is_ok()); -// } -// } -// } + // 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::( + tmp_path.as_path(), + &util::channel::ReleaseChannel::Dev, + )); + assert!( + good_db.select_row::("SELECT * FROM test2").unwrap()() + .unwrap() + .is_none() + ); + }); + + guards.push(guard); + } + + for guard in guards.into_iter() { + assert!(guard.join().is_ok()); + } + } +} diff --git a/crates/db2/src/kvp.rs b/crates/db2/src/kvp.rs index 254d91689d..b4445e3586 100644 --- a/crates/db2/src/kvp.rs +++ b/crates/db2/src/kvp.rs @@ -31,32 +31,32 @@ impl KeyValueStore { } } -// #[cfg(test)] -// mod tests { -// use crate::kvp::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); + #[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); + 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".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-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.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); -// } -// } + db.delete_kvp("key-1".to_string()).await.unwrap(); + assert_eq!(db.read_kvp("key-1").unwrap(), None); + } +} diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 4890b79a9a..a92dbd6ff9 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -198,14 +198,19 @@ impl AnyElement { pub trait Component { fn render(self) -> AnyElement; - fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + U: Component, + { + f(self) + } + + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self where Self: Sized, { - if condition { - self = then(self); - } - self + self.map(|this| if condition { then(this) } else { this }) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 055c31af16..a75c2ef319 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -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; @@ -53,6 +54,7 @@ pub enum DispatchPhase { Capture, } +type AnyObserver = Box bool + 'static>; type AnyListener = Box; type AnyKeyListener = Box< dyn Fn( @@ -185,6 +187,10 @@ pub struct Window { default_prevented: bool, mouse_position: Point, 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>, @@ -203,16 +209,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(); } @@ -256,6 +280,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, @@ -525,6 +553,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. @@ -1717,6 +1762,28 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + pub fn observe_window_bounds( + &mut self, + mut callback: impl FnMut(&mut V, &mut ViewContext) + '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) + '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) + 'static, diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 0ac84de392..4e2c439db0 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -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() }, diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs index 02c93a2e98..ee69eed612 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/colors.rs @@ -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, diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 335b6801d5..8b7137683c 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -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(), ), - ("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(), - ), - ("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.delimiter".into(), - gpui2::rgba(0x4d4f52ff).into(), + neutral().light().step_11().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()), + ( + "punctuation.list_marker".into(), + blue().light().step_11().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()), + ( + "string.special.symbol".into(), + red().light().step_7().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(), } } } diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 34727eaf89..88dcbd1286 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -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 { diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 1668592a38..50a86ff256 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -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), + // TODO: This should be a button + Button(Label), + Text(Label), +} + #[derive(Component)] pub struct ListHeader { label: SharedString, left_icon: Option, + meta: Option, 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) -> 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(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { 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 ListDetailsEntry { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { 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(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - 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 List { 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) diff --git a/crates/ui2/src/components/notifications_panel.rs b/crates/ui2/src/components/notifications_panel.rs index 10b0e07af6..74f015ac06 100644 --- a/crates/ui2/src/components/notifications_panel.rs +++ b/crates/ui2/src/components/notifications_panel.rs @@ -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( - List::new(static_new_notification_items()) - .header(ListHeader::new("NEW").toggle(ToggleState::Toggled)) - .toggle(ToggleState::Toggled), + 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 { + Button(Button), + IconButton(IconButton), +} + +impl From> for ButtonOrIconButton { + fn from(value: Button) -> Self { + Self::Button(value) + } +} + +impl From> for ButtonOrIconButton { + fn from(value: IconButton) -> Self { + Self::IconButton(value) + } +} + +pub struct NotificationAction { + button: ButtonOrIconButton, + 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, SharedString), +} + +impl NotificationAction { + pub fn new( + button: impl Into>, + tooltip: impl Into, + (icon, taken_message): (Option, impl Into), + ) -> Self { + Self { + button: button.into(), + tooltip: tooltip.into(), + taken_message: (icon, taken_message.into()), + } + } +} + +pub enum ActorOrIcon { + Actor(PublicActor), + Icon(Icon), +} + +pub struct NotificationMeta { + items: Vec<(Option, SharedString, Option>)>, +} + +struct NotificationHandlers { + click: Option>, +} + +impl Default for NotificationHandlers { + fn default() -> Self { + Self { click: None } + } +} + +#[derive(Component)] +pub struct Notification { + id: ElementId, + slot: ActorOrIcon, + message: SharedString, + date_received: NaiveDateTime, + meta: Option>, + actions: Option<[NotificationAction; 2]>, + unread: bool, + new: bool, + action_taken: Option>, + handlers: NotificationHandlers, +} + +impl Notification { + fn new( + id: ElementId, + message: SharedString, + date_received: NaiveDateTime, + slot: ActorOrIcon, + click_action: Option>, + ) -> 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, + message: impl Into, + date_received: NaiveDateTime, + actor: PublicActor, + click_action: ClickHandler, + ) -> 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, + message: impl Into, + date_received: NaiveDateTime, + icon: Icon, + click_action: ClickHandler, + ) -> 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, + message: impl Into, + date_received: NaiveDateTime, + actor: PublicActor, + actions: [NotificationAction; 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, + message: impl Into, + date_received: NaiveDateTime, + icon: Icon, + actions: [NotificationAction; 2], + ) -> Self { + Self::new( + id.into(), + message.into(), + date_received, + ActorOrIcon::Icon(icon), + None, + ) + .actions(actions) + } + + fn on_click(mut self, handler: ClickHandler) -> Self { + self.handlers.click = Some(handler); + self + } + + pub fn actions(mut self, actions: [NotificationAction; 2]) -> Self { + self.actions = Some(actions); + self + } + + pub fn meta(mut self, meta: NotificationMeta) -> Self { + self.meta = Some(meta); + self + } + + fn render_meta_items(&self, cx: &mut ViewContext) -> impl Component { + 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::>(), + ) + } else { + div() + } + } + + fn render_slot(&self, cx: &mut ViewContext) -> impl Component { + 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) -> impl Component { + 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( + 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::*; diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index c4a9ac5111..5d941eb50e 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -98,16 +98,14 @@ impl Panel { 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) diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index 8572a22262..e8b0ee3be5 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -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::(|d| d.bg(black())) + .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) .on_drop(|_view, state: View, 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) diff --git a/crates/ui2/src/components/tab_bar.rs b/crates/ui2/src/components/tab_bar.rs index 550105b98e..bb7fca1153 100644 --- a/crates/ui2/src/components/tab_bar.rs +++ b/crates/ui2/src/components/tab_bar.rs @@ -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() diff --git a/crates/ui2/src/elements.rs b/crates/ui2/src/elements.rs index c60902ae98..dfff2761a7 100644 --- a/crates/ui2/src/elements.rs +++ b/crates/ui2/src/elements.rs @@ -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::*; diff --git a/crates/ui2/src/elements/icon.rs b/crates/ui2/src/elements/icon.rs index 8cc62f4a8d..5885d76101 100644 --- a/crates/ui2/src/elements/icon.rs +++ b/crates/ui2/src/elements/icon.rs @@ -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", } } } diff --git a/crates/ui2/src/elements/indicator.rs b/crates/ui2/src/elements/indicator.rs new file mode 100644 index 0000000000..1f6e00e621 --- /dev/null +++ b/crates/ui2/src/elements/indicator.rs @@ -0,0 +1,23 @@ +use gpui2::px; + +use crate::prelude::*; + +#[derive(Component)] +pub struct UnreadIndicator; + +impl UnreadIndicator { + pub fn new() -> Self { + Self + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + 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) + } +} diff --git a/crates/ui2/src/elements/input.rs b/crates/ui2/src/elements/input.rs index 3f82512b84..2884470ce2 100644 --- a/crates/ui2/src/elements/input.rs +++ b/crates/ui2/src/elements/input.rs @@ -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) + } + })) } } diff --git a/crates/ui2/src/elements/label.rs b/crates/ui2/src/elements/label.rs index dcc28a3319..d1d4d6630c 100644 --- a/crates/ui2/src/elements/label.rs +++ b/crates/ui2/src/elements/label.rs @@ -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)), diff --git a/crates/ui2/src/lib.rs b/crates/ui2/src/lib.rs index c1da5e410d..5d0a57c6d9 100644 --- a/crates/ui2/src/lib.rs +++ b/crates/ui2/src/lib.rs @@ -23,6 +23,7 @@ mod elevation; pub mod prelude; pub mod settings; mod static_data; +pub mod utils; pub use components::*; pub use elements::*; diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index c3f530d70c..fbb7ccc528 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -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, avatar: impl Into) -> 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] diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 7062c81954..68f625c75d 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -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 { vec![ @@ -325,27 +328,227 @@ pub fn static_players_with_call_status() -> Vec { ] } -pub fn static_new_notification_items() -> Vec> { +pub fn static_new_notification_items_2() -> Vec> { 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() -> Vec> { +pub fn static_new_notification_items() -> Vec> { 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) diff --git a/crates/ui2/src/utils.rs b/crates/ui2/src/utils.rs new file mode 100644 index 0000000000..573a1333ef --- /dev/null +++ b/crates/ui2/src/utils.rs @@ -0,0 +1,3 @@ +mod format_distance; + +pub use format_distance::*; diff --git a/crates/ui2/src/utils/format_distance.rs b/crates/ui2/src/utils/format_distance.rs new file mode 100644 index 0000000000..8a33619910 --- /dev/null +++ b/crates/ui2/src/utils/format_distance.rs @@ -0,0 +1,173 @@ +use chrono::NaiveDateTime; + +fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { + let duration = date.signed_duration_since(base_date); + -duration.num_seconds() +} + +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 + } +} + +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) +} + +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"); + } +}