mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 11:29:25 +00:00
Merge remote-tracking branch 'origin/main' into gpui2-image-reborrow
This commit is contained in:
commit
1dd20d4c0a
29 changed files with 1489 additions and 530 deletions
1
assets/icons/at-sign.svg
Normal file
1
assets/icons/at-sign.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 300 B |
1
assets/icons/bell-off.svg
Normal file
1
assets/icons/bell-off.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 387 B |
1
assets/icons/bell-ring.svg
Normal file
1
assets/icons/bell-ring.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 382 B |
|
@ -1,8 +1 @@
|
||||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<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"><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"/></svg>
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M8.60124 1.25086C8.60124 1.75459 8.26278 2.17927 7.80087 2.30989C10.1459 2.4647 12 4.41582 12 6.79999V10.25C12 11.0563 12.0329 11.7074 12.7236 12.0528C12.931 12.1565 13.0399 12.3892 12.9866 12.6149C12.9333 12.8406 12.7319 13 12.5 13H8.16144C8.36904 13.1832 8.49997 13.4513 8.49997 13.75C8.49997 14.3023 8.05226 14.75 7.49997 14.75C6.94769 14.75 6.49997 14.3023 6.49997 13.75C6.49997 13.4513 6.63091 13.1832 6.83851 13H2.49999C2.2681 13 2.06664 12.8406 2.01336 12.6149C1.96009 12.3892 2.06897 12.1565 2.27638 12.0528C2.96708 11.7074 2.99999 11.0563 2.99999 10.25V6.79999C2.99999 4.41537 4.85481 2.46396 7.20042 2.3098C6.73867 2.17908 6.40036 1.75448 6.40036 1.25086C6.40036 0.643104 6.89304 0.150421 7.5008 0.150421C8.10855 0.150421 8.60124 0.643104 8.60124 1.25086ZM7.49999 3.29999C5.56699 3.29999 3.99999 4.86699 3.99999 6.79999V10.25L4.00002 10.3009C4.0005 10.7463 4.00121 11.4084 3.69929 12H11.3007C10.9988 11.4084 10.9995 10.7463 11 10.3009L11 10.25V6.79999C11 4.86699 9.43299 3.29999 7.49999 3.29999Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 309 B |
1
assets/icons/mail-open.svg
Normal file
1
assets/icons/mail-open.svg
Normal file
|
@ -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>
|
After Width: | Height: | Size: 390 B |
|
@ -190,138 +190,142 @@ where
|
||||||
.detach()
|
.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod tests {
|
mod tests {
|
||||||
// use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
// use sqlez::domain::Domain;
|
use sqlez::domain::Domain;
|
||||||
// use sqlez_macros::sql;
|
use sqlez_macros::sql;
|
||||||
// use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
// use crate::open_db;
|
use crate::open_db;
|
||||||
|
|
||||||
// // Test bad migration panics
|
// Test bad migration panics
|
||||||
// #[gpui::test]
|
#[gpui2::test]
|
||||||
// #[should_panic]
|
#[should_panic]
|
||||||
// async fn test_bad_migration_panics() {
|
async fn test_bad_migration_panics() {
|
||||||
// enum BadDB {}
|
enum BadDB {}
|
||||||
|
|
||||||
// impl Domain for BadDB {
|
impl Domain for BadDB {
|
||||||
// fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
// "db_tests"
|
"db_tests"
|
||||||
// }
|
}
|
||||||
|
|
||||||
// fn migrations() -> &'static [&'static str] {
|
fn migrations() -> &'static [&'static str] {
|
||||||
// &[
|
&[
|
||||||
// sql!(CREATE TABLE test(value);),
|
sql!(CREATE TABLE test(value);),
|
||||||
// // failure because test already exists
|
// failure because test already exists
|
||||||
// sql!(CREATE TABLE test(value);),
|
sql!(CREATE TABLE test(value);),
|
||||||
// ]
|
]
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
let tempdir = TempDir::new("DbTests").unwrap();
|
||||||
// let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /// Test that DB exists but corrupted (causing recreate)
|
/// Test that DB exists but corrupted (causing recreate)
|
||||||
// #[gpui::test]
|
#[gpui2::test]
|
||||||
// async fn test_db_corruption() {
|
async fn test_db_corruption(cx: &mut gpui2::TestAppContext) {
|
||||||
// enum CorruptedDB {}
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
// impl Domain for CorruptedDB {
|
enum CorruptedDB {}
|
||||||
// fn name() -> &'static str {
|
|
||||||
// "db_tests"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn migrations() -> &'static [&'static str] {
|
impl Domain for CorruptedDB {
|
||||||
// &[sql!(CREATE TABLE test(value);)]
|
fn name() -> &'static str {
|
||||||
// }
|
"db_tests"
|
||||||
// }
|
}
|
||||||
|
|
||||||
// enum GoodDB {}
|
fn migrations() -> &'static [&'static str] {
|
||||||
|
&[sql!(CREATE TABLE test(value);)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// impl Domain for GoodDB {
|
enum GoodDB {}
|
||||||
// fn name() -> &'static str {
|
|
||||||
// "db_tests" //Notice same name
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn migrations() -> &'static [&'static str] {
|
impl Domain for GoodDB {
|
||||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
fn name() -> &'static str {
|
||||||
// }
|
"db_tests" //Notice same name
|
||||||
// }
|
}
|
||||||
|
|
||||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
fn migrations() -> &'static [&'static str] {
|
||||||
// {
|
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||||
// 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;
|
let tempdir = TempDir::new("DbTests").unwrap();
|
||||||
// assert!(
|
{
|
||||||
// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
let corrupt_db =
|
||||||
// .unwrap()
|
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||||
// .is_none()
|
assert!(corrupt_db.persistent());
|
||||||
// );
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Test that DB exists but corrupted (causing recreate)
|
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||||
// #[gpui::test(iterations = 30)]
|
assert!(
|
||||||
// async fn test_simultaneous_db_corruption() {
|
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||||
// enum CorruptedDB {}
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// impl Domain for CorruptedDB {
|
/// Test that DB exists but corrupted (causing recreate)
|
||||||
// fn name() -> &'static str {
|
#[gpui2::test(iterations = 30)]
|
||||||
// "db_tests"
|
async fn test_simultaneous_db_corruption(cx: &mut gpui2::TestAppContext) {
|
||||||
// }
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
// fn migrations() -> &'static [&'static str] {
|
enum CorruptedDB {}
|
||||||
// &[sql!(CREATE TABLE test(value);)]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// enum GoodDB {}
|
impl Domain for CorruptedDB {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"db_tests"
|
||||||
|
}
|
||||||
|
|
||||||
// impl Domain for GoodDB {
|
fn migrations() -> &'static [&'static str] {
|
||||||
// fn name() -> &'static str {
|
&[sql!(CREATE TABLE test(value);)]
|
||||||
// "db_tests" //Notice same name
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// fn migrations() -> &'static [&'static str] {
|
enum GoodDB {}
|
||||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
impl Domain for GoodDB {
|
||||||
// {
|
fn name() -> &'static str {
|
||||||
// // Setup the bad database
|
"db_tests" //Notice same name
|
||||||
// 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
|
fn migrations() -> &'static [&'static str] {
|
||||||
// let mut guards = vec![];
|
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||||
// 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);
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
// for guard in guards.into_iter() {
|
// Try to connect to it a bunch of times at once
|
||||||
// assert!(guard.join().is_ok());
|
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)]
|
#[cfg(test)]
|
||||||
// mod tests {
|
mod tests {
|
||||||
// use crate::kvp::KeyValueStore;
|
use crate::kvp::KeyValueStore;
|
||||||
|
|
||||||
// #[gpui::test]
|
#[gpui2::test]
|
||||||
// async fn test_kvp() {
|
async fn test_kvp() {
|
||||||
// let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
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())
|
db.write_kvp("key-1".to_string(), "one".to_string())
|
||||||
// .await
|
.await
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
||||||
|
|
||||||
// db.write_kvp("key-1".to_string(), "one-2".to_string())
|
db.write_kvp("key-1".to_string(), "one-2".to_string())
|
||||||
// .await
|
.await
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
||||||
|
|
||||||
// db.write_kvp("key-2".to_string(), "two".to_string())
|
db.write_kvp("key-2".to_string(), "two".to_string())
|
||||||
// .await
|
.await
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
||||||
|
|
||||||
// db.delete_kvp("key-1".to_string()).await.unwrap();
|
db.delete_kvp("key-1".to_string()).await.unwrap();
|
||||||
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
|
@ -198,14 +198,19 @@ impl<V> AnyElement<V> {
|
||||||
pub trait Component<V> {
|
pub trait Component<V> {
|
||||||
fn render(self) -> AnyElement<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>,
|
||||||
|
{
|
||||||
|
f(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
if condition {
|
self.map(|this| if condition { then(this) } else { this })
|
||||||
self = then(self);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,9 @@ use crate::{
|
||||||
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
|
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||||
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
|
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
|
||||||
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
|
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
|
||||||
SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
|
SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
|
||||||
Underline, UnderlineStyle, View, VisualContext, WeakView, WindowOptions, SUBPIXEL_VARIANTS,
|
TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
|
||||||
|
WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
@ -56,6 +57,7 @@ pub enum DispatchPhase {
|
||||||
Capture,
|
Capture,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||||
type AnyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
type AnyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
||||||
type AnyKeyListener = Box<
|
type AnyKeyListener = Box<
|
||||||
dyn Fn(
|
dyn Fn(
|
||||||
|
@ -187,6 +189,10 @@ pub struct Window {
|
||||||
default_prevented: bool,
|
default_prevented: bool,
|
||||||
mouse_position: Point<Pixels>,
|
mouse_position: Point<Pixels>,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
|
bounds: WindowBounds,
|
||||||
|
bounds_observers: SubscriberSet<(), AnyObserver>,
|
||||||
|
active: bool,
|
||||||
|
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||||
pub(crate) scene_builder: SceneBuilder,
|
pub(crate) scene_builder: SceneBuilder,
|
||||||
pub(crate) dirty: bool,
|
pub(crate) dirty: bool,
|
||||||
pub(crate) last_blur: Option<Option<FocusId>>,
|
pub(crate) last_blur: Option<Option<FocusId>>,
|
||||||
|
@ -205,16 +211,34 @@ impl Window {
|
||||||
let mouse_position = platform_window.mouse_position();
|
let mouse_position = platform_window.mouse_position();
|
||||||
let content_size = platform_window.content_size();
|
let content_size = platform_window.content_size();
|
||||||
let scale_factor = platform_window.scale_factor();
|
let scale_factor = platform_window.scale_factor();
|
||||||
|
let bounds = platform_window.bounds();
|
||||||
|
|
||||||
platform_window.on_resize(Box::new({
|
platform_window.on_resize(Box::new({
|
||||||
let mut cx = cx.to_async();
|
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
|
handle
|
||||||
.update(&mut cx, |_, cx| {
|
.update(&mut cx, |_, cx| {
|
||||||
cx.window.scale_factor = scale_factor;
|
cx.window.active = active;
|
||||||
cx.window.scene_builder = SceneBuilder::new();
|
cx.window
|
||||||
cx.window.content_size = content_size;
|
.activation_observers
|
||||||
cx.window.display_id = cx.window.platform_window.display().id();
|
.clone()
|
||||||
cx.window.dirty = true;
|
.retain(&(), |callback| callback(cx));
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
@ -257,6 +281,10 @@ impl Window {
|
||||||
default_prevented: true,
|
default_prevented: true,
|
||||||
mouse_position,
|
mouse_position,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
|
bounds,
|
||||||
|
bounds_observers: SubscriberSet::new(),
|
||||||
|
active: false,
|
||||||
|
activation_observers: SubscriberSet::new(),
|
||||||
scene_builder: SceneBuilder::new(),
|
scene_builder: SceneBuilder::new(),
|
||||||
dirty: true,
|
dirty: true,
|
||||||
last_blur: None,
|
last_blur: None,
|
||||||
|
@ -534,6 +562,23 @@ impl<'a> WindowContext<'a> {
|
||||||
bounds
|
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
|
/// 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
|
/// return 2.0 for a "retina" display, indicating that each logical pixel should actually
|
||||||
/// be rendered as two pixels on screen.
|
/// 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(
|
pub fn on_focus_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
|
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
|
||||||
|
|
|
@ -1107,74 +1107,74 @@ impl FakeLanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod tests {
|
mod tests {
|
||||||
// use super::*;
|
use super::*;
|
||||||
// use gpui::TestAppContext;
|
use gpui2::TestAppContext;
|
||||||
|
|
||||||
// #[ctor::ctor]
|
#[ctor::ctor]
|
||||||
// fn init_logger() {
|
fn init_logger() {
|
||||||
// if std::env::var("RUST_LOG").is_ok() {
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
// env_logger::init();
|
env_logger::init();
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[gpui::test]
|
#[gpui2::test]
|
||||||
// async fn test_fake(cx: &mut TestAppContext) {
|
async fn test_fake(cx: &mut TestAppContext) {
|
||||||
// let (server, mut fake) =
|
let (server, mut fake) =
|
||||||
// LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async());
|
LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async());
|
||||||
|
|
||||||
// let (message_tx, message_rx) = channel::unbounded();
|
let (message_tx, message_rx) = channel::unbounded();
|
||||||
// let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
||||||
// server
|
server
|
||||||
// .on_notification::<notification::ShowMessage, _>(move |params, _| {
|
.on_notification::<notification::ShowMessage, _>(move |params, _| {
|
||||||
// message_tx.try_send(params).unwrap()
|
message_tx.try_send(params).unwrap()
|
||||||
// })
|
})
|
||||||
// .detach();
|
.detach();
|
||||||
// server
|
server
|
||||||
// .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
|
.on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
|
||||||
// diagnostics_tx.try_send(params).unwrap()
|
diagnostics_tx.try_send(params).unwrap()
|
||||||
// })
|
})
|
||||||
// .detach();
|
.detach();
|
||||||
|
|
||||||
// let server = server.initialize(None).await.unwrap();
|
let server = server.initialize(None).await.unwrap();
|
||||||
// server
|
server
|
||||||
// .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||||
// text_document: TextDocumentItem::new(
|
text_document: TextDocumentItem::new(
|
||||||
// Url::from_str("file://a/b").unwrap(),
|
Url::from_str("file://a/b").unwrap(),
|
||||||
// "rust".to_string(),
|
"rust".to_string(),
|
||||||
// 0,
|
0,
|
||||||
// "".to_string(),
|
"".to_string(),
|
||||||
// ),
|
),
|
||||||
// })
|
})
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// fake.receive_notification::<notification::DidOpenTextDocument>()
|
fake.receive_notification::<notification::DidOpenTextDocument>()
|
||||||
// .await
|
.await
|
||||||
// .text_document
|
.text_document
|
||||||
// .uri
|
.uri
|
||||||
// .as_str(),
|
.as_str(),
|
||||||
// "file://a/b"
|
"file://a/b"
|
||||||
// );
|
);
|
||||||
|
|
||||||
// fake.notify::<notification::ShowMessage>(ShowMessageParams {
|
fake.notify::<notification::ShowMessage>(ShowMessageParams {
|
||||||
// typ: MessageType::ERROR,
|
typ: MessageType::ERROR,
|
||||||
// message: "ok".to_string(),
|
message: "ok".to_string(),
|
||||||
// });
|
});
|
||||||
// fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
|
fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
|
||||||
// uri: Url::from_str("file://b/c").unwrap(),
|
uri: Url::from_str("file://b/c").unwrap(),
|
||||||
// version: Some(5),
|
version: Some(5),
|
||||||
// diagnostics: vec![],
|
diagnostics: vec![],
|
||||||
// });
|
});
|
||||||
// assert_eq!(message_rx.recv().await.unwrap().message, "ok");
|
assert_eq!(message_rx.recv().await.unwrap().message, "ok");
|
||||||
// assert_eq!(
|
assert_eq!(
|
||||||
// diagnostics_rx.recv().await.unwrap().uri.as_str(),
|
diagnostics_rx.recv().await.unwrap().uri.as_str(),
|
||||||
// "file://b/c"
|
"file://b/c"
|
||||||
// );
|
);
|
||||||
|
|
||||||
// fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
|
fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
|
||||||
|
|
||||||
// drop(server);
|
drop(server);
|
||||||
// fake.receive_notification::<notification::Exit>().await;
|
fake.receive_notification::<notification::Exit>().await;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ fn main() {
|
||||||
WindowOptions {
|
WindowOptions {
|
||||||
bounds: WindowBounds::Fixed(Bounds {
|
bounds: WindowBounds::Fixed(Bounds {
|
||||||
origin: Default::default(),
|
origin: Default::default(),
|
||||||
size: size(px(1700.), px(980.)).into(),
|
size: size(px(1500.), px(780.)).into(),
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|
|
@ -64,6 +64,7 @@ pub struct ThemeColors {
|
||||||
pub element_selected: Hsla,
|
pub element_selected: Hsla,
|
||||||
pub element_disabled: Hsla,
|
pub element_disabled: Hsla,
|
||||||
pub element_placeholder: Hsla,
|
pub element_placeholder: Hsla,
|
||||||
|
pub element_drop_target: Hsla,
|
||||||
pub ghost_element: Hsla,
|
pub ghost_element: Hsla,
|
||||||
pub ghost_element_hover: Hsla,
|
pub ghost_element_hover: Hsla,
|
||||||
pub ghost_element_active: Hsla,
|
pub ghost_element_active: Hsla,
|
||||||
|
@ -83,6 +84,8 @@ pub struct ThemeColors {
|
||||||
pub title_bar: Hsla,
|
pub title_bar: Hsla,
|
||||||
pub toolbar: Hsla,
|
pub toolbar: Hsla,
|
||||||
pub tab_bar: Hsla,
|
pub tab_bar: Hsla,
|
||||||
|
pub tab_inactive: Hsla,
|
||||||
|
pub tab_active: Hsla,
|
||||||
pub editor: Hsla,
|
pub editor: Hsla,
|
||||||
pub editor_subheader: Hsla,
|
pub editor_subheader: Hsla,
|
||||||
pub editor_active_line: Hsla,
|
pub editor_active_line: Hsla,
|
||||||
|
|
|
@ -9,6 +9,10 @@ use crate::{
|
||||||
ColorScale,
|
ColorScale,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn neutral() -> ColorScaleSet {
|
||||||
|
slate()
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for SystemColors {
|
impl Default for SystemColors {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -24,16 +28,16 @@ impl Default for StatusColors {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
conflict: red().dark().step_11(),
|
conflict: red().dark().step_11(),
|
||||||
created: gpui2::black(),
|
created: grass().dark().step_11(),
|
||||||
deleted: gpui2::black(),
|
deleted: red().dark().step_11(),
|
||||||
error: gpui2::black(),
|
error: red().dark().step_11(),
|
||||||
hidden: gpui2::black(),
|
hidden: neutral().dark().step_11(),
|
||||||
ignored: gpui2::black(),
|
ignored: neutral().dark().step_11(),
|
||||||
info: gpui2::black(),
|
info: blue().dark().step_11(),
|
||||||
modified: gpui2::black(),
|
modified: yellow().dark().step_11(),
|
||||||
renamed: gpui2::black(),
|
renamed: blue().dark().step_11(),
|
||||||
success: gpui2::black(),
|
success: grass().dark().step_11(),
|
||||||
warning: gpui2::black(),
|
warning: yellow().dark().step_11(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,12 +45,12 @@ impl Default for StatusColors {
|
||||||
impl Default for GitStatusColors {
|
impl Default for GitStatusColors {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
conflict: gpui2::rgba(0xdec184ff).into(),
|
conflict: orange().dark().step_11(),
|
||||||
created: gpui2::rgba(0xa1c181ff).into(),
|
created: grass().dark().step_11(),
|
||||||
deleted: gpui2::rgba(0xd07277ff).into(),
|
deleted: red().dark().step_11(),
|
||||||
ignored: gpui2::rgba(0x555a63ff).into(),
|
ignored: neutral().dark().step_11(),
|
||||||
modified: gpui2::rgba(0x74ade8ff).into(),
|
modified: yellow().dark().step_11(),
|
||||||
renamed: gpui2::rgba(0xdec184ff).into(),
|
renamed: blue().dark().step_11(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,54 +86,57 @@ impl SyntaxTheme {
|
||||||
pub fn default_light() -> Self {
|
pub fn default_light() -> Self {
|
||||||
Self {
|
Self {
|
||||||
highlights: vec![
|
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(),
|
"punctuation.bracket".into(),
|
||||||
gpui2::rgba(0xad6e26ff).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(),
|
"punctuation.delimiter".into(),
|
||||||
gpui2::rgba(0x4d4f52ff).into(),
|
neutral().light().step_11().into(),
|
||||||
),
|
),
|
||||||
("label".into(), gpui2::rgba(0x5c78e2ff).into()),
|
(
|
||||||
("keyword".into(), gpui2::rgba(0xa449abff).into()),
|
"punctuation.list_marker".into(),
|
||||||
("string.escape".into(), gpui2::rgba(0x7c7e86ff).into()),
|
blue().light().step_11().into(),
|
||||||
("text.literal".into(), gpui2::rgba(0x649f57ff).into()),
|
),
|
||||||
("variable.special".into(), gpui2::rgba(0xad6e25ff).into()),
|
("punctuation.special".into(), red().light().step_7().into()),
|
||||||
("comment.doc".into(), gpui2::rgba(0x7c7e86ff).into()),
|
("string".into(), jade().light().step_11().into()),
|
||||||
("enum".into(), gpui2::rgba(0xd3604fff).into()),
|
("string.escape".into(), red().light().step_7().into()),
|
||||||
("operator".into(), gpui2::rgba(0x3882b7ff).into()),
|
("string.regex".into(), tomato().light().step_11().into()),
|
||||||
("preproc".into(), gpui2::rgba(0x383a41ff).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 {
|
pub fn default_dark() -> Self {
|
||||||
Self {
|
Self {
|
||||||
highlights: vec![
|
highlights: vec![
|
||||||
("keyword".into(), gpui2::rgba(0xb477cfff).into()),
|
("attribute".into(), cyan().dark().step_11().into()),
|
||||||
("comment.doc".into(), gpui2::rgba(0x878e98ff).into()),
|
("boolean".into(), tomato().dark().step_11().into()),
|
||||||
("variant".into(), gpui2::rgba(0x73ade9ff).into()),
|
("comment".into(), neutral().dark().step_11().into()),
|
||||||
("property".into(), gpui2::rgba(0xd07277ff).into()),
|
("comment.doc".into(), iris().dark().step_12().into()),
|
||||||
("function".into(), gpui2::rgba(0x73ade9ff).into()),
|
("constant".into(), red().dark().step_7().into()),
|
||||||
("type".into(), gpui2::rgba(0x6eb4bfff).into()),
|
("constructor".into(), red().dark().step_7().into()),
|
||||||
("tag".into(), gpui2::rgba(0x74ade8ff).into()),
|
("embedded".into(), red().dark().step_7().into()),
|
||||||
("string.escape".into(), gpui2::rgba(0x878e98ff).into()),
|
("emphasis".into(), red().dark().step_7().into()),
|
||||||
("punctuation.bracket".into(), gpui2::rgba(0xb2b9c6ff).into()),
|
("emphasis.strong".into(), red().dark().step_7().into()),
|
||||||
("hint".into(), gpui2::rgba(0x5a6f89ff).into()),
|
("enum".into(), red().dark().step_7().into()),
|
||||||
("punctuation".into(), gpui2::rgba(0xacb2beff).into()),
|
("function".into(), red().dark().step_7().into()),
|
||||||
("comment".into(), gpui2::rgba(0x5d636fff).into()),
|
("hint".into(), red().dark().step_7().into()),
|
||||||
("emphasis".into(), gpui2::rgba(0x74ade8ff).into()),
|
("keyword".into(), orange().dark().step_11().into()),
|
||||||
("punctuation.special".into(), gpui2::rgba(0xb1574bff).into()),
|
("label".into(), red().dark().step_7().into()),
|
||||||
("link_uri".into(), gpui2::rgba(0x6eb4bfff).into()),
|
("link_text".into(), red().dark().step_7().into()),
|
||||||
("string.regex".into(), gpui2::rgba(0xbf956aff).into()),
|
("link_uri".into(), red().dark().step_7().into()),
|
||||||
("constructor".into(), gpui2::rgba(0x73ade9ff).into()),
|
("number".into(), red().dark().step_7().into()),
|
||||||
("operator".into(), gpui2::rgba(0x6eb4bfff).into()),
|
("operator".into(), red().dark().step_7().into()),
|
||||||
("constant".into(), gpui2::rgba(0xdfc184ff).into()),
|
("predictive".into(), red().dark().step_7().into()),
|
||||||
("string.special".into(), gpui2::rgba(0xbf956aff).into()),
|
("preproc".into(), red().dark().step_7().into()),
|
||||||
("emphasis.strong".into(), gpui2::rgba(0xbf956aff).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(),
|
"punctuation.bracket".into(),
|
||||||
gpui2::rgba(0xbf956aff).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(),
|
"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(),
|
"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 {
|
impl ThemeColors {
|
||||||
pub fn default_light() -> Self {
|
pub fn default_light() -> Self {
|
||||||
|
let system = SystemColors::default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
border: gpui2::white(),
|
border: neutral().light().step_6(),
|
||||||
border_variant: gpui2::white(),
|
border_variant: neutral().light().step_5(),
|
||||||
border_focused: gpui2::white(),
|
border_focused: blue().light().step_5(),
|
||||||
border_transparent: gpui2::white(),
|
border_transparent: system.transparent,
|
||||||
elevated_surface: gpui2::white(),
|
elevated_surface: neutral().light().step_2(),
|
||||||
surface: gpui2::white(),
|
surface: neutral().light().step_2(),
|
||||||
background: gpui2::white(),
|
background: neutral().light().step_1(),
|
||||||
element: gpui2::white(),
|
element: neutral().light().step_3(),
|
||||||
element_hover: gpui2::white(),
|
element_hover: neutral().light().step_4(),
|
||||||
element_active: gpui2::white(),
|
element_active: neutral().light().step_5(),
|
||||||
element_selected: gpui2::white(),
|
element_selected: neutral().light().step_5(),
|
||||||
element_disabled: gpui2::white(),
|
element_disabled: neutral().light_alpha().step_3(),
|
||||||
element_placeholder: gpui2::white(),
|
element_placeholder: neutral().light().step_11(),
|
||||||
ghost_element: gpui2::white(),
|
element_drop_target: blue().light_alpha().step_2(),
|
||||||
ghost_element_hover: gpui2::white(),
|
ghost_element: system.transparent,
|
||||||
ghost_element_active: gpui2::white(),
|
ghost_element_hover: neutral().light().step_4(),
|
||||||
ghost_element_selected: gpui2::white(),
|
ghost_element_active: neutral().light().step_5(),
|
||||||
ghost_element_disabled: gpui2::white(),
|
ghost_element_selected: neutral().light().step_5(),
|
||||||
text: gpui2::white(),
|
ghost_element_disabled: neutral().light_alpha().step_3(),
|
||||||
text_muted: gpui2::white(),
|
text: neutral().light().step_12(),
|
||||||
text_placeholder: gpui2::white(),
|
text_muted: neutral().light().step_11(),
|
||||||
text_disabled: gpui2::white(),
|
text_placeholder: neutral().light().step_10(),
|
||||||
text_accent: gpui2::white(),
|
text_disabled: neutral().light().step_9(),
|
||||||
icon: gpui2::white(),
|
text_accent: blue().light().step_11(),
|
||||||
icon_muted: gpui2::white(),
|
icon: neutral().light().step_11(),
|
||||||
icon_disabled: gpui2::white(),
|
icon_muted: neutral().light().step_10(),
|
||||||
icon_placeholder: gpui2::white(),
|
icon_disabled: neutral().light().step_9(),
|
||||||
icon_accent: gpui2::white(),
|
icon_placeholder: neutral().light().step_10(),
|
||||||
status_bar: gpui2::white(),
|
icon_accent: blue().light().step_11(),
|
||||||
title_bar: gpui2::white(),
|
status_bar: neutral().light().step_2(),
|
||||||
toolbar: gpui2::white(),
|
title_bar: neutral().light().step_2(),
|
||||||
tab_bar: gpui2::white(),
|
toolbar: neutral().light().step_1(),
|
||||||
editor: gpui2::white(),
|
tab_bar: neutral().light().step_2(),
|
||||||
editor_subheader: gpui2::white(),
|
tab_active: neutral().light().step_1(),
|
||||||
editor_active_line: gpui2::white(),
|
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 {
|
pub fn default_dark() -> Self {
|
||||||
|
let system = SystemColors::default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
border: gpui2::rgba(0x464b57ff).into(),
|
border: neutral().dark().step_6(),
|
||||||
border_variant: gpui2::rgba(0x464b57ff).into(),
|
border_variant: neutral().dark().step_5(),
|
||||||
border_focused: gpui2::rgba(0x293b5bff).into(),
|
border_focused: blue().dark().step_5(),
|
||||||
border_transparent: gpui2::rgba(0x00000000).into(),
|
border_transparent: system.transparent,
|
||||||
elevated_surface: gpui2::rgba(0x3b414dff).into(),
|
elevated_surface: neutral().dark().step_2(),
|
||||||
surface: gpui2::rgba(0x2f343eff).into(),
|
surface: neutral().dark().step_2(),
|
||||||
background: gpui2::rgba(0x3b414dff).into(),
|
background: neutral().dark().step_1(),
|
||||||
element: gpui2::rgba(0x3b414dff).into(),
|
element: neutral().dark().step_3(),
|
||||||
element_hover: gpui2::rgba(0xffffff1e).into(),
|
element_hover: neutral().dark().step_4(),
|
||||||
element_active: gpui2::rgba(0xffffff28).into(),
|
element_active: neutral().dark().step_5(),
|
||||||
element_selected: gpui2::rgba(0x18243dff).into(),
|
element_selected: neutral().dark().step_5(),
|
||||||
element_disabled: gpui2::rgba(0x00000000).into(),
|
element_disabled: neutral().dark_alpha().step_3(),
|
||||||
element_placeholder: gpui2::black(),
|
element_placeholder: neutral().dark().step_11(),
|
||||||
ghost_element: gpui2::rgba(0x00000000).into(),
|
element_drop_target: blue().dark_alpha().step_2(),
|
||||||
ghost_element_hover: gpui2::rgba(0xffffff14).into(),
|
ghost_element: system.transparent,
|
||||||
ghost_element_active: gpui2::rgba(0xffffff1e).into(),
|
ghost_element_hover: neutral().dark().step_4(),
|
||||||
ghost_element_selected: gpui2::rgba(0x18243dff).into(),
|
ghost_element_active: neutral().dark().step_5(),
|
||||||
ghost_element_disabled: gpui2::rgba(0x00000000).into(),
|
ghost_element_selected: neutral().dark().step_5(),
|
||||||
text: gpui2::rgba(0xc8ccd4ff).into(),
|
ghost_element_disabled: neutral().dark_alpha().step_3(),
|
||||||
text_muted: gpui2::rgba(0x838994ff).into(),
|
text: neutral().dark().step_12(),
|
||||||
text_placeholder: gpui2::rgba(0xd07277ff).into(),
|
text_muted: neutral().dark().step_11(),
|
||||||
text_disabled: gpui2::rgba(0x555a63ff).into(),
|
text_placeholder: neutral().dark().step_10(),
|
||||||
text_accent: gpui2::rgba(0x74ade8ff).into(),
|
text_disabled: neutral().dark().step_9(),
|
||||||
icon: gpui2::black(),
|
text_accent: blue().dark().step_11(),
|
||||||
icon_muted: gpui2::rgba(0x838994ff).into(),
|
icon: neutral().dark().step_11(),
|
||||||
icon_disabled: gpui2::black(),
|
icon_muted: neutral().dark().step_10(),
|
||||||
icon_placeholder: gpui2::black(),
|
icon_disabled: neutral().dark().step_9(),
|
||||||
icon_accent: gpui2::black(),
|
icon_placeholder: neutral().dark().step_10(),
|
||||||
status_bar: gpui2::rgba(0x3b414dff).into(),
|
icon_accent: blue().dark().step_11(),
|
||||||
title_bar: gpui2::rgba(0x3b414dff).into(),
|
status_bar: neutral().dark().step_2(),
|
||||||
toolbar: gpui2::rgba(0x282c33ff).into(),
|
title_bar: neutral().dark().step_2(),
|
||||||
tab_bar: gpui2::rgba(0x2f343eff).into(),
|
toolbar: neutral().dark().step_1(),
|
||||||
editor: gpui2::rgba(0x282c33ff).into(),
|
tab_bar: neutral().dark().step_2(),
|
||||||
editor_subheader: gpui2::rgba(0x2f343eff).into(),
|
tab_active: neutral().dark().step_1(),
|
||||||
editor_active_line: gpui2::rgba(0x2f343eff).into(),
|
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
|
&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.
|
/// Returns the color for the syntax node with the given name.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn syntax_color(&self, name: &str) -> Hsla {
|
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::settings::user_settings;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -15,12 +15,20 @@ pub enum ListItemVariant {
|
||||||
Inset,
|
Inset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum ListHeaderMeta {
|
||||||
|
// TODO: These should be IconButtons
|
||||||
|
Tools(Vec<Icon>),
|
||||||
|
// TODO: This should be a button
|
||||||
|
Button(Label),
|
||||||
|
Text(Label),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct ListHeader {
|
pub struct ListHeader {
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
left_icon: Option<Icon>,
|
left_icon: Option<Icon>,
|
||||||
|
meta: Option<ListHeaderMeta>,
|
||||||
variant: ListItemVariant,
|
variant: ListItemVariant,
|
||||||
state: InteractionState,
|
|
||||||
toggleable: Toggleable,
|
toggleable: Toggleable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +37,9 @@ impl ListHeader {
|
||||||
Self {
|
Self {
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
left_icon: None,
|
left_icon: None,
|
||||||
|
meta: None,
|
||||||
variant: ListItemVariant::default(),
|
variant: ListItemVariant::default(),
|
||||||
state: InteractionState::default(),
|
toggleable: Toggleable::NotToggleable,
|
||||||
toggleable: Toggleable::Toggleable(ToggleState::Toggled),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +58,8 @@ impl ListHeader {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(mut self, state: InteractionState) -> Self {
|
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
||||||
self.state = state;
|
self.meta = meta;
|
||||||
self
|
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> {
|
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
||||||
let is_toggled = self.toggleable.is_toggled();
|
let is_toggled = self.toggleable.is_toggled();
|
||||||
|
|
||||||
let disclosure_control = self.disclosure_control();
|
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()
|
h_stack()
|
||||||
.flex_1()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.bg(cx.theme().colors().surface)
|
.bg(cx.theme().colors().surface)
|
||||||
.when(self.state == InteractionState::Focused, |this| {
|
// TODO: Add focus state
|
||||||
this.border()
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
.border_color(cx.theme().colors().border_focused)
|
// this.border()
|
||||||
})
|
// .border_color(cx.theme().colors().border_focused)
|
||||||
|
// })
|
||||||
.relative()
|
.relative()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
@ -109,9 +119,13 @@ impl ListHeader {
|
||||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.child(
|
||||||
|
h_stack()
|
||||||
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
|
@ -126,6 +140,8 @@ impl ListHeader {
|
||||||
)
|
)
|
||||||
.child(disclosure_control),
|
.child(disclosure_control),
|
||||||
)
|
)
|
||||||
|
.child(meta),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,32 +489,52 @@ impl<V: 'static> ListDetailsEntry<V> {
|
||||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
let settings = user_settings(cx);
|
let settings = user_settings(cx);
|
||||||
|
|
||||||
let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
|
let (item_bg, item_bg_hover, item_bg_active) = (
|
||||||
true => (
|
|
||||||
cx.theme().colors().ghost_element,
|
cx.theme().colors().ghost_element,
|
||||||
cx.theme().colors().ghost_element_hover,
|
cx.theme().colors().ghost_element_hover,
|
||||||
cx.theme().colors().ghost_element_active,
|
cx.theme().colors().ghost_element_active,
|
||||||
),
|
);
|
||||||
false => (
|
|
||||||
cx.theme().colors().element,
|
|
||||||
cx.theme().colors().element_hover,
|
|
||||||
cx.theme().colors().element_active,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let label_color = match self.seen {
|
let label_color = match self.seen {
|
||||||
true => LabelColor::Muted,
|
true => LabelColor::Muted,
|
||||||
false => LabelColor::Default,
|
false => LabelColor::Default,
|
||||||
};
|
};
|
||||||
|
|
||||||
v_stack()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.group("")
|
.group("")
|
||||||
.bg(item_bg)
|
.bg(item_bg)
|
||||||
.px_1()
|
.px_2()
|
||||||
.py_1_5()
|
.py_1p5()
|
||||||
|
.w_full()
|
||||||
|
.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(
|
||||||
|
v_stack()
|
||||||
.w_full()
|
.w_full()
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w_5()
|
||||||
|
.h_5()
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().colors().icon_accent),
|
||||||
|
)
|
||||||
.child(Label::new(self.label.clone()).color(label_color))
|
.child(Label::new(self.label.clone()).color(label_color))
|
||||||
.children(
|
.children(
|
||||||
self.meta
|
self.meta
|
||||||
|
@ -509,6 +545,7 @@ impl<V: 'static> ListDetailsEntry<V> {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.children(self.actions.unwrap_or_default()),
|
.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> {
|
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 is_toggled = Toggleable::is_toggled(&self.toggleable);
|
||||||
|
|
||||||
let list_content = match (self.items.is_empty(), is_toggled) {
|
let list_content = match (self.items.is_empty(), is_toggled) {
|
||||||
(_, false) => div(),
|
|
||||||
(false, _) => div().children(self.items),
|
(false, _) => div().children(self.items),
|
||||||
(true, _) => {
|
(true, false) => div(),
|
||||||
|
(true, true) => {
|
||||||
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
|
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
v_stack()
|
v_stack()
|
||||||
|
.w_full()
|
||||||
.py_1()
|
.py_1()
|
||||||
.children(self.header.map(|header| header.toggleable(self.toggleable)))
|
.children(self.header.map(|header| header.toggleable(self.toggleable)))
|
||||||
.child(list_content)
|
.child(list_content)
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
|
use crate::utils::naive_format_distance_from_now;
|
||||||
use crate::{List, ListHeader};
|
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)]
|
#[derive(Component)]
|
||||||
pub struct NotificationsPanel {
|
pub struct NotificationsPanel {
|
||||||
|
@ -16,31 +21,348 @@ impl NotificationsPanel {
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.w_full()
|
.size_full()
|
||||||
.h_full()
|
|
||||||
.bg(cx.theme().colors().surface)
|
.bg(cx.theme().colors().surface)
|
||||||
|
.child(
|
||||||
|
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(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id("header")
|
.mx_2()
|
||||||
.w_full()
|
.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()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.overflow_y_scroll()
|
.w_full()
|
||||||
.child(
|
.children(
|
||||||
List::new(static_new_notification_items())
|
Some(
|
||||||
.header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
|
div()
|
||||||
.toggle(ToggleState::Toggled),
|
.absolute()
|
||||||
|
.left(px(3.0))
|
||||||
|
.top_3()
|
||||||
|
.z_index(2)
|
||||||
|
.child(UnreadIndicator::new()),
|
||||||
|
)
|
||||||
|
.filter(|_| self.unread),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
List::new(static_read_notification_items())
|
v_stack()
|
||||||
.header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
|
.z_index(1)
|
||||||
.empty_message("No new notifications")
|
.gap_1()
|
||||||
.toggle(ToggleState::Toggled),
|
.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(
|
||||||
|
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")]
|
#[cfg(feature = "stories")]
|
||||||
pub use stories::*;
|
pub use stories::*;
|
||||||
|
|
||||||
|
|
|
@ -98,16 +98,14 @@ impl<V: 'static> Panel<V> {
|
||||||
v_stack()
|
v_stack()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.flex_initial()
|
.flex_initial()
|
||||||
.when(
|
.map(|this| match self.current_side {
|
||||||
self.current_side == PanelSide::Left || self.current_side == PanelSide::Right,
|
PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
|
||||||
|this| this.h_full().w(current_size),
|
PanelSide::Bottom => this,
|
||||||
)
|
|
||||||
.when(self.current_side == PanelSide::Left, |this| this.border_r())
|
|
||||||
.when(self.current_side == PanelSide::Right, |this| {
|
|
||||||
this.border_l()
|
|
||||||
})
|
})
|
||||||
.when(self.current_side == PanelSide::Bottom, |this| {
|
.map(|this| match self.current_side {
|
||||||
this.border_b().w_full().h(current_size)
|
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)
|
.bg(cx.theme().colors().surface)
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{Icon, IconColor, IconElement, Label, LabelColor};
|
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)]
|
#[derive(Component, Clone)]
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
|
@ -108,13 +108,13 @@ impl Tab {
|
||||||
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
|
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
|
||||||
|
|
||||||
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
|
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
|
||||||
true => (
|
false => (
|
||||||
cx.theme().colors().ghost_element,
|
cx.theme().colors().tab_inactive,
|
||||||
cx.theme().colors().ghost_element_hover,
|
cx.theme().colors().ghost_element_hover,
|
||||||
cx.theme().colors().ghost_element_active,
|
cx.theme().colors().ghost_element_active,
|
||||||
),
|
),
|
||||||
false => (
|
true => (
|
||||||
cx.theme().colors().element,
|
cx.theme().colors().tab_active,
|
||||||
cx.theme().colors().element_hover,
|
cx.theme().colors().element_hover,
|
||||||
cx.theme().colors().element_active,
|
cx.theme().colors().element_active,
|
||||||
),
|
),
|
||||||
|
@ -127,7 +127,7 @@ impl Tab {
|
||||||
div()
|
div()
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.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| {
|
.on_drop(|_view, state: View<TabDragState>, cx| {
|
||||||
eprintln!("{:?}", state.read(cx));
|
eprintln!("{:?}", state.read(cx));
|
||||||
})
|
})
|
||||||
|
@ -144,7 +144,7 @@ impl Tab {
|
||||||
.px_1()
|
.px_1()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1p5()
|
||||||
.children(has_fs_conflict.then(|| {
|
.children(has_fs_conflict.then(|| {
|
||||||
IconElement::new(Icon::ExclamationTriangle)
|
IconElement::new(Icon::ExclamationTriangle)
|
||||||
.size(crate::IconSize::Small)
|
.size(crate::IconSize::Small)
|
||||||
|
|
|
@ -27,6 +27,7 @@ impl TabBar {
|
||||||
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
|
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
|
.group("tab_bar")
|
||||||
.id(self.id.clone())
|
.id(self.id.clone())
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
|
@ -34,6 +35,7 @@ impl TabBar {
|
||||||
// Left Side
|
// Left Side
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.relative()
|
||||||
.px_1()
|
.px_1()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
@ -41,6 +43,7 @@ impl TabBar {
|
||||||
// Nav Buttons
|
// Nav Buttons
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.right_0()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_px()
|
.gap_px()
|
||||||
|
@ -67,10 +70,15 @@ impl TabBar {
|
||||||
// Right Side
|
// Right Side
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
// We only use absolute here since we don't
|
||||||
|
// have opacity or `hidden()` yet
|
||||||
|
.absolute()
|
||||||
|
.neg_top_7()
|
||||||
.px_1()
|
.px_1()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
.group_hover("tab_bar", |this| this.top_0())
|
||||||
// Nav Buttons
|
// Nav Buttons
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod avatar;
|
||||||
mod button;
|
mod button;
|
||||||
mod details;
|
mod details;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
mod indicator;
|
||||||
mod input;
|
mod input;
|
||||||
mod label;
|
mod label;
|
||||||
mod player;
|
mod player;
|
||||||
|
@ -12,6 +13,7 @@ pub use avatar::*;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use details::*;
|
pub use details::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
|
pub use indicator::*;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use player::*;
|
pub use player::*;
|
||||||
|
|
|
@ -26,23 +26,21 @@ pub enum IconColor {
|
||||||
|
|
||||||
impl IconColor {
|
impl IconColor {
|
||||||
pub fn color(self, cx: &WindowContext) -> Hsla {
|
pub fn color(self, cx: &WindowContext) -> Hsla {
|
||||||
let theme_colors = cx.theme().colors();
|
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
IconColor::Default => theme_colors.icon,
|
IconColor::Default => cx.theme().colors().icon,
|
||||||
IconColor::Muted => theme_colors.icon_muted,
|
IconColor::Muted => cx.theme().colors().icon_muted,
|
||||||
IconColor::Disabled => theme_colors.icon_disabled,
|
IconColor::Disabled => cx.theme().colors().icon_disabled,
|
||||||
IconColor::Placeholder => theme_colors.icon_placeholder,
|
IconColor::Placeholder => cx.theme().colors().icon_placeholder,
|
||||||
IconColor::Accent => theme_colors.icon_accent,
|
IconColor::Accent => cx.theme().colors().icon_accent,
|
||||||
IconColor::Error => gpui2::red(),
|
IconColor::Error => cx.theme().status().error,
|
||||||
IconColor::Warning => gpui2::red(),
|
IconColor::Warning => cx.theme().status().warning,
|
||||||
IconColor::Success => gpui2::red(),
|
IconColor::Success => cx.theme().status().success,
|
||||||
IconColor::Info => gpui2::red(),
|
IconColor::Info => cx.theme().status().info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
|
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
|
||||||
pub enum Icon {
|
pub enum Icon {
|
||||||
Ai,
|
Ai,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
@ -51,6 +49,7 @@ pub enum Icon {
|
||||||
AudioOff,
|
AudioOff,
|
||||||
AudioOn,
|
AudioOn,
|
||||||
Bolt,
|
Bolt,
|
||||||
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
@ -69,7 +68,6 @@ pub enum Icon {
|
||||||
Folder,
|
Folder,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
FolderX,
|
FolderX,
|
||||||
#[default]
|
|
||||||
Hash,
|
Hash,
|
||||||
InlayHint,
|
InlayHint,
|
||||||
MagicWand,
|
MagicWand,
|
||||||
|
@ -91,6 +89,11 @@ pub enum Icon {
|
||||||
XCircle,
|
XCircle,
|
||||||
Copilot,
|
Copilot,
|
||||||
Envelope,
|
Envelope,
|
||||||
|
Bell,
|
||||||
|
BellOff,
|
||||||
|
BellRing,
|
||||||
|
MailOpen,
|
||||||
|
AtSign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Icon {
|
impl Icon {
|
||||||
|
@ -103,6 +106,7 @@ impl Icon {
|
||||||
Icon::AudioOff => "icons/speaker-off.svg",
|
Icon::AudioOff => "icons/speaker-off.svg",
|
||||||
Icon::AudioOn => "icons/speaker-loud.svg",
|
Icon::AudioOn => "icons/speaker-loud.svg",
|
||||||
Icon::Bolt => "icons/bolt.svg",
|
Icon::Bolt => "icons/bolt.svg",
|
||||||
|
Icon::Check => "icons/check.svg",
|
||||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||||
|
@ -142,6 +146,11 @@ impl Icon {
|
||||||
Icon::XCircle => "icons/error.svg",
|
Icon::XCircle => "icons/error.svg",
|
||||||
Icon::Copilot => "icons/copilot.svg",
|
Icon::Copilot => "icons/copilot.svg",
|
||||||
Icon::Envelope => "icons/feedback.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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
crates/ui2/src/elements/indicator.rs
Normal file
23
crates/ui2/src/elements/indicator.rs
Normal file
|
@ -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))
|
.active(|style| style.bg(input_active_bg))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(
|
.child(div().flex().items_center().text_sm().map(|this| {
|
||||||
div()
|
if self.value.is_empty() {
|
||||||
.flex()
|
this.child(placeholder_label)
|
||||||
.items_center()
|
} else {
|
||||||
.text_sm()
|
this.child(label)
|
||||||
.when(self.value.is_empty(), |this| this.child(placeholder_label))
|
}
|
||||||
.when(!self.value.is_empty(), |this| this.child(label)),
|
}))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,11 @@ impl LabelColor {
|
||||||
match self {
|
match self {
|
||||||
Self::Default => cx.theme().colors().text,
|
Self::Default => cx.theme().colors().text,
|
||||||
Self::Muted => cx.theme().colors().text_muted,
|
Self::Muted => cx.theme().colors().text_muted,
|
||||||
Self::Created => gpui2::red(),
|
Self::Created => cx.theme().status().created,
|
||||||
Self::Modified => gpui2::red(),
|
Self::Modified => cx.theme().status().modified,
|
||||||
Self::Deleted => gpui2::red(),
|
Self::Deleted => cx.theme().status().deleted,
|
||||||
Self::Disabled => cx.theme().colors().text_disabled,
|
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::Placeholder => cx.theme().colors().text_placeholder,
|
||||||
Self::Accent => cx.theme().colors().text_accent,
|
Self::Accent => cx.theme().colors().text_accent,
|
||||||
}
|
}
|
||||||
|
@ -79,8 +79,7 @@ impl Label {
|
||||||
this.relative().child(
|
this.relative().child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_px()
|
.top_1_2()
|
||||||
.my_auto()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_px()
|
.h_px()
|
||||||
.bg(LabelColor::Hidden.hsla(cx)),
|
.bg(LabelColor::Hidden.hsla(cx)),
|
||||||
|
|
|
@ -23,6 +23,7 @@ mod elevation;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
mod static_data;
|
mod static_data;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
pub use elements::*;
|
pub use elements::*;
|
||||||
|
|
|
@ -10,6 +10,24 @@ pub use theme2::ActiveTheme;
|
||||||
use gpui2::Hsla;
|
use gpui2::Hsla;
|
||||||
use strum::EnumIter;
|
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)]
|
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||||
pub enum FileSystemStatus {
|
pub enum FileSystemStatus {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
use gpui2::{AppContext, ViewContext};
|
use gpui2::{AppContext, ViewContext};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
|
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
|
||||||
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
|
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
|
||||||
Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus,
|
Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
|
||||||
PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
|
PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
|
||||||
};
|
};
|
||||||
use crate::{HighlightedText, ListDetailsEntry};
|
use crate::{HighlightedText, ListDetailsEntry};
|
||||||
|
use crate::{ListItem, NotificationAction};
|
||||||
|
|
||||||
pub fn static_tabs_example() -> Vec<Tab> {
|
pub fn static_tabs_example() -> Vec<Tab> {
|
||||||
vec![
|
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![
|
vec![
|
||||||
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
|
Notification::new_icon_message(
|
||||||
.meta("4 people in stream."),
|
"notif-1",
|
||||||
ListDetailsEntry::new("nathansobo accepted your contact request."),
|
"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![
|
vec![
|
||||||
|
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![
|
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
|
||||||
Button::new("Decline"),
|
Button::new("Decline"),
|
||||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||||
]),
|
]),
|
||||||
|
),
|
||||||
|
ListItem::Details(
|
||||||
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
|
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
|
||||||
.seen(true)
|
.seen(true)
|
||||||
.meta("This stream has ended."),
|
.meta("This stream has ended."),
|
||||||
ListDetailsEntry::new("as-cii accepted your contact request."),
|
),
|
||||||
|
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()
|
.into_iter()
|
||||||
.map(From::from)
|
.map(From::from)
|
||||||
|
|
3
crates/ui2/src/utils.rs
Normal file
3
crates/ui2/src/utils.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod format_distance;
|
||||||
|
|
||||||
|
pub use format_distance::*;
|
231
crates/ui2/src/utils/format_distance.rs
Normal file
231
crates/ui2/src/utils/format_distance.rs
Normal file
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue