mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-04 05:46:48 +03:00
Merge remote-tracking branch 'origin/main' into zed2-workspace
This commit is contained in:
commit
0bab1a92df
45
.github/workflows/release_actions.yml
vendored
45
.github/workflows/release_actions.yml
vendored
@ -6,26 +6,27 @@ jobs:
|
||||
discord_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.2.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.3.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1603,7 +1603,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
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">
|
||||
<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>
|
||||
<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>
|
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 |
@ -153,10 +153,17 @@ impl FakeCompletionProvider {
|
||||
|
||||
pub fn send_completion(&self, completion: impl Into<String>) {
|
||||
let mut tx = self.last_completion_tx.lock();
|
||||
tx.as_mut().unwrap().try_send(completion.into()).unwrap();
|
||||
|
||||
println!("COMPLETION TX: {:?}", &tx);
|
||||
|
||||
let a = tx.as_mut().unwrap();
|
||||
a.try_send(completion.into()).unwrap();
|
||||
|
||||
// tx.as_mut().unwrap().try_send(completion.into()).unwrap();
|
||||
}
|
||||
|
||||
pub fn finish_completion(&self) {
|
||||
println!("FINISHING COMPLETION");
|
||||
self.last_completion_tx.lock().take().unwrap();
|
||||
}
|
||||
}
|
||||
@ -181,8 +188,10 @@ impl CompletionProvider for FakeCompletionProvider {
|
||||
&self,
|
||||
_prompt: Box<dyn CompletionRequest>,
|
||||
) -> BoxFuture<'static, anyhow::Result<BoxStream<'static, anyhow::Result<String>>>> {
|
||||
println!("COMPLETING");
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
*self.last_completion_tx.lock() = Some(tx);
|
||||
println!("TX: {:?}", *self.last_completion_tx.lock());
|
||||
async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
|
||||
}
|
||||
fn box_clone(&self) -> Box<dyn CompletionProvider> {
|
||||
|
@ -142,7 +142,7 @@ pub struct AssistantPanel {
|
||||
zoomed: bool,
|
||||
has_focus: bool,
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
completion_provider: Box<dyn CompletionProvider>,
|
||||
completion_provider: Arc<dyn CompletionProvider>,
|
||||
api_key_editor: Option<ViewHandle<Editor>>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@ -204,7 +204,7 @@ impl AssistantPanel {
|
||||
|
||||
let semantic_index = SemanticIndex::global(cx);
|
||||
// Defaulting currently to GPT4, allow for this to be set via config.
|
||||
let completion_provider = Box::new(OpenAICompletionProvider::new(
|
||||
let completion_provider = Arc::new(OpenAICompletionProvider::new(
|
||||
"gpt-4",
|
||||
cx.background().clone(),
|
||||
));
|
||||
@ -259,7 +259,13 @@ impl AssistantPanel {
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
|
||||
if this.update(cx, |assistant, _| assistant.has_credentials()) {
|
||||
if this.update(cx, |assistant, cx| {
|
||||
if !assistant.has_credentials() {
|
||||
assistant.load_credentials(cx);
|
||||
};
|
||||
|
||||
assistant.has_credentials()
|
||||
}) {
|
||||
this
|
||||
} else {
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
@ -320,13 +326,10 @@ impl AssistantPanel {
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
"gpt-4",
|
||||
cx.background().clone(),
|
||||
));
|
||||
let provider = self.completion_provider.clone();
|
||||
|
||||
// Retrieve Credentials Authenticates the Provider
|
||||
// provider.retrieve_credentials(cx);
|
||||
provider.retrieve_credentials(cx);
|
||||
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
@ -1439,7 +1442,7 @@ struct Conversation {
|
||||
pending_save: Task<Result<()>>,
|
||||
path: Option<PathBuf>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
completion_provider: Box<dyn CompletionProvider>,
|
||||
completion_provider: Arc<dyn CompletionProvider>,
|
||||
}
|
||||
|
||||
impl Entity for Conversation {
|
||||
@ -1450,7 +1453,7 @@ impl Conversation {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
completion_provider: Box<dyn CompletionProvider>,
|
||||
completion_provider: Arc<dyn CompletionProvider>,
|
||||
) -> Self {
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let buffer = cx.add_model(|cx| {
|
||||
@ -1544,7 +1547,7 @@ impl Conversation {
|
||||
None => Some(Uuid::new_v4().to_string()),
|
||||
};
|
||||
let model = saved_conversation.model;
|
||||
let completion_provider: Box<dyn CompletionProvider> = Box::new(
|
||||
let completion_provider: Arc<dyn CompletionProvider> = Arc::new(
|
||||
OpenAICompletionProvider::new(model.full_name(), cx.background().clone()),
|
||||
);
|
||||
completion_provider.retrieve_credentials(cx);
|
||||
@ -2201,7 +2204,7 @@ struct ConversationEditor {
|
||||
|
||||
impl ConversationEditor {
|
||||
fn new(
|
||||
completion_provider: Box<dyn CompletionProvider>,
|
||||
completion_provider: Arc<dyn CompletionProvider>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
@ -3406,7 +3409,7 @@ mod tests {
|
||||
init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
|
||||
let completion_provider = Box::new(FakeCompletionProvider::new());
|
||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||
let buffer = conversation.read(cx).buffer.clone();
|
||||
|
||||
@ -3535,7 +3538,7 @@ mod tests {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let completion_provider = Box::new(FakeCompletionProvider::new());
|
||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||
|
||||
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||
let buffer = conversation.read(cx).buffer.clone();
|
||||
@ -3633,7 +3636,7 @@ mod tests {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let completion_provider = Box::new(FakeCompletionProvider::new());
|
||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||
let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||
let buffer = conversation.read(cx).buffer.clone();
|
||||
|
||||
@ -3716,7 +3719,7 @@ mod tests {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let completion_provider = Box::new(FakeCompletionProvider::new());
|
||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||
let conversation =
|
||||
cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
|
||||
let buffer = conversation.read(cx).buffer.clone();
|
||||
|
@ -367,6 +367,8 @@ fn strip_invalid_spans_from_codeblock(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
@ -437,6 +439,7 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
deterministic.run_until_parked();
|
||||
@ -569,6 +572,7 @@ mod tests {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
deterministic.run_until_parked();
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.27.0"
|
||||
version = "0.28.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
@ -190,138 +190,142 @@ where
|
||||
.detach()
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use std::thread;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::thread;
|
||||
|
||||
// use sqlez::domain::Domain;
|
||||
// use sqlez_macros::sql;
|
||||
// use tempdir::TempDir;
|
||||
use sqlez::domain::Domain;
|
||||
use sqlez_macros::sql;
|
||||
use tempdir::TempDir;
|
||||
|
||||
// use crate::open_db;
|
||||
use crate::open_db;
|
||||
|
||||
// // Test bad migration panics
|
||||
// #[gpui::test]
|
||||
// #[should_panic]
|
||||
// async fn test_bad_migration_panics() {
|
||||
// enum BadDB {}
|
||||
// Test bad migration panics
|
||||
#[gpui2::test]
|
||||
#[should_panic]
|
||||
async fn test_bad_migration_panics() {
|
||||
enum BadDB {}
|
||||
|
||||
// impl Domain for BadDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
impl Domain for BadDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[
|
||||
// sql!(CREATE TABLE test(value);),
|
||||
// // failure because test already exists
|
||||
// sql!(CREATE TABLE test(value);),
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// }
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
}
|
||||
|
||||
// /// Test that DB exists but corrupted (causing recreate)
|
||||
// #[gpui::test]
|
||||
// async fn test_db_corruption() {
|
||||
// enum CorruptedDB {}
|
||||
/// Test that DB exists but corrupted (causing recreate)
|
||||
#[gpui2::test]
|
||||
async fn test_db_corruption(cx: &mut gpui2::TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
// impl Domain for CorruptedDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
enum CorruptedDB {}
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test(value);)]
|
||||
// }
|
||||
// }
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
// enum GoodDB {}
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
// impl Domain for GoodDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests" //Notice same name
|
||||
// }
|
||||
enum GoodDB {}
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
// }
|
||||
// }
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// {
|
||||
// let corrupt_db =
|
||||
// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(corrupt_db.persistent());
|
||||
// }
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
// let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(
|
||||
// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
// .unwrap()
|
||||
// .is_none()
|
||||
// );
|
||||
// }
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
{
|
||||
let corrupt_db =
|
||||
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(corrupt_db.persistent());
|
||||
}
|
||||
|
||||
// /// Test that DB exists but corrupted (causing recreate)
|
||||
// #[gpui::test(iterations = 30)]
|
||||
// async fn test_simultaneous_db_corruption() {
|
||||
// enum CorruptedDB {}
|
||||
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(
|
||||
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
// impl Domain for CorruptedDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
/// Test that DB exists but corrupted (causing recreate)
|
||||
#[gpui2::test(iterations = 30)]
|
||||
async fn test_simultaneous_db_corruption(cx: &mut gpui2::TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test(value);)]
|
||||
// }
|
||||
// }
|
||||
enum CorruptedDB {}
|
||||
|
||||
// enum GoodDB {}
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
// impl Domain for GoodDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests" //Notice same name
|
||||
// }
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
}
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
// }
|
||||
// }
|
||||
enum GoodDB {}
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// {
|
||||
// // Setup the bad database
|
||||
// let corrupt_db =
|
||||
// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(corrupt_db.persistent());
|
||||
// }
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
// // Try to connect to it a bunch of times at once
|
||||
// let mut guards = vec![];
|
||||
// for _ in 0..10 {
|
||||
// let tmp_path = tempdir.path().to_path_buf();
|
||||
// let guard = thread::spawn(move || {
|
||||
// let good_db = smol::block_on(open_db::<GoodDB>(
|
||||
// tmp_path.as_path(),
|
||||
// &util::channel::ReleaseChannel::Dev,
|
||||
// ));
|
||||
// assert!(
|
||||
// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
// .unwrap()
|
||||
// .is_none()
|
||||
// );
|
||||
// });
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
}
|
||||
|
||||
// guards.push(guard);
|
||||
// }
|
||||
let tempdir = TempDir::new("DbTests").unwrap();
|
||||
{
|
||||
// Setup the bad database
|
||||
let corrupt_db =
|
||||
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
assert!(corrupt_db.persistent());
|
||||
}
|
||||
|
||||
// for guard in guards.into_iter() {
|
||||
// assert!(guard.join().is_ok());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Try to connect to it a bunch of times at once
|
||||
let mut guards = vec![];
|
||||
for _ in 0..10 {
|
||||
let tmp_path = tempdir.path().to_path_buf();
|
||||
let guard = thread::spawn(move || {
|
||||
let good_db = smol::block_on(open_db::<GoodDB>(
|
||||
tmp_path.as_path(),
|
||||
&util::channel::ReleaseChannel::Dev,
|
||||
));
|
||||
assert!(
|
||||
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
});
|
||||
|
||||
guards.push(guard);
|
||||
}
|
||||
|
||||
for guard in guards.into_iter() {
|
||||
assert!(guard.join().is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,32 +31,32 @@ impl KeyValueStore {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use crate::kvp::KeyValueStore;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::kvp::KeyValueStore;
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_kvp() {
|
||||
// let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
||||
#[gpui2::test]
|
||||
async fn test_kvp() {
|
||||
let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
||||
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
|
||||
// db.write_kvp("key-1".to_string(), "one".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
||||
db.write_kvp("key-1".to_string(), "one".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
||||
|
||||
// db.write_kvp("key-1".to_string(), "one-2".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
||||
db.write_kvp("key-1".to_string(), "one-2".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
||||
|
||||
// db.write_kvp("key-2".to_string(), "two".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
||||
db.write_kvp("key-2".to_string(), "two".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
||||
|
||||
// db.delete_kvp("key-1".to_string()).await.unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
// }
|
||||
// }
|
||||
db.delete_kvp("key-1".to_string()).await.unwrap();
|
||||
assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
}
|
||||
}
|
||||
|
@ -198,14 +198,19 @@ impl<V> AnyElement<V> {
|
||||
pub trait Component<V> {
|
||||
fn render(self) -> AnyElement<V>;
|
||||
|
||||
fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
|
||||
where
|
||||
Self: Sized,
|
||||
U: Component<V>,
|
||||
{
|
||||
f(self)
|
||||
}
|
||||
|
||||
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if condition {
|
||||
self = then(self);
|
||||
}
|
||||
self
|
||||
self.map(|this| if condition { then(this) } else { this })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,9 @@ use crate::{
|
||||
Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformWindow, Point, PolychromeSprite,
|
||||
PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels,
|
||||
SceneBuilder, Shadow, SharedString, Size, Style, Subscription, TaffyLayoutEngine, Task,
|
||||
Underline, UnderlineStyle, View, VisualContext, WeakView, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription,
|
||||
TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView,
|
||||
WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
@ -53,6 +54,7 @@ pub enum DispatchPhase {
|
||||
Capture,
|
||||
}
|
||||
|
||||
type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||
type AnyListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
|
||||
type AnyKeyListener = Box<
|
||||
dyn Fn(
|
||||
@ -185,6 +187,10 @@ pub struct Window {
|
||||
default_prevented: bool,
|
||||
mouse_position: Point<Pixels>,
|
||||
scale_factor: f32,
|
||||
bounds: WindowBounds,
|
||||
bounds_observers: SubscriberSet<(), AnyObserver>,
|
||||
active: bool,
|
||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
pub(crate) scene_builder: SceneBuilder,
|
||||
pub(crate) dirty: bool,
|
||||
pub(crate) last_blur: Option<Option<FocusId>>,
|
||||
@ -203,16 +209,34 @@ impl Window {
|
||||
let mouse_position = platform_window.mouse_position();
|
||||
let content_size = platform_window.content_size();
|
||||
let scale_factor = platform_window.scale_factor();
|
||||
let bounds = platform_window.bounds();
|
||||
|
||||
platform_window.on_resize(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move |content_size, scale_factor| {
|
||||
move |_, _| {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| cx.window_bounds_changed())
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_moved(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move || {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| cx.window_bounds_changed())
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_active_status_change(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move |active| {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.window.scale_factor = scale_factor;
|
||||
cx.window.scene_builder = SceneBuilder::new();
|
||||
cx.window.content_size = content_size;
|
||||
cx.window.display_id = cx.window.platform_window.display().id();
|
||||
cx.window.dirty = true;
|
||||
cx.window.active = active;
|
||||
cx.window
|
||||
.activation_observers
|
||||
.clone()
|
||||
.retain(&(), |callback| callback(cx));
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@ -256,6 +280,10 @@ impl Window {
|
||||
default_prevented: true,
|
||||
mouse_position,
|
||||
scale_factor,
|
||||
bounds,
|
||||
bounds_observers: SubscriberSet::new(),
|
||||
active: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
scene_builder: SceneBuilder::new(),
|
||||
dirty: true,
|
||||
last_blur: None,
|
||||
@ -525,6 +553,23 @@ impl<'a> WindowContext<'a> {
|
||||
bounds
|
||||
}
|
||||
|
||||
fn window_bounds_changed(&mut self) {
|
||||
self.window.scale_factor = self.window.platform_window.scale_factor();
|
||||
self.window.content_size = self.window.platform_window.content_size();
|
||||
self.window.bounds = self.window.platform_window.bounds();
|
||||
self.window.display_id = self.window.platform_window.display().id();
|
||||
self.window.dirty = true;
|
||||
|
||||
self.window
|
||||
.bounds_observers
|
||||
.clone()
|
||||
.retain(&(), |callback| callback(self));
|
||||
}
|
||||
|
||||
pub fn window_bounds(&self) -> WindowBounds {
|
||||
self.window.bounds
|
||||
}
|
||||
|
||||
/// The scale factor of the display associated with the window. For example, it could
|
||||
/// return 2.0 for a "retina" display, indicating that each logical pixel should actually
|
||||
/// be rendered as two pixels on screen.
|
||||
@ -1717,6 +1762,28 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn observe_window_bounds(
|
||||
&mut self,
|
||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.view.downgrade();
|
||||
self.window.bounds_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn observe_window_activation(
|
||||
&mut self,
|
||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.view.downgrade();
|
||||
self.window.activation_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_focus_changed(
|
||||
&mut self,
|
||||
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
|
||||
|
@ -77,7 +77,7 @@ fn main() {
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds {
|
||||
origin: Default::default(),
|
||||
size: size(px(1700.), px(980.)).into(),
|
||||
size: size(px(1500.), px(780.)).into(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -64,6 +64,7 @@ pub struct ThemeColors {
|
||||
pub element_selected: Hsla,
|
||||
pub element_disabled: Hsla,
|
||||
pub element_placeholder: Hsla,
|
||||
pub element_drop_target: Hsla,
|
||||
pub ghost_element: Hsla,
|
||||
pub ghost_element_hover: Hsla,
|
||||
pub ghost_element_active: Hsla,
|
||||
@ -83,6 +84,8 @@ pub struct ThemeColors {
|
||||
pub title_bar: Hsla,
|
||||
pub toolbar: Hsla,
|
||||
pub tab_bar: Hsla,
|
||||
pub tab_inactive: Hsla,
|
||||
pub tab_active: Hsla,
|
||||
pub editor: Hsla,
|
||||
pub editor_subheader: Hsla,
|
||||
pub editor_active_line: Hsla,
|
||||
|
@ -9,6 +9,10 @@ use crate::{
|
||||
ColorScale,
|
||||
};
|
||||
|
||||
fn neutral() -> ColorScaleSet {
|
||||
slate()
|
||||
}
|
||||
|
||||
impl Default for SystemColors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -24,16 +28,16 @@ impl Default for StatusColors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
conflict: red().dark().step_11(),
|
||||
created: gpui2::black(),
|
||||
deleted: gpui2::black(),
|
||||
error: gpui2::black(),
|
||||
hidden: gpui2::black(),
|
||||
ignored: gpui2::black(),
|
||||
info: gpui2::black(),
|
||||
modified: gpui2::black(),
|
||||
renamed: gpui2::black(),
|
||||
success: gpui2::black(),
|
||||
warning: gpui2::black(),
|
||||
created: grass().dark().step_11(),
|
||||
deleted: red().dark().step_11(),
|
||||
error: red().dark().step_11(),
|
||||
hidden: neutral().dark().step_11(),
|
||||
ignored: neutral().dark().step_11(),
|
||||
info: blue().dark().step_11(),
|
||||
modified: yellow().dark().step_11(),
|
||||
renamed: blue().dark().step_11(),
|
||||
success: grass().dark().step_11(),
|
||||
warning: yellow().dark().step_11(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,12 +45,12 @@ impl Default for StatusColors {
|
||||
impl Default for GitStatusColors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
conflict: gpui2::rgba(0xdec184ff).into(),
|
||||
created: gpui2::rgba(0xa1c181ff).into(),
|
||||
deleted: gpui2::rgba(0xd07277ff).into(),
|
||||
ignored: gpui2::rgba(0x555a63ff).into(),
|
||||
modified: gpui2::rgba(0x74ade8ff).into(),
|
||||
renamed: gpui2::rgba(0xdec184ff).into(),
|
||||
conflict: orange().dark().step_11(),
|
||||
created: grass().dark().step_11(),
|
||||
deleted: red().dark().step_11(),
|
||||
ignored: neutral().dark().step_11(),
|
||||
modified: yellow().dark().step_11(),
|
||||
renamed: blue().dark().step_11(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,54 +86,57 @@ impl SyntaxTheme {
|
||||
pub fn default_light() -> Self {
|
||||
Self {
|
||||
highlights: vec![
|
||||
("attribute".into(), cyan().light().step_11().into()),
|
||||
("boolean".into(), tomato().light().step_11().into()),
|
||||
("comment".into(), neutral().light().step_11().into()),
|
||||
("comment.doc".into(), iris().light().step_12().into()),
|
||||
("constant".into(), red().light().step_7().into()),
|
||||
("constructor".into(), red().light().step_7().into()),
|
||||
("embedded".into(), red().light().step_7().into()),
|
||||
("emphasis".into(), red().light().step_7().into()),
|
||||
("emphasis.strong".into(), red().light().step_7().into()),
|
||||
("enum".into(), red().light().step_7().into()),
|
||||
("function".into(), red().light().step_7().into()),
|
||||
("hint".into(), red().light().step_7().into()),
|
||||
("keyword".into(), orange().light().step_11().into()),
|
||||
("label".into(), red().light().step_7().into()),
|
||||
("link_text".into(), red().light().step_7().into()),
|
||||
("link_uri".into(), red().light().step_7().into()),
|
||||
("number".into(), red().light().step_7().into()),
|
||||
("operator".into(), red().light().step_7().into()),
|
||||
("predictive".into(), red().light().step_7().into()),
|
||||
("preproc".into(), red().light().step_7().into()),
|
||||
("primary".into(), red().light().step_7().into()),
|
||||
("property".into(), red().light().step_7().into()),
|
||||
("punctuation".into(), neutral().light().step_11().into()),
|
||||
(
|
||||
"string.special.symbol".into(),
|
||||
gpui2::rgba(0xad6e26ff).into(),
|
||||
"punctuation.bracket".into(),
|
||||
neutral().light().step_11().into(),
|
||||
),
|
||||
("hint".into(), gpui2::rgba(0x9294beff).into()),
|
||||
("link_uri".into(), gpui2::rgba(0x3882b7ff).into()),
|
||||
("type".into(), gpui2::rgba(0x3882b7ff).into()),
|
||||
("string.regex".into(), gpui2::rgba(0xad6e26ff).into()),
|
||||
("constant".into(), gpui2::rgba(0x669f59ff).into()),
|
||||
("function".into(), gpui2::rgba(0x5b79e3ff).into()),
|
||||
("string.special".into(), gpui2::rgba(0xad6e26ff).into()),
|
||||
("punctuation.bracket".into(), gpui2::rgba(0x4d4f52ff).into()),
|
||||
("variable".into(), gpui2::rgba(0x383a41ff).into()),
|
||||
("punctuation".into(), gpui2::rgba(0x383a41ff).into()),
|
||||
("property".into(), gpui2::rgba(0xd3604fff).into()),
|
||||
("string".into(), gpui2::rgba(0x649f57ff).into()),
|
||||
("predictive".into(), gpui2::rgba(0x9b9ec6ff).into()),
|
||||
("attribute".into(), gpui2::rgba(0x5c78e2ff).into()),
|
||||
("number".into(), gpui2::rgba(0xad6e25ff).into()),
|
||||
("constructor".into(), gpui2::rgba(0x5c78e2ff).into()),
|
||||
("embedded".into(), gpui2::rgba(0x383a41ff).into()),
|
||||
("title".into(), gpui2::rgba(0xd3604fff).into()),
|
||||
("tag".into(), gpui2::rgba(0x5c78e2ff).into()),
|
||||
("boolean".into(), gpui2::rgba(0xad6e25ff).into()),
|
||||
(
|
||||
"punctuation.list_marker".into(),
|
||||
gpui2::rgba(0xd3604fff).into(),
|
||||
),
|
||||
("variant".into(), gpui2::rgba(0x5b79e3ff).into()),
|
||||
("emphasis".into(), gpui2::rgba(0x5c78e2ff).into()),
|
||||
("link_text".into(), gpui2::rgba(0x5b79e3ff).into()),
|
||||
("comment".into(), gpui2::rgba(0xa2a3a7ff).into()),
|
||||
("punctuation.special".into(), gpui2::rgba(0xb92b46ff).into()),
|
||||
("emphasis.strong".into(), gpui2::rgba(0xad6e25ff).into()),
|
||||
("primary".into(), gpui2::rgba(0x383a41ff).into()),
|
||||
(
|
||||
"punctuation.delimiter".into(),
|
||||
gpui2::rgba(0x4d4f52ff).into(),
|
||||
neutral().light().step_11().into(),
|
||||
),
|
||||
("label".into(), gpui2::rgba(0x5c78e2ff).into()),
|
||||
("keyword".into(), gpui2::rgba(0xa449abff).into()),
|
||||
("string.escape".into(), gpui2::rgba(0x7c7e86ff).into()),
|
||||
("text.literal".into(), gpui2::rgba(0x649f57ff).into()),
|
||||
("variable.special".into(), gpui2::rgba(0xad6e25ff).into()),
|
||||
("comment.doc".into(), gpui2::rgba(0x7c7e86ff).into()),
|
||||
("enum".into(), gpui2::rgba(0xd3604fff).into()),
|
||||
("operator".into(), gpui2::rgba(0x3882b7ff).into()),
|
||||
("preproc".into(), gpui2::rgba(0x383a41ff).into()),
|
||||
(
|
||||
"punctuation.list_marker".into(),
|
||||
blue().light().step_11().into(),
|
||||
),
|
||||
("punctuation.special".into(), red().light().step_7().into()),
|
||||
("string".into(), jade().light().step_11().into()),
|
||||
("string.escape".into(), red().light().step_7().into()),
|
||||
("string.regex".into(), tomato().light().step_11().into()),
|
||||
("string.special".into(), red().light().step_7().into()),
|
||||
(
|
||||
"string.special.symbol".into(),
|
||||
red().light().step_7().into(),
|
||||
),
|
||||
("tag".into(), red().light().step_7().into()),
|
||||
("text.literal".into(), red().light().step_7().into()),
|
||||
("title".into(), red().light().step_7().into()),
|
||||
("type".into(), red().light().step_7().into()),
|
||||
("variable".into(), red().light().step_7().into()),
|
||||
("variable.special".into(), red().light().step_7().into()),
|
||||
("variant".into(), red().light().step_7().into()),
|
||||
],
|
||||
}
|
||||
}
|
||||
@ -137,54 +144,54 @@ impl SyntaxTheme {
|
||||
pub fn default_dark() -> Self {
|
||||
Self {
|
||||
highlights: vec![
|
||||
("keyword".into(), gpui2::rgba(0xb477cfff).into()),
|
||||
("comment.doc".into(), gpui2::rgba(0x878e98ff).into()),
|
||||
("variant".into(), gpui2::rgba(0x73ade9ff).into()),
|
||||
("property".into(), gpui2::rgba(0xd07277ff).into()),
|
||||
("function".into(), gpui2::rgba(0x73ade9ff).into()),
|
||||
("type".into(), gpui2::rgba(0x6eb4bfff).into()),
|
||||
("tag".into(), gpui2::rgba(0x74ade8ff).into()),
|
||||
("string.escape".into(), gpui2::rgba(0x878e98ff).into()),
|
||||
("punctuation.bracket".into(), gpui2::rgba(0xb2b9c6ff).into()),
|
||||
("hint".into(), gpui2::rgba(0x5a6f89ff).into()),
|
||||
("punctuation".into(), gpui2::rgba(0xacb2beff).into()),
|
||||
("comment".into(), gpui2::rgba(0x5d636fff).into()),
|
||||
("emphasis".into(), gpui2::rgba(0x74ade8ff).into()),
|
||||
("punctuation.special".into(), gpui2::rgba(0xb1574bff).into()),
|
||||
("link_uri".into(), gpui2::rgba(0x6eb4bfff).into()),
|
||||
("string.regex".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
("constructor".into(), gpui2::rgba(0x73ade9ff).into()),
|
||||
("operator".into(), gpui2::rgba(0x6eb4bfff).into()),
|
||||
("constant".into(), gpui2::rgba(0xdfc184ff).into()),
|
||||
("string.special".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
("emphasis.strong".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
("attribute".into(), cyan().dark().step_11().into()),
|
||||
("boolean".into(), tomato().dark().step_11().into()),
|
||||
("comment".into(), neutral().dark().step_11().into()),
|
||||
("comment.doc".into(), iris().dark().step_12().into()),
|
||||
("constant".into(), red().dark().step_7().into()),
|
||||
("constructor".into(), red().dark().step_7().into()),
|
||||
("embedded".into(), red().dark().step_7().into()),
|
||||
("emphasis".into(), red().dark().step_7().into()),
|
||||
("emphasis.strong".into(), red().dark().step_7().into()),
|
||||
("enum".into(), red().dark().step_7().into()),
|
||||
("function".into(), red().dark().step_7().into()),
|
||||
("hint".into(), red().dark().step_7().into()),
|
||||
("keyword".into(), orange().dark().step_11().into()),
|
||||
("label".into(), red().dark().step_7().into()),
|
||||
("link_text".into(), red().dark().step_7().into()),
|
||||
("link_uri".into(), red().dark().step_7().into()),
|
||||
("number".into(), red().dark().step_7().into()),
|
||||
("operator".into(), red().dark().step_7().into()),
|
||||
("predictive".into(), red().dark().step_7().into()),
|
||||
("preproc".into(), red().dark().step_7().into()),
|
||||
("primary".into(), red().dark().step_7().into()),
|
||||
("property".into(), red().dark().step_7().into()),
|
||||
("punctuation".into(), neutral().dark().step_11().into()),
|
||||
(
|
||||
"string.special.symbol".into(),
|
||||
gpui2::rgba(0xbf956aff).into(),
|
||||
"punctuation.bracket".into(),
|
||||
neutral().dark().step_11().into(),
|
||||
),
|
||||
("primary".into(), gpui2::rgba(0xacb2beff).into()),
|
||||
("preproc".into(), gpui2::rgba(0xc8ccd4ff).into()),
|
||||
("string".into(), gpui2::rgba(0xa1c181ff).into()),
|
||||
(
|
||||
"punctuation.delimiter".into(),
|
||||
gpui2::rgba(0xb2b9c6ff).into(),
|
||||
neutral().dark().step_11().into(),
|
||||
),
|
||||
("embedded".into(), gpui2::rgba(0xc8ccd4ff).into()),
|
||||
("enum".into(), gpui2::rgba(0xd07277ff).into()),
|
||||
("variable.special".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
("text.literal".into(), gpui2::rgba(0xa1c181ff).into()),
|
||||
("attribute".into(), gpui2::rgba(0x74ade8ff).into()),
|
||||
("link_text".into(), gpui2::rgba(0x73ade9ff).into()),
|
||||
("title".into(), gpui2::rgba(0xd07277ff).into()),
|
||||
("predictive".into(), gpui2::rgba(0x5a6a87ff).into()),
|
||||
("number".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
("label".into(), gpui2::rgba(0x74ade8ff).into()),
|
||||
("variable".into(), gpui2::rgba(0xc8ccd4ff).into()),
|
||||
("boolean".into(), gpui2::rgba(0xbf956aff).into()),
|
||||
(
|
||||
"punctuation.list_marker".into(),
|
||||
gpui2::rgba(0xd07277ff).into(),
|
||||
blue().dark().step_11().into(),
|
||||
),
|
||||
("punctuation.special".into(), red().dark().step_7().into()),
|
||||
("string".into(), jade().dark().step_11().into()),
|
||||
("string.escape".into(), red().dark().step_7().into()),
|
||||
("string.regex".into(), tomato().dark().step_11().into()),
|
||||
("string.special".into(), red().dark().step_7().into()),
|
||||
("string.special.symbol".into(), red().dark().step_7().into()),
|
||||
("tag".into(), red().dark().step_7().into()),
|
||||
("text.literal".into(), red().dark().step_7().into()),
|
||||
("title".into(), red().dark().step_7().into()),
|
||||
("type".into(), red().dark().step_7().into()),
|
||||
("variable".into(), red().dark().step_7().into()),
|
||||
("variable.special".into(), red().dark().step_7().into()),
|
||||
("variant".into(), red().dark().step_7().into()),
|
||||
],
|
||||
}
|
||||
}
|
||||
@ -192,82 +199,92 @@ impl SyntaxTheme {
|
||||
|
||||
impl ThemeColors {
|
||||
pub fn default_light() -> Self {
|
||||
let system = SystemColors::default();
|
||||
|
||||
Self {
|
||||
border: gpui2::white(),
|
||||
border_variant: gpui2::white(),
|
||||
border_focused: gpui2::white(),
|
||||
border_transparent: gpui2::white(),
|
||||
elevated_surface: gpui2::white(),
|
||||
surface: gpui2::white(),
|
||||
background: gpui2::white(),
|
||||
element: gpui2::white(),
|
||||
element_hover: gpui2::white(),
|
||||
element_active: gpui2::white(),
|
||||
element_selected: gpui2::white(),
|
||||
element_disabled: gpui2::white(),
|
||||
element_placeholder: gpui2::white(),
|
||||
ghost_element: gpui2::white(),
|
||||
ghost_element_hover: gpui2::white(),
|
||||
ghost_element_active: gpui2::white(),
|
||||
ghost_element_selected: gpui2::white(),
|
||||
ghost_element_disabled: gpui2::white(),
|
||||
text: gpui2::white(),
|
||||
text_muted: gpui2::white(),
|
||||
text_placeholder: gpui2::white(),
|
||||
text_disabled: gpui2::white(),
|
||||
text_accent: gpui2::white(),
|
||||
icon: gpui2::white(),
|
||||
icon_muted: gpui2::white(),
|
||||
icon_disabled: gpui2::white(),
|
||||
icon_placeholder: gpui2::white(),
|
||||
icon_accent: gpui2::white(),
|
||||
status_bar: gpui2::white(),
|
||||
title_bar: gpui2::white(),
|
||||
toolbar: gpui2::white(),
|
||||
tab_bar: gpui2::white(),
|
||||
editor: gpui2::white(),
|
||||
editor_subheader: gpui2::white(),
|
||||
editor_active_line: gpui2::white(),
|
||||
border: neutral().light().step_6(),
|
||||
border_variant: neutral().light().step_5(),
|
||||
border_focused: blue().light().step_5(),
|
||||
border_transparent: system.transparent,
|
||||
elevated_surface: neutral().light().step_2(),
|
||||
surface: neutral().light().step_2(),
|
||||
background: neutral().light().step_1(),
|
||||
element: neutral().light().step_3(),
|
||||
element_hover: neutral().light().step_4(),
|
||||
element_active: neutral().light().step_5(),
|
||||
element_selected: neutral().light().step_5(),
|
||||
element_disabled: neutral().light_alpha().step_3(),
|
||||
element_placeholder: neutral().light().step_11(),
|
||||
element_drop_target: blue().light_alpha().step_2(),
|
||||
ghost_element: system.transparent,
|
||||
ghost_element_hover: neutral().light().step_4(),
|
||||
ghost_element_active: neutral().light().step_5(),
|
||||
ghost_element_selected: neutral().light().step_5(),
|
||||
ghost_element_disabled: neutral().light_alpha().step_3(),
|
||||
text: neutral().light().step_12(),
|
||||
text_muted: neutral().light().step_11(),
|
||||
text_placeholder: neutral().light().step_10(),
|
||||
text_disabled: neutral().light().step_9(),
|
||||
text_accent: blue().light().step_11(),
|
||||
icon: neutral().light().step_11(),
|
||||
icon_muted: neutral().light().step_10(),
|
||||
icon_disabled: neutral().light().step_9(),
|
||||
icon_placeholder: neutral().light().step_10(),
|
||||
icon_accent: blue().light().step_11(),
|
||||
status_bar: neutral().light().step_2(),
|
||||
title_bar: neutral().light().step_2(),
|
||||
toolbar: neutral().light().step_1(),
|
||||
tab_bar: neutral().light().step_2(),
|
||||
tab_active: neutral().light().step_1(),
|
||||
tab_inactive: neutral().light().step_2(),
|
||||
editor: neutral().light().step_1(),
|
||||
editor_subheader: neutral().light().step_2(),
|
||||
editor_active_line: neutral().light_alpha().step_3(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_dark() -> Self {
|
||||
let system = SystemColors::default();
|
||||
|
||||
Self {
|
||||
border: gpui2::rgba(0x464b57ff).into(),
|
||||
border_variant: gpui2::rgba(0x464b57ff).into(),
|
||||
border_focused: gpui2::rgba(0x293b5bff).into(),
|
||||
border_transparent: gpui2::rgba(0x00000000).into(),
|
||||
elevated_surface: gpui2::rgba(0x3b414dff).into(),
|
||||
surface: gpui2::rgba(0x2f343eff).into(),
|
||||
background: gpui2::rgba(0x3b414dff).into(),
|
||||
element: gpui2::rgba(0x3b414dff).into(),
|
||||
element_hover: gpui2::rgba(0xffffff1e).into(),
|
||||
element_active: gpui2::rgba(0xffffff28).into(),
|
||||
element_selected: gpui2::rgba(0x18243dff).into(),
|
||||
element_disabled: gpui2::rgba(0x00000000).into(),
|
||||
element_placeholder: gpui2::black(),
|
||||
ghost_element: gpui2::rgba(0x00000000).into(),
|
||||
ghost_element_hover: gpui2::rgba(0xffffff14).into(),
|
||||
ghost_element_active: gpui2::rgba(0xffffff1e).into(),
|
||||
ghost_element_selected: gpui2::rgba(0x18243dff).into(),
|
||||
ghost_element_disabled: gpui2::rgba(0x00000000).into(),
|
||||
text: gpui2::rgba(0xc8ccd4ff).into(),
|
||||
text_muted: gpui2::rgba(0x838994ff).into(),
|
||||
text_placeholder: gpui2::rgba(0xd07277ff).into(),
|
||||
text_disabled: gpui2::rgba(0x555a63ff).into(),
|
||||
text_accent: gpui2::rgba(0x74ade8ff).into(),
|
||||
icon: gpui2::black(),
|
||||
icon_muted: gpui2::rgba(0x838994ff).into(),
|
||||
icon_disabled: gpui2::black(),
|
||||
icon_placeholder: gpui2::black(),
|
||||
icon_accent: gpui2::black(),
|
||||
status_bar: gpui2::rgba(0x3b414dff).into(),
|
||||
title_bar: gpui2::rgba(0x3b414dff).into(),
|
||||
toolbar: gpui2::rgba(0x282c33ff).into(),
|
||||
tab_bar: gpui2::rgba(0x2f343eff).into(),
|
||||
editor: gpui2::rgba(0x282c33ff).into(),
|
||||
editor_subheader: gpui2::rgba(0x2f343eff).into(),
|
||||
editor_active_line: gpui2::rgba(0x2f343eff).into(),
|
||||
border: neutral().dark().step_6(),
|
||||
border_variant: neutral().dark().step_5(),
|
||||
border_focused: blue().dark().step_5(),
|
||||
border_transparent: system.transparent,
|
||||
elevated_surface: neutral().dark().step_2(),
|
||||
surface: neutral().dark().step_2(),
|
||||
background: neutral().dark().step_1(),
|
||||
element: neutral().dark().step_3(),
|
||||
element_hover: neutral().dark().step_4(),
|
||||
element_active: neutral().dark().step_5(),
|
||||
element_selected: neutral().dark().step_5(),
|
||||
element_disabled: neutral().dark_alpha().step_3(),
|
||||
element_placeholder: neutral().dark().step_11(),
|
||||
element_drop_target: blue().dark_alpha().step_2(),
|
||||
ghost_element: system.transparent,
|
||||
ghost_element_hover: neutral().dark().step_4(),
|
||||
ghost_element_active: neutral().dark().step_5(),
|
||||
ghost_element_selected: neutral().dark().step_5(),
|
||||
ghost_element_disabled: neutral().dark_alpha().step_3(),
|
||||
text: neutral().dark().step_12(),
|
||||
text_muted: neutral().dark().step_11(),
|
||||
text_placeholder: neutral().dark().step_10(),
|
||||
text_disabled: neutral().dark().step_9(),
|
||||
text_accent: blue().dark().step_11(),
|
||||
icon: neutral().dark().step_11(),
|
||||
icon_muted: neutral().dark().step_10(),
|
||||
icon_disabled: neutral().dark().step_9(),
|
||||
icon_placeholder: neutral().dark().step_10(),
|
||||
icon_accent: blue().dark().step_11(),
|
||||
status_bar: neutral().dark().step_2(),
|
||||
title_bar: neutral().dark().step_2(),
|
||||
toolbar: neutral().dark().step_1(),
|
||||
tab_bar: neutral().dark().step_2(),
|
||||
tab_active: neutral().dark().step_1(),
|
||||
tab_inactive: neutral().dark().step_2(),
|
||||
editor: neutral().dark().step_1(),
|
||||
editor_subheader: neutral().dark().step_2(),
|
||||
editor_active_line: neutral().dark_alpha().step_3(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,18 @@ impl ThemeVariant {
|
||||
&self.styles.syntax
|
||||
}
|
||||
|
||||
/// Returns the [`StatusColors`] for the theme.
|
||||
#[inline(always)]
|
||||
pub fn status(&self) -> &StatusColors {
|
||||
&self.styles.status
|
||||
}
|
||||
|
||||
/// Returns the [`GitStatusColors`] for the theme.
|
||||
#[inline(always)]
|
||||
pub fn git(&self) -> &GitStatusColors {
|
||||
&self.styles.git
|
||||
}
|
||||
|
||||
/// Returns the color for the syntax node with the given name.
|
||||
#[inline(always)]
|
||||
pub fn syntax_color(&self, name: &str) -> Hsla {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use gpui2::{div, relative, Div};
|
||||
use gpui2::{div, px, relative, Div};
|
||||
|
||||
use crate::settings::user_settings;
|
||||
use crate::{
|
||||
@ -15,12 +15,20 @@ pub enum ListItemVariant {
|
||||
Inset,
|
||||
}
|
||||
|
||||
pub enum ListHeaderMeta {
|
||||
// TODO: These should be IconButtons
|
||||
Tools(Vec<Icon>),
|
||||
// TODO: This should be a button
|
||||
Button(Label),
|
||||
Text(Label),
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ListHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
meta: Option<ListHeaderMeta>,
|
||||
variant: ListItemVariant,
|
||||
state: InteractionState,
|
||||
toggleable: Toggleable,
|
||||
}
|
||||
|
||||
@ -29,9 +37,9 @@ impl ListHeader {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
meta: None,
|
||||
variant: ListItemVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
toggleable: Toggleable::Toggleable(ToggleState::Toggled),
|
||||
toggleable: Toggleable::NotToggleable,
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +58,8 @@ impl ListHeader {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
|
||||
self.meta = meta;
|
||||
self
|
||||
}
|
||||
|
||||
@ -74,34 +82,36 @@ impl ListHeader {
|
||||
}
|
||||
}
|
||||
|
||||
fn label_color(&self) -> LabelColor {
|
||||
match self.state {
|
||||
InteractionState::Disabled => LabelColor::Disabled,
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_color(&self) -> IconColor {
|
||||
match self.state {
|
||||
InteractionState::Disabled => IconColor::Disabled,
|
||||
_ => Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
|
||||
let is_toggled = self.toggleable.is_toggled();
|
||||
|
||||
let disclosure_control = self.disclosure_control();
|
||||
|
||||
let meta = match self.meta {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.children(icons.into_iter().map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
})),
|
||||
),
|
||||
Some(ListHeaderMeta::Button(label)) => div().child(label),
|
||||
Some(ListHeaderMeta::Text(label)) => div().child(label),
|
||||
None => div(),
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().surface)
|
||||
.when(self.state == InteractionState::Focused, |this| {
|
||||
this.border()
|
||||
.border_color(cx.theme().colors().border_focused)
|
||||
})
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
@ -109,22 +119,28 @@ impl ListHeader {
|
||||
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(LabelColor::Muted)),
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(IconColor::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(Label::new(self.label.clone()).color(LabelColor::Muted)),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
)
|
||||
.child(disclosure_control),
|
||||
.child(meta),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -473,42 +489,63 @@ impl<V: 'static> ListDetailsEntry<V> {
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
let settings = user_settings(cx);
|
||||
|
||||
let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
|
||||
true => (
|
||||
cx.theme().colors().ghost_element,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
false => (
|
||||
cx.theme().colors().element,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
};
|
||||
let (item_bg, item_bg_hover, item_bg_active) = (
|
||||
cx.theme().colors().ghost_element,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
);
|
||||
|
||||
let label_color = match self.seen {
|
||||
true => LabelColor::Muted,
|
||||
false => LabelColor::Default,
|
||||
};
|
||||
|
||||
v_stack()
|
||||
div()
|
||||
.relative()
|
||||
.group("")
|
||||
.bg(item_bg)
|
||||
.px_1()
|
||||
.py_1_5()
|
||||
.px_2()
|
||||
.py_1p5()
|
||||
.w_full()
|
||||
.line_height(relative(1.2))
|
||||
.child(Label::new(self.label.clone()).color(label_color))
|
||||
.children(
|
||||
self.meta
|
||||
.map(|meta| Label::new(meta).color(LabelColor::Muted)),
|
||||
)
|
||||
.z_index(1)
|
||||
.when(!self.seen, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left(px(3.0))
|
||||
.top_3()
|
||||
.rounded_full()
|
||||
.border_2()
|
||||
.border_color(cx.theme().colors().surface)
|
||||
.w(px(9.0))
|
||||
.h(px(9.0))
|
||||
.z_index(2)
|
||||
.bg(cx.theme().status().info),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
v_stack()
|
||||
.w_full()
|
||||
.line_height(relative(1.2))
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.children(self.actions.unwrap_or_default()),
|
||||
.child(
|
||||
div()
|
||||
.w_5()
|
||||
.h_5()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().icon_accent),
|
||||
)
|
||||
.child(Label::new(self.label.clone()).color(label_color))
|
||||
.children(
|
||||
self.meta
|
||||
.map(|meta| Label::new(meta).color(LabelColor::Muted)),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.children(self.actions.unwrap_or_default()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -522,7 +559,7 @@ impl ListSeparator {
|
||||
}
|
||||
|
||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border)
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,14 +601,15 @@ impl<V: 'static> List<V> {
|
||||
let is_toggled = Toggleable::is_toggled(&self.toggleable);
|
||||
|
||||
let list_content = match (self.items.is_empty(), is_toggled) {
|
||||
(_, false) => div(),
|
||||
(false, _) => div().children(self.items),
|
||||
(true, _) => {
|
||||
(true, false) => div(),
|
||||
(true, true) => {
|
||||
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
|
||||
}
|
||||
};
|
||||
|
||||
v_stack()
|
||||
.w_full()
|
||||
.py_1()
|
||||
.children(self.header.map(|header| header.toggleable(self.toggleable)))
|
||||
.child(list_content)
|
||||
|
@ -1,5 +1,10 @@
|
||||
use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
|
||||
use crate::{List, ListHeader};
|
||||
use crate::utils::naive_format_distance_from_now;
|
||||
use crate::{
|
||||
h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon,
|
||||
IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
|
||||
UnreadIndicator,
|
||||
};
|
||||
use crate::{ClickHandler, ListHeader};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct NotificationsPanel {
|
||||
@ -16,31 +21,348 @@ impl NotificationsPanel {
|
||||
.id(self.id.clone())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().surface)
|
||||
.child(
|
||||
div()
|
||||
.id("header")
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
|
||||
Icon::AtSign,
|
||||
Icon::BellOff,
|
||||
Icon::MailOpen,
|
||||
]))),
|
||||
)
|
||||
.child(ListSeparator::new())
|
||||
.child(
|
||||
v_stack()
|
||||
.id("notifications-panel-scroll-view")
|
||||
.py_1()
|
||||
.overflow_y_scroll()
|
||||
.flex_1()
|
||||
.child(
|
||||
List::new(static_new_notification_items())
|
||||
.header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
|
||||
.toggle(ToggleState::Toggled),
|
||||
div()
|
||||
.mx_2()
|
||||
.p_1()
|
||||
// TODO: Add cursor style
|
||||
// .cursor(Cursor::IBeam)
|
||||
.bg(cx.theme().colors().element)
|
||||
.border()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new("Search...")
|
||||
.color(LabelColor::Placeholder)
|
||||
.line_height_style(LineHeightStyle::UILabel),
|
||||
),
|
||||
)
|
||||
.child(v_stack().px_1().children(static_new_notification_items_2())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ButtonOrIconButton<V: 'static> {
|
||||
Button(Button<V>),
|
||||
IconButton(IconButton<V>),
|
||||
}
|
||||
|
||||
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
|
||||
fn from(value: Button<V>) -> Self {
|
||||
Self::Button(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
|
||||
fn from(value: IconButton<V>) -> Self {
|
||||
Self::IconButton(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationAction<V: 'static> {
|
||||
button: ButtonOrIconButton<V>,
|
||||
tooltip: SharedString,
|
||||
/// Shows after action is chosen
|
||||
///
|
||||
/// For example, if the action is "Accept" the taken message could be:
|
||||
///
|
||||
/// - `(None,"Accepted")` - "Accepted"
|
||||
///
|
||||
/// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
|
||||
taken_message: (Option<Icon>, SharedString),
|
||||
}
|
||||
|
||||
impl<V: 'static> NotificationAction<V> {
|
||||
pub fn new(
|
||||
button: impl Into<ButtonOrIconButton<V>>,
|
||||
tooltip: impl Into<SharedString>,
|
||||
(icon, taken_message): (Option<Icon>, impl Into<SharedString>),
|
||||
) -> Self {
|
||||
Self {
|
||||
button: button.into(),
|
||||
tooltip: tooltip.into(),
|
||||
taken_message: (icon, taken_message.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActorOrIcon {
|
||||
Actor(PublicActor),
|
||||
Icon(Icon),
|
||||
}
|
||||
|
||||
pub struct NotificationMeta<V: 'static> {
|
||||
items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
|
||||
}
|
||||
|
||||
struct NotificationHandlers<V: 'static> {
|
||||
click: Option<ClickHandler<V>>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Default for NotificationHandlers<V> {
|
||||
fn default() -> Self {
|
||||
Self { click: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Notification<V: 'static> {
|
||||
id: ElementId,
|
||||
slot: ActorOrIcon,
|
||||
message: SharedString,
|
||||
date_received: NaiveDateTime,
|
||||
meta: Option<NotificationMeta<V>>,
|
||||
actions: Option<[NotificationAction<V>; 2]>,
|
||||
unread: bool,
|
||||
new: bool,
|
||||
action_taken: Option<NotificationAction<V>>,
|
||||
handlers: NotificationHandlers<V>,
|
||||
}
|
||||
|
||||
impl<V> Notification<V> {
|
||||
fn new(
|
||||
id: ElementId,
|
||||
message: SharedString,
|
||||
date_received: NaiveDateTime,
|
||||
slot: ActorOrIcon,
|
||||
click_action: Option<ClickHandler<V>>,
|
||||
) -> Self {
|
||||
let handlers = if click_action.is_some() {
|
||||
NotificationHandlers {
|
||||
click: click_action,
|
||||
}
|
||||
} else {
|
||||
NotificationHandlers::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
date_received,
|
||||
message,
|
||||
meta: None,
|
||||
slot,
|
||||
actions: None,
|
||||
unread: true,
|
||||
new: false,
|
||||
action_taken: None,
|
||||
handlers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new notification with an actor slot.
|
||||
///
|
||||
/// Requires a click action.
|
||||
pub fn new_actor_message(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
actor: PublicActor,
|
||||
click_action: ClickHandler<V>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Actor(actor),
|
||||
Some(click_action),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an icon slot.
|
||||
///
|
||||
/// Requires a click action.
|
||||
pub fn new_icon_message(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
icon: Icon,
|
||||
click_action: ClickHandler<V>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Icon(icon),
|
||||
Some(click_action),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an actor slot
|
||||
/// and a Call To Action row.
|
||||
///
|
||||
/// Cannot take a click action due to required actions.
|
||||
pub fn new_actor_with_actions(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
actor: PublicActor,
|
||||
actions: [NotificationAction<V>; 2],
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Actor(actor),
|
||||
None,
|
||||
)
|
||||
.actions(actions)
|
||||
}
|
||||
|
||||
/// Creates a new notification with an icon slot
|
||||
/// and a Call To Action row.
|
||||
///
|
||||
/// Cannot take a click action due to required actions.
|
||||
pub fn new_icon_with_actions(
|
||||
id: impl Into<ElementId>,
|
||||
message: impl Into<SharedString>,
|
||||
date_received: NaiveDateTime,
|
||||
icon: Icon,
|
||||
actions: [NotificationAction<V>; 2],
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id.into(),
|
||||
message.into(),
|
||||
date_received,
|
||||
ActorOrIcon::Icon(icon),
|
||||
None,
|
||||
)
|
||||
.actions(actions)
|
||||
}
|
||||
|
||||
fn on_click(mut self, handler: ClickHandler<V>) -> Self {
|
||||
self.handlers.click = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
|
||||
self.actions = Some(actions);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
|
||||
self.meta = Some(meta);
|
||||
self
|
||||
}
|
||||
|
||||
fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
if let Some(meta) = &self.meta {
|
||||
h_stack().children(
|
||||
meta.items
|
||||
.iter()
|
||||
.map(|(icon, text, _)| {
|
||||
let mut meta_el = div();
|
||||
if let Some(icon) = icon {
|
||||
meta_el = meta_el.child(IconElement::new(icon.clone()));
|
||||
}
|
||||
meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
match &self.slot {
|
||||
ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
|
||||
ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||
div()
|
||||
.relative()
|
||||
.id(self.id.clone())
|
||||
.p_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.children(
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.left(px(3.0))
|
||||
.top_3()
|
||||
.z_index(2)
|
||||
.child(UnreadIndicator::new()),
|
||||
)
|
||||
.filter(|_| self.unread),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.z_index(1)
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(self.render_slot(cx))
|
||||
.child(div().flex_1().child(Label::new(self.message.clone()))),
|
||||
)
|
||||
.child(
|
||||
List::new(static_read_notification_items())
|
||||
.header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
|
||||
.empty_message("No new notifications")
|
||||
.toggle(ToggleState::Toggled),
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(naive_format_distance_from_now(
|
||||
self.date_received,
|
||||
true,
|
||||
true,
|
||||
))
|
||||
.color(LabelColor::Muted),
|
||||
)
|
||||
.child(self.render_meta_items(cx)),
|
||||
)
|
||||
.child(match (self.actions, self.action_taken) {
|
||||
// Show nothing
|
||||
(None, _) => div(),
|
||||
// Show the taken_message
|
||||
(Some(_), Some(action_taken)) => h_stack()
|
||||
.children(action_taken.taken_message.0.map(|icon| {
|
||||
IconElement::new(icon).color(crate::IconColor::Muted)
|
||||
}))
|
||||
.child(
|
||||
Label::new(action_taken.taken_message.1.clone())
|
||||
.color(LabelColor::Muted),
|
||||
),
|
||||
// Show the actions
|
||||
(Some(actions), None) => {
|
||||
h_stack().children(actions.map(|action| match action.button {
|
||||
ButtonOrIconButton::Button(button) => {
|
||||
Component::render(button)
|
||||
}
|
||||
ButtonOrIconButton::IconButton(icon_button) => {
|
||||
Component::render(icon_button)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use gpui2::{px, Styled};
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
|
||||
|
@ -98,16 +98,14 @@ impl<V: 'static> Panel<V> {
|
||||
v_stack()
|
||||
.id(self.id.clone())
|
||||
.flex_initial()
|
||||
.when(
|
||||
self.current_side == PanelSide::Left || self.current_side == PanelSide::Right,
|
||||
|this| this.h_full().w(current_size),
|
||||
)
|
||||
.when(self.current_side == PanelSide::Left, |this| this.border_r())
|
||||
.when(self.current_side == PanelSide::Right, |this| {
|
||||
this.border_l()
|
||||
.map(|this| match self.current_side {
|
||||
PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
|
||||
PanelSide::Bottom => this,
|
||||
})
|
||||
.when(self.current_side == PanelSide::Bottom, |this| {
|
||||
this.border_b().w_full().h(current_size)
|
||||
.map(|this| match self.current_side {
|
||||
PanelSide::Left => this.border_r(),
|
||||
PanelSide::Right => this.border_l(),
|
||||
PanelSide::Bottom => this.border_b().w_full().h(current_size),
|
||||
})
|
||||
.bg(cx.theme().colors().surface)
|
||||
.border_color(cx.theme().colors().border)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::{Icon, IconColor, IconElement, Label, LabelColor};
|
||||
use gpui2::{black, red, Div, ElementId, Render, View, VisualContext};
|
||||
use gpui2::{red, Div, ElementId, Render, View, VisualContext};
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
pub struct Tab {
|
||||
@ -108,13 +108,13 @@ impl Tab {
|
||||
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
|
||||
|
||||
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
|
||||
true => (
|
||||
cx.theme().colors().ghost_element,
|
||||
false => (
|
||||
cx.theme().colors().tab_inactive,
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
),
|
||||
false => (
|
||||
cx.theme().colors().element,
|
||||
true => (
|
||||
cx.theme().colors().tab_active,
|
||||
cx.theme().colors().element_hover,
|
||||
cx.theme().colors().element_active,
|
||||
),
|
||||
@ -127,7 +127,7 @@ impl Tab {
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
|
||||
.drag_over::<TabDragState>(|d| d.bg(black()))
|
||||
.drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().element_drop_target))
|
||||
.on_drop(|_view, state: View<TabDragState>, cx| {
|
||||
eprintln!("{:?}", state.read(cx));
|
||||
})
|
||||
@ -144,7 +144,7 @@ impl Tab {
|
||||
.px_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.children(has_fs_conflict.then(|| {
|
||||
IconElement::new(Icon::ExclamationTriangle)
|
||||
.size(crate::IconSize::Small)
|
||||
|
@ -27,6 +27,7 @@ impl TabBar {
|
||||
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
|
||||
|
||||
div()
|
||||
.group("tab_bar")
|
||||
.id(self.id.clone())
|
||||
.w_full()
|
||||
.flex()
|
||||
@ -34,6 +35,7 @@ impl TabBar {
|
||||
// Left Side
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_none()
|
||||
@ -41,6 +43,7 @@ impl TabBar {
|
||||
// Nav Buttons
|
||||
.child(
|
||||
div()
|
||||
.right_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_px()
|
||||
@ -67,10 +70,15 @@ impl TabBar {
|
||||
// Right Side
|
||||
.child(
|
||||
div()
|
||||
// We only use absolute here since we don't
|
||||
// have opacity or `hidden()` yet
|
||||
.absolute()
|
||||
.neg_top_7()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.gap_2()
|
||||
.group_hover("tab_bar", |this| this.top_0())
|
||||
// Nav Buttons
|
||||
.child(
|
||||
div()
|
||||
|
@ -2,6 +2,7 @@ mod avatar;
|
||||
mod button;
|
||||
mod details;
|
||||
mod icon;
|
||||
mod indicator;
|
||||
mod input;
|
||||
mod label;
|
||||
mod player;
|
||||
@ -12,6 +13,7 @@ pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use details::*;
|
||||
pub use icon::*;
|
||||
pub use indicator::*;
|
||||
pub use input::*;
|
||||
pub use label::*;
|
||||
pub use player::*;
|
||||
|
@ -26,23 +26,21 @@ pub enum IconColor {
|
||||
|
||||
impl IconColor {
|
||||
pub fn color(self, cx: &WindowContext) -> Hsla {
|
||||
let theme_colors = cx.theme().colors();
|
||||
|
||||
match self {
|
||||
IconColor::Default => theme_colors.icon,
|
||||
IconColor::Muted => theme_colors.icon_muted,
|
||||
IconColor::Disabled => theme_colors.icon_disabled,
|
||||
IconColor::Placeholder => theme_colors.icon_placeholder,
|
||||
IconColor::Accent => theme_colors.icon_accent,
|
||||
IconColor::Error => gpui2::red(),
|
||||
IconColor::Warning => gpui2::red(),
|
||||
IconColor::Success => gpui2::red(),
|
||||
IconColor::Info => gpui2::red(),
|
||||
IconColor::Default => cx.theme().colors().icon,
|
||||
IconColor::Muted => cx.theme().colors().icon_muted,
|
||||
IconColor::Disabled => cx.theme().colors().icon_disabled,
|
||||
IconColor::Placeholder => cx.theme().colors().icon_placeholder,
|
||||
IconColor::Accent => cx.theme().colors().icon_accent,
|
||||
IconColor::Error => cx.theme().status().error,
|
||||
IconColor::Warning => cx.theme().status().warning,
|
||||
IconColor::Success => cx.theme().status().success,
|
||||
IconColor::Info => cx.theme().status().info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
|
||||
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
|
||||
pub enum Icon {
|
||||
Ai,
|
||||
ArrowLeft,
|
||||
@ -51,6 +49,7 @@ pub enum Icon {
|
||||
AudioOff,
|
||||
AudioOn,
|
||||
Bolt,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
@ -69,7 +68,6 @@ pub enum Icon {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
#[default]
|
||||
Hash,
|
||||
InlayHint,
|
||||
MagicWand,
|
||||
@ -91,6 +89,11 @@ pub enum Icon {
|
||||
XCircle,
|
||||
Copilot,
|
||||
Envelope,
|
||||
Bell,
|
||||
BellOff,
|
||||
BellRing,
|
||||
MailOpen,
|
||||
AtSign,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
@ -103,6 +106,7 @@ impl Icon {
|
||||
Icon::AudioOff => "icons/speaker-off.svg",
|
||||
Icon::AudioOn => "icons/speaker-loud.svg",
|
||||
Icon::Bolt => "icons/bolt.svg",
|
||||
Icon::Check => "icons/check.svg",
|
||||
Icon::ChevronDown => "icons/chevron_down.svg",
|
||||
Icon::ChevronLeft => "icons/chevron_left.svg",
|
||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||
@ -142,6 +146,11 @@ impl Icon {
|
||||
Icon::XCircle => "icons/error.svg",
|
||||
Icon::Copilot => "icons/copilot.svg",
|
||||
Icon::Envelope => "icons/feedback.svg",
|
||||
Icon::Bell => "icons/bell.svg",
|
||||
Icon::BellOff => "icons/bell-off.svg",
|
||||
Icon::BellRing => "icons/bell-ring.svg",
|
||||
Icon::MailOpen => "icons/mail-open.svg",
|
||||
Icon::AtSign => "icons/at-sign.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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))
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.text_sm()
|
||||
.when(self.value.is_empty(), |this| this.child(placeholder_label))
|
||||
.when(!self.value.is_empty(), |this| this.child(label)),
|
||||
)
|
||||
.child(div().flex().items_center().text_sm().map(|this| {
|
||||
if self.value.is_empty() {
|
||||
this.child(placeholder_label)
|
||||
} else {
|
||||
this.child(label)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,11 +21,11 @@ impl LabelColor {
|
||||
match self {
|
||||
Self::Default => cx.theme().colors().text,
|
||||
Self::Muted => cx.theme().colors().text_muted,
|
||||
Self::Created => gpui2::red(),
|
||||
Self::Modified => gpui2::red(),
|
||||
Self::Deleted => gpui2::red(),
|
||||
Self::Created => cx.theme().status().created,
|
||||
Self::Modified => cx.theme().status().modified,
|
||||
Self::Deleted => cx.theme().status().deleted,
|
||||
Self::Disabled => cx.theme().colors().text_disabled,
|
||||
Self::Hidden => gpui2::red(),
|
||||
Self::Hidden => cx.theme().status().hidden,
|
||||
Self::Placeholder => cx.theme().colors().text_placeholder,
|
||||
Self::Accent => cx.theme().colors().text_accent,
|
||||
}
|
||||
@ -79,8 +79,7 @@ impl Label {
|
||||
this.relative().child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_px()
|
||||
.my_auto()
|
||||
.top_1_2()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(LabelColor::Hidden.hsla(cx)),
|
||||
|
@ -23,6 +23,7 @@ mod elevation;
|
||||
pub mod prelude;
|
||||
pub mod settings;
|
||||
mod static_data;
|
||||
pub mod utils;
|
||||
|
||||
pub use components::*;
|
||||
pub use elements::*;
|
||||
|
@ -10,6 +10,24 @@ pub use theme2::ActiveTheme;
|
||||
use gpui2::Hsla;
|
||||
use strum::EnumIter;
|
||||
|
||||
/// Represents a person with a Zed account's public profile.
|
||||
/// All data in this struct should be considered public.
|
||||
pub struct PublicActor {
|
||||
pub username: SharedString,
|
||||
pub avatar: SharedString,
|
||||
pub is_contact: bool,
|
||||
}
|
||||
|
||||
impl PublicActor {
|
||||
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
username: username.into(),
|
||||
avatar: avatar.into(),
|
||||
is_contact: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
||||
pub enum FileSystemStatus {
|
||||
#[default]
|
||||
|
@ -1,17 +1,20 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::DateTime;
|
||||
use gpui2::{AppContext, ViewContext};
|
||||
use rand::Rng;
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
use crate::{
|
||||
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
|
||||
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
|
||||
Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus,
|
||||
PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
|
||||
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
|
||||
Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
|
||||
PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
|
||||
};
|
||||
use crate::{HighlightedText, ListDetailsEntry};
|
||||
use crate::{ListItem, NotificationAction};
|
||||
|
||||
pub fn static_tabs_example() -> Vec<Tab> {
|
||||
vec![
|
||||
@ -325,27 +328,227 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
|
||||
pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
|
||||
vec![
|
||||
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
|
||||
.meta("4 people in stream."),
|
||||
ListDetailsEntry::new("nathansobo accepted your contact request."),
|
||||
Notification::new_icon_message(
|
||||
"notif-1",
|
||||
"You were mentioned in a note.",
|
||||
DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
Icon::AtSign,
|
||||
Arc::new(|_, _| {}),
|
||||
),
|
||||
Notification::new_actor_with_actions(
|
||||
"notif-2",
|
||||
"as-cii sent you a contact request.",
|
||||
DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
|
||||
[
|
||||
NotificationAction::new(
|
||||
Button::new("Decline"),
|
||||
"Decline Request",
|
||||
(Some(Icon::XCircle), "Declined"),
|
||||
),
|
||||
NotificationAction::new(
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
"Accept Request",
|
||||
(Some(Icon::Check), "Accepted"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Notification::new_icon_message(
|
||||
"notif-3",
|
||||
"You were mentioned #design.",
|
||||
DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
Icon::MessageBubbles,
|
||||
Arc::new(|_, _| {}),
|
||||
),
|
||||
Notification::new_actor_with_actions(
|
||||
"notif-4",
|
||||
"as-cii sent you a contact request.",
|
||||
DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
|
||||
[
|
||||
NotificationAction::new(
|
||||
Button::new("Decline"),
|
||||
"Decline Request",
|
||||
(Some(Icon::XCircle), "Declined"),
|
||||
),
|
||||
NotificationAction::new(
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
"Accept Request",
|
||||
(Some(Icon::Check), "Accepted"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Notification::new_icon_message(
|
||||
"notif-5",
|
||||
"You were mentioned in a note.",
|
||||
DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
Icon::AtSign,
|
||||
Arc::new(|_, _| {}),
|
||||
),
|
||||
Notification::new_actor_with_actions(
|
||||
"notif-6",
|
||||
"as-cii sent you a contact request.",
|
||||
DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
|
||||
[
|
||||
NotificationAction::new(
|
||||
Button::new("Decline"),
|
||||
"Decline Request",
|
||||
(Some(Icon::XCircle), "Declined"),
|
||||
),
|
||||
NotificationAction::new(
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
"Accept Request",
|
||||
(Some(Icon::Check), "Accepted"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Notification::new_icon_message(
|
||||
"notif-7",
|
||||
"You were mentioned in a note.",
|
||||
DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
Icon::AtSign,
|
||||
Arc::new(|_, _| {}),
|
||||
),
|
||||
Notification::new_actor_with_actions(
|
||||
"notif-8",
|
||||
"as-cii sent you a contact request.",
|
||||
DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
|
||||
[
|
||||
NotificationAction::new(
|
||||
Button::new("Decline"),
|
||||
"Decline Request",
|
||||
(Some(Icon::XCircle), "Declined"),
|
||||
),
|
||||
NotificationAction::new(
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
"Accept Request",
|
||||
(Some(Icon::Check), "Accepted"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Notification::new_icon_message(
|
||||
"notif-9",
|
||||
"You were mentioned in a note.",
|
||||
DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
Icon::AtSign,
|
||||
Arc::new(|_, _| {}),
|
||||
),
|
||||
Notification::new_actor_with_actions(
|
||||
"notif-10",
|
||||
"as-cii sent you a contact request.",
|
||||
DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
|
||||
[
|
||||
NotificationAction::new(
|
||||
Button::new("Decline"),
|
||||
"Decline Request",
|
||||
(Some(Icon::XCircle), "Declined"),
|
||||
),
|
||||
NotificationAction::new(
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
"Accept Request",
|
||||
(Some(Icon::Check), "Accepted"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn static_read_notification_items<V: 'static>() -> Vec<ListItem<V>> {
|
||||
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
|
||||
vec![
|
||||
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
|
||||
Button::new("Decline"),
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
]),
|
||||
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
|
||||
.seen(true)
|
||||
.meta("This stream has ended."),
|
||||
ListDetailsEntry::new("as-cii accepted your contact request."),
|
||||
ListItem::Header(ListSubHeader::new("New")),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
|
||||
.meta("4 people in stream."),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"nathansobo accepted your contact request.",
|
||||
)),
|
||||
ListItem::Header(ListSubHeader::new("Earlier")),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
|
||||
Button::new("Decline"),
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
]),
|
||||
),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
|
||||
.seen(true)
|
||||
.meta("This stream has ended."),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"as-cii accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"osiewicz accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"ConradIrwin accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
|
||||
.seen(true)
|
||||
.meta("This stream has ended."),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"nathansobo accepted your contact request.",
|
||||
)),
|
||||
ListItem::Header(ListSubHeader::new("Earlier")),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
|
||||
Button::new("Decline"),
|
||||
Button::new("Accept").variant(crate::ButtonVariant::Filled),
|
||||
]),
|
||||
),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
|
||||
.seen(true)
|
||||
.meta("This stream has ended."),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"as-cii accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
|
||||
),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"osiewicz accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(ListDetailsEntry::new(
|
||||
"ConradIrwin accepted your contact request.",
|
||||
)),
|
||||
ListItem::Details(
|
||||
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
|
||||
.seen(true)
|
||||
.meta("This stream has ended."),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(From::from)
|
||||
|
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::*;
|
173
crates/ui2/src/utils/format_distance.rs
Normal file
173
crates/ui2/src/utils/format_distance.rs
Normal file
@ -0,0 +1,173 @@
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
|
||||
let duration = date.signed_duration_since(base_date);
|
||||
-duration.num_seconds()
|
||||
}
|
||||
|
||||
fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
|
||||
let suffix = if distance < 0 { " from now" } else { " ago" };
|
||||
|
||||
let d = distance.abs();
|
||||
|
||||
let minutes = d / 60;
|
||||
let hours = d / 3600;
|
||||
let days = d / 86400;
|
||||
let months = d / 2592000;
|
||||
let years = d / 31536000;
|
||||
|
||||
let string = if d < 5 && include_seconds {
|
||||
"less than 5 seconds".to_string()
|
||||
} else if d < 10 && include_seconds {
|
||||
"less than 10 seconds".to_string()
|
||||
} else if d < 20 && include_seconds {
|
||||
"less than 20 seconds".to_string()
|
||||
} else if d < 40 && include_seconds {
|
||||
"half a minute".to_string()
|
||||
} else if d < 60 && include_seconds {
|
||||
"less than a minute".to_string()
|
||||
} else if d < 90 && include_seconds {
|
||||
"1 minute".to_string()
|
||||
} else if d < 30 {
|
||||
"less than a minute".to_string()
|
||||
} else if d < 90 {
|
||||
"1 minute".to_string()
|
||||
} else if d < 2700 {
|
||||
format!("{} minutes", minutes)
|
||||
} else if d < 5400 {
|
||||
"about 1 hour".to_string()
|
||||
} else if d < 86400 {
|
||||
format!("about {} hours", hours)
|
||||
} else if d < 172800 {
|
||||
"1 day".to_string()
|
||||
} else if d < 2592000 {
|
||||
format!("{} days", days)
|
||||
} else if d < 5184000 {
|
||||
"about 1 month".to_string()
|
||||
} else if d < 7776000 {
|
||||
"about 2 months".to_string()
|
||||
} else if d < 31540000 {
|
||||
format!("{} months", months)
|
||||
} else if d < 39425000 {
|
||||
"about 1 year".to_string()
|
||||
} else if d < 55195000 {
|
||||
"over 1 year".to_string()
|
||||
} else if d < 63080000 {
|
||||
"almost 2 years".to_string()
|
||||
} else {
|
||||
let years = d / 31536000;
|
||||
let remaining_months = (d % 31536000) / 2592000;
|
||||
|
||||
if remaining_months < 3 {
|
||||
format!("about {} years", years)
|
||||
} else if remaining_months < 9 {
|
||||
format!("over {} years", years)
|
||||
} else {
|
||||
format!("almost {} years", years + 1)
|
||||
}
|
||||
};
|
||||
|
||||
if add_suffix {
|
||||
return format!("{}{}", string, suffix);
|
||||
} else {
|
||||
string
|
||||
}
|
||||
}
|
||||
|
||||
pub fn naive_format_distance(
|
||||
date: NaiveDateTime,
|
||||
base_date: NaiveDateTime,
|
||||
include_seconds: bool,
|
||||
add_suffix: bool,
|
||||
) -> String {
|
||||
let distance = distance_in_seconds(date, base_date);
|
||||
|
||||
distance_string(distance, include_seconds, add_suffix)
|
||||
}
|
||||
|
||||
pub fn naive_format_distance_from_now(
|
||||
datetime: NaiveDateTime,
|
||||
include_seconds: bool,
|
||||
add_suffix: bool,
|
||||
) -> String {
|
||||
let now = chrono::offset::Local::now().naive_local();
|
||||
|
||||
naive_format_distance(datetime, now, include_seconds, add_suffix)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
#[test]
|
||||
fn test_naive_format_distance() {
|
||||
let date =
|
||||
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
|
||||
let base_date =
|
||||
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
|
||||
|
||||
assert_eq!(
|
||||
"about 2 hours",
|
||||
naive_format_distance(date, base_date, false, false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_naive_format_distance_with_suffix() {
|
||||
let date =
|
||||
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
|
||||
let base_date =
|
||||
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
|
||||
|
||||
assert_eq!(
|
||||
"about 2 hours from now",
|
||||
naive_format_distance(date, base_date, false, true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_naive_format_distance_from_now() {
|
||||
let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
|
||||
.expect("Invalid NaiveDateTime for date");
|
||||
|
||||
assert_eq!(
|
||||
"over 54 years ago",
|
||||
naive_format_distance_from_now(date, false, true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_naive_format_distance_string() {
|
||||
assert_eq!(distance_string(3, false, false), "less than a minute");
|
||||
assert_eq!(distance_string(7, false, false), "less than a minute");
|
||||
assert_eq!(distance_string(13, false, false), "less than a minute");
|
||||
assert_eq!(distance_string(21, false, false), "less than a minute");
|
||||
assert_eq!(distance_string(45, false, false), "1 minute");
|
||||
assert_eq!(distance_string(61, false, false), "1 minute");
|
||||
assert_eq!(distance_string(1920, false, false), "32 minutes");
|
||||
assert_eq!(distance_string(3902, false, false), "about 1 hour");
|
||||
assert_eq!(distance_string(18002, false, false), "about 5 hours");
|
||||
assert_eq!(distance_string(86470, false, false), "1 day");
|
||||
assert_eq!(distance_string(345880, false, false), "4 days");
|
||||
assert_eq!(distance_string(2764800, false, false), "about 1 month");
|
||||
assert_eq!(distance_string(5184000, false, false), "about 2 months");
|
||||
assert_eq!(distance_string(10368000, false, false), "4 months");
|
||||
assert_eq!(distance_string(34694000, false, false), "about 1 year");
|
||||
assert_eq!(distance_string(47310000, false, false), "over 1 year");
|
||||
assert_eq!(distance_string(61503000, false, false), "almost 2 years");
|
||||
assert_eq!(distance_string(160854000, false, false), "about 5 years");
|
||||
assert_eq!(distance_string(236550000, false, false), "over 7 years");
|
||||
assert_eq!(distance_string(249166000, false, false), "almost 8 years");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_naive_format_distance_string_include_seconds() {
|
||||
assert_eq!(distance_string(3, true, false), "less than 5 seconds");
|
||||
assert_eq!(distance_string(7, true, false), "less than 10 seconds");
|
||||
assert_eq!(distance_string(13, true, false), "less than 20 seconds");
|
||||
assert_eq!(distance_string(21, true, false), "half a minute");
|
||||
assert_eq!(distance_string(45, true, false), "less than a minute");
|
||||
assert_eq!(distance_string(61, true, false), "1 minute");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user