diff --git a/assets/icons/at-sign.svg b/assets/icons/at-sign.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5adac38f62fb661e11d0f2e8f4ea17c94683aa5f
--- /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 0000000000000000000000000000000000000000..db1021f2d3e5b3e05e75872c52a4d415d93fd2c9
--- /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 0000000000000000000000000000000000000000..da51fdc5bed44faa82038e86de3b33b2733e17a4
--- /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 ea1c6dd42e8821b632f6de97d143a7b9f4b97fd2..4c7d5472db3744172ec69069865bd7db1726be22 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 0000000000000000000000000000000000000000..b63915bd73c400c71966b94ec6e082bdd7faf9b9
--- /dev/null
+++ b/assets/icons/mail-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/db2/src/db2.rs b/crates/db2/src/db2.rs
index fe79dfbb0c2662c8583ee11b04ddad590fa178f7..e052d59d124c031f6afe0f9db3bc19624aabbead 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;
-
-// 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::(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::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// assert!(corrupt_db.persistent());
-// }
-
-// let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-// assert!(
-// good_db.select_row::("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::(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::(
-// 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());
-// }
-// }
-// }
+#[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::(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::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(corrupt_db.persistent());
+ }
+
+ let good_db = open_db::(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ assert!(
+ good_db.select_row::("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::(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::(
+ 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 254d91689d607ad8d9b2b5f844d73f50d7919ea7..b4445e358672c40819b4579839fa5664e5d8d8a4 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;
-
-// #[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);
+ }
+}
diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs
index 4890b79a9acc811ea72816a4a01dbd45634e1667..a92dbd6ff94a98165f5b200bc4eadedb2471bf5a 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,
{
- 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 })
}
}
diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs
index 2897c9f38e010d766b4a0ed87e544f4578cf58d6..37b4d643451f1387246700d316fd2ec018780d8e 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;
@@ -56,6 +57,7 @@ pub enum DispatchPhase {
Capture,
}
+type AnyObserver = Box bool + 'static>;
type AnyListener = Box;
type AnyKeyListener = Box<
dyn Fn(
@@ -187,6 +189,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