diff --git a/Cargo.lock b/Cargo.lock index 7e0a686..beb9ee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,7 +410,7 @@ dependencies = [ [[package]] name = "async-wsocket" version = "0.11.0" -source = "git+https://github.com/yukibtc/async-wsocket?rev=27f606af6b2028634022a97b5e56c332dfe3f611#27f606af6b2028634022a97b5e56c332dfe3f611" +source = "git+https://github.com/yukibtc/async-wsocket?rev=259c0bc372e7d60d94827b484178bf995afdcbe6#259c0bc372e7d60d94827b484178bf995afdcbe6" dependencies = [ "async-utility", "futures", @@ -548,18 +548,18 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bit_field" @@ -661,7 +661,7 @@ checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "blade-graphics" version = "0.5.0" -source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969" +source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80" dependencies = [ "ash", "ash-window", @@ -691,7 +691,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969" +source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80" dependencies = [ "proc-macro2", "quote", @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.1.0" -source = "git+https://github.com/kvark/blade?rev=e142a3a5e678eb6a13e642ad8401b1f3aa38e969#e142a3a5e678eb6a13e642ad8401b1f3aa38e969" +source = "git+https://github.com/kvark/blade?rev=099555282605c7c4cca9e66a8f40148298347f80#099555282605c7c4cca9e66a8f40148298347f80" dependencies = [ "blade-graphics", "bytemuck", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", "libc", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "rustc-hash 1.1.0", ] @@ -1379,7 +1379,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "proc-macro2", "quote", @@ -2104,7 +2104,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2189,7 +2189,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "proc-macro2", "quote", @@ -2400,7 +2400,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "bytes", @@ -2421,9 +2421,9 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -2441,9 +2441,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "f6884a48c6826ec44f524c7456b163cebe9e55a18d7b5e307cb4f100371cc767" dependencies = [ "futures-util", "http", @@ -2822,7 +2822,7 @@ dependencies = [ "dbus-secret-service", "log", "security-framework 2.11.1", - "security-framework 3.0.1", + "security-framework 3.1.0", "windows-sys 0.59.0", ] @@ -2878,9 +2878,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libdbus-sys" @@ -3065,7 +3065,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "bindgen", @@ -3102,9 +3102,8 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +version = "0.30.0" +source = "git+https://github.com/gfx-rs/metal-rs?rev=ef768ff9d742ae6a0f4e83ddc8031264e7d460c4#ef768ff9d742ae6a0f4e83ddc8031264e7d460c4" dependencies = [ "bitflags 2.6.0", "block", @@ -3129,9 +3128,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "simd-adler32", @@ -3156,9 +3155,8 @@ dependencies = [ [[package]] name = "naga" -version = "22.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +version = "23.0.0" +source = "git+https://github.com/gfx-rs/wgpu?rev=1a643291c2e8854ba7e4f5445a4388202731bfa1#1a643291c2e8854ba7e4f5445a4388202731bfa1" dependencies = [ "arrayvec", "bit-set", @@ -3234,7 +3232,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "aes", "async-trait", @@ -3265,7 +3263,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "async-trait", "flatbuffers", @@ -3276,7 +3274,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "async-utility", "heed", @@ -3288,7 +3286,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "async-utility", "async-wsocket", @@ -3304,7 +3302,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "async-utility", "lnurl-pay", @@ -3313,7 +3311,6 @@ dependencies = [ "nostr-lmdb", "nostr-relay-pool", "nostr-zapper", - "thiserror 1.0.69", "tokio", "tracing", ] @@ -3321,7 +3318,7 @@ dependencies = [ [[package]] name = "nostr-zapper" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" +source = "git+https://github.com/rust-nostr/nostr#2efdc69ad385b51b1c5f93706bfeeff5b6dd4d41" dependencies = [ "async-trait", "nostr", @@ -3916,7 +3913,7 @@ dependencies = [ "rustc-hash 2.1.0", "rustls", "socket2 0.5.8", - "thiserror 2.0.7", + "thiserror 2.0.8", "tokio", "tracing", ] @@ -3935,7 +3932,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.7", + "thiserror 2.0.8", "tinyvec", "tracing", "web-time", @@ -3943,9 +3940,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -4121,7 +4118,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "derive_refineable", ] @@ -4248,7 +4245,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "bytes", @@ -4417,7 +4414,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.0.1", + "security-framework 3.1.0", ] [[package]] @@ -4632,9 +4629,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" dependencies = [ "bitflags 2.6.0", "core-foundation 0.10.0", @@ -4645,9 +4642,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -4662,7 +4659,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "serde", @@ -4988,7 +4985,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "arrayvec", "log", @@ -5263,11 +5260,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.8", ] [[package]] @@ -5283,9 +5280,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" dependencies = [ "proc-macro2", "quote", @@ -5622,9 +5619,9 @@ checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -5783,7 +5780,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" +source = "git+https://github.com/zed-industries/zed#3632b36fde4c4e73eadc7e231d7b040a3b7fb55b" dependencies = [ "anyhow", "async-fs 1.6.0", diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 7a9fae2..6adef3e 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -102,37 +102,39 @@ async fn main() { } = message { if event.kind == Kind::GiftWrap { - if let Ok(UnwrappedGift { rumor, .. }) = - client.unwrap_gift_wrap(&event).await - { - let mut rumor_clone = rumor.clone(); + match client.unwrap_gift_wrap(&event).await { + Ok(UnwrappedGift { rumor, .. }) => { + let mut rumor_clone = rumor.clone(); - // Compute event id if not exist - rumor_clone.ensure_id(); + // Compute event id if not exist + rumor_clone.ensure_id(); - if let Some(id) = rumor_clone.id { - let ev = Event::new( - id, - rumor_clone.pubkey, - rumor_clone.created_at, - rumor_clone.kind, - rumor_clone.tags, - rumor_clone.content, - sig, - ); + if let Some(id) = rumor_clone.id { + let ev = Event::new( + id, + rumor_clone.pubkey, + rumor_clone.created_at, + rumor_clone.kind, + rumor_clone.tags, + rumor_clone.content, + sig, + ); - // Save rumor to database to further query - if let Err(e) = client.database().save_event(&ev).await { - println!("Save error: {}", e); - } + // Save rumor to database to further query + if let Err(e) = client.database().save_event(&ev).await { + println!("Save error: {}", e); + } - // Send event back to channel - if subscription_id == new_message { - if let Err(e) = signal_tx.send(Signal::RecvEvent(ev)).await { - println!("Error: {}", e) + // Send event back to channel + if subscription_id == new_message { + if let Err(e) = signal_tx.send(Signal::RecvEvent(ev)).await + { + println!("Error: {}", e) + } } } } + Err(e) => println!("Error: {}", e), } } else if event.kind == Kind::Metadata { if let Err(e) = signal_tx.send(Signal::RecvMetadata(event.pubkey)).await { diff --git a/crates/app/src/views/dock/chat/form.rs b/crates/app/src/views/dock/chat/form.rs index f0cc5cc..5b403bc 100644 --- a/crates/app/src/views/dock/chat/form.rs +++ b/crates/app/src/views/dock/chat/form.rs @@ -24,12 +24,9 @@ impl Form { .cleanable() }); - cx.subscribe(&input, move |form, text_input, input_event, cx| { + cx.subscribe(&input, move |form, _, input_event, cx| { if let InputEvent::PressEnter = input_event { - let content = text_input.read(cx).text().to_string(); - // TODO: clean up content - - form.send_message(content, cx); + form.send_message(cx); } }) .detach(); @@ -37,28 +34,45 @@ impl Form { Self { to, input } } - fn send_message(&mut self, content: String, cx: &mut ViewContext) { + fn send_message(&mut self, cx: &mut ViewContext) { let send_to = self.to; + let content = self.input.read(cx).text().to_string(); let content_clone = content.clone(); + let async_input = self.input.clone(); + let mut async_cx = cx.to_async(); + cx.foreground_executor() .spawn(async move { let client = get_client(); - let signer = client.signer().await.unwrap(); - let public_key = signer.get_public_key().await.unwrap(); - match client.send_private_msg(send_to, content, vec![]).await { - Ok(_) => { - // Send a copy to yourself - if let Err(_e) = client - .send_private_msg(public_key, content_clone, vec![]) + async_cx + .background_executor() + .spawn(async move { + let signer = client.signer().await.unwrap(); + let public_key = signer.get_public_key().await.unwrap(); + + // Send message to all members + if client + .send_private_msg(send_to, content, vec![]) .await + .is_ok() { - todo!() + // Send a copy to yourself + _ = client + .send_private_msg( + public_key, + content_clone, + vec![Tag::public_key(send_to)], + ) + .await; } - } - Err(_) => todo!(), - } + }) + .await; + + _ = async_cx.update_view(&async_input, |input, cx| { + input.set_text("", cx); + }); }) .detach(); } @@ -67,8 +81,11 @@ impl Form { impl Render for Form { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() - .h_12() .flex_shrink_0() + .w_full() + .h_12() + .border_t_1() + .border_color(cx.theme().border.opacity(0.7)) .flex() .items_center() .gap_2() diff --git a/crates/app/src/views/dock/chat/list.rs b/crates/app/src/views/dock/chat/list.rs index 21571dd..49e2efd 100644 --- a/crates/app/src/views/dock/chat/list.rs +++ b/crates/app/src/views/dock/chat/list.rs @@ -1,7 +1,8 @@ use gpui::*; use nostr_sdk::prelude::*; +use prelude::FluentBuilder; -use crate::get_client; +use crate::{get_client, states::chat::ChatRegistry}; pub struct MessageList { member: PublicKey, @@ -56,28 +57,38 @@ impl MessageList { } pub fn subscribe(&self, cx: &mut ViewContext) { - /* - let receiver = cx.global::().receiver.clone(); + let messages = self.messages.clone(); - cx.foreground_executor() - .spawn(async move { - while let Ok(event) = receiver.recv_async().await { - println!("New message: {}", event.as_json()) + cx.observe_global::(move |_, cx| { + let state = cx.global::(); + let events = state.new_messages.clone(); + + cx.update_model(&messages, |a, b| { + if let Some(m) = a { + m.extend(events); + b.notify(); } - }) - .detach(); - */ + }); + }) + .detach(); } } impl Render for MessageList { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut content = div().size_full().flex().flex_col().justify_end(); - - if let Some(messages) = self.messages.read(cx).as_ref() { - content = content.children(messages.clone().into_iter().map(|m| div().child(m.content))) - } - - div().flex_1().child(content) + div() + .h_full() + .flex() + .flex_col_reverse() + .justify_end() + .when_some(self.messages.read(cx).as_ref(), |this, messages| { + this.children(messages.clone().into_iter().map(|m| { + div() + .flex() + .flex_col() + .child(m.pubkey.to_hex()) + .child(m.content) + })) + }) } } diff --git a/crates/app/src/views/dock/chat/mod.rs b/crates/app/src/views/dock/chat/mod.rs index e336c75..e4befa2 100644 --- a/crates/app/src/views/dock/chat/mod.rs +++ b/crates/app/src/views/dock/chat/mod.rs @@ -2,6 +2,7 @@ use coop_ui::{ button::Button, dock::{Panel, PanelEvent, PanelState, TitleStyle}, popup_menu::PopupMenu, + v_flex, }; use form::Form; use gpui::*; @@ -90,11 +91,9 @@ impl FocusableView for ChatPanel { impl Render for ChatPanel { fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl IntoElement { - div() + v_flex() .size_full() - .flex() - .flex_col() - .child(self.list.clone()) + .child(div().flex_1().min_h_0().child(self.list.clone())) .child(self.form.clone()) } } diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 5311dce..d6e5bd1 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -1,3 +1,9 @@ +use gpui::{ + div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges, + ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, + RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, +}; + use crate::{ h_flex, indicator::Indicator, @@ -5,11 +11,6 @@ use crate::{ tooltip::Tooltip, Disableable, Icon, Selectable, Sizable, Size, }; -use gpui::{ - div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Corners, Div, Edges, - ElementId, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, - RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, WindowContext, -}; pub enum ButtonRounded { None, @@ -574,7 +575,8 @@ impl ButtonVariant { fn active(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { ButtonVariant::Primary => cx.theme().primary_active, - ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => { + ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_active, + ButtonVariant::Ghost => { if cx.theme().mode.is_dark() { cx.theme().secondary.lighten(0.2).opacity(0.8) } else { @@ -607,7 +609,8 @@ impl ButtonVariant { fn selected(&self, cx: &WindowContext) -> ButtonVariantStyle { let bg = match self { ButtonVariant::Primary => cx.theme().primary_active, - ButtonVariant::Secondary | ButtonVariant::Outline | ButtonVariant::Ghost => { + ButtonVariant::Secondary | ButtonVariant::Outline => cx.theme().secondary_active, + ButtonVariant::Ghost => { if cx.theme().mode.is_dark() { cx.theme().secondary.lighten(0.2).opacity(0.8) } else { diff --git a/crates/ui/src/color_picker.rs b/crates/ui/src/color_picker.rs index a128aa8..1a77603 100644 --- a/crates/ui/src/color_picker.rs +++ b/crates/ui/src/color_picker.rs @@ -1,9 +1,8 @@ use gpui::{ - anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AnchorCorner, - AppContext, Bounds, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla, - InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, - Render, SharedString, StatefulInteractiveElement as _, Styled, View, ViewContext, - VisualContext, + anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, relative, AppContext, Bounds, + Corner, ElementId, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement as _, + IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render, SharedString, + StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, }; use crate::{ @@ -63,7 +62,7 @@ pub struct ColorPicker { hovered_color: Option, label: Option, size: Size, - anchor: AnchorCorner, + anchor: Corner, color_input: View, open: bool, @@ -112,7 +111,7 @@ impl ColorPicker { hovered_color: None, size: Size::Medium, label: None, - anchor: AnchorCorner::TopLeft, + anchor: Corner::TopLeft, color_input, open: false, bounds: Bounds::default(), @@ -149,8 +148,8 @@ impl ColorPicker { /// Set the anchor corner of the color picker. /// - /// Default is `AnchorCorner::TopLeft`. - pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + /// Default is `Corner::TopLeft`. + pub fn anchor(mut self, anchor: Corner) -> Self { self.anchor = anchor; self } @@ -262,13 +261,12 @@ impl ColorPicker { } fn resolved_corner(&self, bounds: Bounds) -> Point { - match self.anchor { - AnchorCorner::TopLeft => AnchorCorner::BottomLeft, - AnchorCorner::TopRight => AnchorCorner::BottomRight, - AnchorCorner::BottomLeft => AnchorCorner::TopLeft, - AnchorCorner::BottomRight => AnchorCorner::TopRight, - } - .corner(bounds) + bounds.corner(match self.anchor { + Corner::TopLeft => Corner::BottomLeft, + Corner::TopRight => Corner::BottomRight, + Corner::BottomLeft => Corner::TopLeft, + Corner::BottomRight => Corner::TopRight, + }) } } @@ -347,12 +345,8 @@ impl Render for ColorPicker { div() .occlude() .map(|this| match self.anchor { - AnchorCorner::TopLeft | AnchorCorner::TopRight => { - this.mt_1p5() - } - AnchorCorner::BottomLeft | AnchorCorner::BottomRight => { - this.mb_1p5() - } + Corner::TopLeft | Corner::TopRight => this.mt_1p5(), + Corner::BottomLeft | Corner::BottomRight => this.mb_1p5(), }) .w_72() .overflow_hidden() diff --git a/crates/ui/src/context_menu.rs b/crates/ui/src/context_menu.rs index 0337dee..68fa435 100644 --- a/crates/ui/src/context_menu.rs +++ b/crates/ui/src/context_menu.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - anchored, deferred, div, prelude::FluentBuilder, px, relative, AnchorCorner, AnyElement, + anchored, deferred, div, prelude::FluentBuilder, px, relative, AnyElement, Corner, DismissEvent, DispatchPhase, Element, ElementId, Focusable, GlobalElementId, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Position, Stateful, Style, View, ViewContext, WindowContext, @@ -27,7 +27,7 @@ type Menu = Option) -> PopupMenu + pub struct ContextMenu { id: ElementId, menu: Menu, - anchor: AnchorCorner, + anchor: Corner, } impl ContextMenu { @@ -35,7 +35,7 @@ impl ContextMenu { Self { id: id.into(), menu: None, - anchor: AnchorCorner::TopLeft, + anchor: Corner::TopLeft, } } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 32dcca3..55a380a 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -1,5 +1,5 @@ use gpui::{ - div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, + div, prelude::FluentBuilder, px, rems, AppContext, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, IntoElement, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, @@ -367,7 +367,7 @@ impl TabPanel { this.separator().menu("Close", Box::new(ClosePanel)) }) }) - .anchor(AnchorCorner::TopRight), + .anchor(Corner::TopRight), ) } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index b277573..02ab26d 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -44,6 +44,7 @@ pub mod switch; pub mod tab; pub mod theme; pub mod tooltip; +pub mod virtual_list; pub use crate::Disableable; pub use event::InteractiveElementExt; @@ -51,6 +52,7 @@ pub use focusable::FocusableCycle; pub use root::{ContextModal, Root}; pub use styled::*; pub use title_bar::*; +pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList}; pub use colors::*; pub use icon::*; diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 3667f31..df4e708 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -1,6 +1,6 @@ use gpui::{ - actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnchorCorner, AnyElement, - AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle, + actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, AppContext, + Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle, FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style, StyleRefinement, Styled, View, ViewContext, VisualContext, WindowContext, @@ -69,7 +69,7 @@ type ViewContent = Option View + 'static> pub struct Popover { id: ElementId, - anchor: AnchorCorner, + anchor: Corner, trigger: Trigger, content: ViewContent, /// Style for trigger element. @@ -87,7 +87,7 @@ where pub fn new(id: impl Into) -> Self { Self { id: id.into(), - anchor: AnchorCorner::TopLeft, + anchor: Corner::TopLeft, trigger: None, trigger_style: None, content: None, @@ -96,7 +96,7 @@ where } } - pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + pub fn anchor(mut self, anchor: Corner) -> Self { self.anchor = anchor; self } @@ -153,13 +153,12 @@ where } fn resolved_corner(&self, bounds: Bounds) -> Point { - match self.anchor { - AnchorCorner::TopLeft => AnchorCorner::BottomLeft, - AnchorCorner::TopRight => AnchorCorner::BottomRight, - AnchorCorner::BottomLeft => AnchorCorner::TopLeft, - AnchorCorner::BottomRight => AnchorCorner::TopRight, - } - .corner(bounds) + bounds.corner(match self.anchor { + Corner::TopLeft => Corner::BottomLeft, + Corner::TopRight => Corner::BottomRight, + Corner::BottomLeft => Corner::TopLeft, + Corner::BottomRight => Corner::TopRight, + }) } fn with_element_state( @@ -273,12 +272,8 @@ impl Element for Popover { .occlude() .when(!no_style, |this| this.popover_style(cx)) .map(|this| match anchor { - AnchorCorner::TopLeft | AnchorCorner::TopRight => { - this.top_1p5() - } - AnchorCorner::BottomLeft | AnchorCorner::BottomRight => { - this.bottom_1p5() - } + Corner::TopLeft | Corner::TopRight => this.top_1p5(), + Corner::BottomLeft | Corner::BottomRight => this.bottom_1p5(), }) .child(content_view.clone()) .when(!no_style, |this| { diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 9f2a080..c61427d 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -8,7 +8,7 @@ use gpui::{ SharedString, View, ViewContext, VisualContext as _, WindowContext, }; use gpui::{ - anchored, canvas, rems, AnchorCorner, AnyElement, Bounds, Edges, FocusableView, Keystroke, + anchored, canvas, rems, AnyElement, Bounds, Corner, Edges, FocusableView, Keystroke, ScrollHandle, StatefulInteractiveElement, Styled, WeakView, }; @@ -37,13 +37,13 @@ pub trait PopupMenuExt: Styled + Selectable + IntoElement + 'static { self, f: impl Fn(PopupMenu, &mut ViewContext) -> PopupMenu + 'static, ) -> Popover { - self.popup_menu_with_anchor(AnchorCorner::TopLeft, f) + self.popup_menu_with_anchor(Corner::TopLeft, f) } /// Create a popup menu with the given items, anchored to the given corner fn popup_menu_with_anchor( mut self, - anchor: impl Into, + anchor: impl Into, f: impl Fn(PopupMenu, &mut ViewContext) -> PopupMenu + 'static, ) -> Popover { let style = self.style().clone(); @@ -646,13 +646,10 @@ impl Render for PopupMenu { - bounds.origin.x < max_width { - ( - AnchorCorner::TopRight, - -px(15.), - ) + (Corner::TopRight, -px(15.)) } else { ( - AnchorCorner::TopLeft, + Corner::TopLeft, bounds.size.width - px(10.), ) diff --git a/crates/ui/src/scroll/scrollable.rs b/crates/ui/src/scroll/scrollable.rs index bfa47f8..dbd2a65 100644 --- a/crates/ui/src/scroll/scrollable.rs +++ b/crates/ui/src/scroll/scrollable.rs @@ -1,11 +1,11 @@ -use std::{cell::Cell, rc::Rc}; - -use super::{Scrollbar, ScrollbarAxis, ScrollbarState}; use gpui::{ canvas, div, relative, AnyElement, Div, Element, ElementId, EntityId, GlobalElementId, InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, WindowContext, }; +use std::{cell::Cell, rc::Rc}; + +use super::{Scrollbar, ScrollbarAxis, ScrollbarState}; /// A scroll view is a container that allows the user to scroll through a large amount of content. pub struct Scrollable { @@ -121,6 +121,7 @@ where } } } + impl StatefulInteractiveElement for Scrollable where E: Element + StatefulInteractiveElement {} impl IntoElement for Scrollable @@ -202,8 +203,8 @@ where ), ) .into_any_element(); - let element_id = element.request_layout(cx); + let element_id = element.request_layout(cx); let layout_id = cx.request_layout(style, vec![element_id]); (layout_id, element) diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 45cec31..b783baf 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -1,12 +1,8 @@ +use gpui::*; +use serde::{Deserialize, Serialize}; use std::{cell::Cell, rc::Rc, time::Instant}; use crate::theme::ActiveTheme; -use gpui::{ - fill, point, px, relative, AppContext, Bounds, ContentMask, CursorStyle, Edges, Element, - EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Style, UniformListScrollHandle, -}; -use serde::{Deserialize, Serialize}; /// Scrollbar show mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)] @@ -22,6 +18,7 @@ impl ScrollbarShow { } } +const BORDER_WIDTH: Pixels = px(0.); const MIN_THUMB_SIZE: f32 = 80.; const THUMB_RADIUS: Pixels = Pixels(3.0); const THUMB_INSET: Pixels = Pixels(4.); @@ -357,11 +354,12 @@ pub struct AxisPrepaintState { axis: ScrollbarAxis, bar_hitbox: Hitbox, bounds: Bounds, - border_width: Pixels, radius: Pixels, bg: Hsla, border: Hsla, thumb_bounds: Bounds, + // Bounds of thumb to be rendered. + thumb_fill_bounds: Bounds, thumb_bg: Hsla, scroll_size: Pixels, container_size: Pixels, @@ -387,7 +385,7 @@ impl Element for Scrollbar { position: Position::Absolute, flex_grow: 1.0, flex_shrink: 1.0, - size: gpui::Size { + size: Size { width: relative(1.).into(), height: relative(1.).into(), }, @@ -517,11 +515,21 @@ impl Element for Scrollbar { idle_state }; - let border_width = px(0.); let thumb_bounds = if is_vertical { + Bounds::from_corners( + point(bounds.origin.x, bounds.origin.y + thumb_start), + point(bounds.origin.x + self.width, bounds.origin.y + thumb_end), + ) + } else { + Bounds::from_corners( + point(bounds.origin.x + thumb_start, bounds.origin.y), + point(bounds.origin.x + thumb_end, bounds.origin.y + self.width), + ) + }; + let thumb_fill_bounds = if is_vertical { Bounds::from_corners( point( - bounds.origin.x + inset + border_width, + bounds.origin.x + inset + BORDER_WIDTH, bounds.origin.y + thumb_start + inset, ), point( @@ -533,7 +541,7 @@ impl Element for Scrollbar { Bounds::from_corners( point( bounds.origin.x + thumb_start + inset, - bounds.origin.y + inset + border_width, + bounds.origin.y + inset + BORDER_WIDTH, ), point( bounds.origin.x + thumb_end - inset, @@ -550,11 +558,11 @@ impl Element for Scrollbar { axis, bar_hitbox, bounds, - border_width, radius, bg: bar_bg, border: bar_border, thumb_bounds, + thumb_fill_bounds, thumb_bg, scroll_size: scroll_area_size, container_size, @@ -603,11 +611,11 @@ impl Element for Scrollbar { top: px(0.), right: px(0.), bottom: px(0.), - left: state.border_width, + left: BORDER_WIDTH, } } else { Edges { - top: state.border_width, + top: BORDER_WIDTH, right: px(0.), bottom: px(0.), left: px(0.), @@ -616,7 +624,7 @@ impl Element for Scrollbar { border_color: state.border, }); - cx.paint_quad(fill(thumb_bounds, state.thumb_bg).corner_radii(radius)); + cx.paint_quad(fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius)); }); cx.on_mouse_event({ diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 679e9fe..f51fce4 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -263,7 +263,7 @@ impl ThemeColor { scrollbar_thumb: hsl(0., 0., 69.).opacity(0.9), scrollbar_thumb_hover: hsl(0., 0., 59.), secondary: hsl(240.0, 5.9, 96.9), - secondary_active: hsl(240.0, 5.9, 93.), + secondary_active: hsl(240.0, 5.9, 90.), secondary_foreground: hsl(240.0, 59.0, 10.), secondary_hover: hsl(240.0, 5.9, 98.), selection: hsl(211.0, 97.0, 85.0), diff --git a/crates/ui/src/virtual_list.rs b/crates/ui/src/virtual_list.rs new file mode 100644 index 0000000..ba5be48 --- /dev/null +++ b/crates/ui/src/virtual_list.rs @@ -0,0 +1,447 @@ +//! Vistual List for render a large number of differently sized rows/columns. +//! +//! > NOTE: This must ensure each column width or row height. +//! +//! Only visible range are rendered for performance reasons. +//! +//! Inspired by `gpui::uniform_list`. +//! https://github.com/zed-industries/zed/blob/0ae1603610ab6b265bdfbee7b8dbc23c5ab06edc/crates/gpui/src/elements/uniform_list.rs +//! +//! Unlike the `uniform_list`, the each item can have different size. +//! +//! This is useful for more complex layout, for example, a table with different row height. +use std::{cmp, ops::Range, rc::Rc}; + +use gpui::{ + div, point, px, size, AnyElement, AvailableSpace, Axis, Bounds, ContentMask, Div, Element, + ElementId, GlobalElementId, Hitbox, InteractiveElement, IntoElement, IsZero as _, Pixels, + Render, ScrollHandle, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, + View, ViewContext, WindowContext, +}; +use smallvec::SmallVec; + +/// Create a virtual list in Vertical direction. +/// +/// This is like `uniform_list` in GPUI, but support two axis. +/// +/// The `item_sizes` is the size of each column. +pub fn v_virtual_list( + view: View, + id: impl Into, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + virtual_list(view, id, Axis::Vertical, item_sizes, f) +} + +/// Create a virtual list in Horizontal direction. +pub fn h_virtual_list( + view: View, + id: impl Into, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + virtual_list(view, id, Axis::Horizontal, item_sizes, f) +} + +pub(crate) fn virtual_list( + view: View, + id: impl Into, + axis: Axis, + item_sizes: Rc>>, + f: impl 'static + Fn(&mut V, Range, Size, &mut ViewContext) -> Vec, +) -> VirtualList +where + R: IntoElement, + V: Render, +{ + let id: ElementId = id.into(); + let scroll_handle = ScrollHandle::default(); + let render_range = move |visible_range, content_size, cx: &mut WindowContext| { + view.update(cx, |this, cx| { + f(this, visible_range, content_size, cx) + .into_iter() + .map(|component| component.into_any_element()) + .collect() + }) + }; + + VirtualList { + id: id.clone(), + axis, + base: div() + .id(id) + .size_full() + .overflow_scroll() + .track_scroll(&scroll_handle), + scroll_handle, + items_count: item_sizes.len(), + item_sizes, + render_items: Box::new(render_range), + } +} + +type RenderItems = Box< + dyn for<'a> Fn(Range, Size, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>, +>; + +/// VirtualItem component for rendering a large number of differently sized columns. +pub struct VirtualList { + id: ElementId, + axis: Axis, + base: Stateful
, + scroll_handle: ScrollHandle, + // scroll_handle: ScrollHandle, + items_count: usize, + item_sizes: Rc>>, + render_items: RenderItems, +} + +impl Styled for VirtualList { + fn style(&mut self) -> &mut StyleRefinement { + self.base.style() + } +} + +impl VirtualList { + pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.base = self.base.track_scroll(scroll_handle); + self.scroll_handle = scroll_handle.clone(); + self + } + + /// Specify for table + #[allow(dead_code)] + pub(crate) fn with_scroll_handle(mut self, scroll_handle: &ScrollHandle) -> Self { + self.base = div().id(self.id.clone()).size_full(); + self.scroll_handle = scroll_handle.clone(); + self + } + + /// Measure first item to get the size. + fn measure_item(&self, cx: &mut WindowContext) -> Size { + if self.items_count == 0 { + return Size::default(); + } + + let item_ix = 0; + let mut items = (self.render_items)(item_ix..item_ix + 1, Size::default(), cx); + let Some(mut item_to_measure) = items.pop() else { + return Size::default(); + }; + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + item_to_measure.layout_as_root(available_space, cx) + } +} + +/// Frame state used by the [VirtualItem]. +pub struct VirtualListFrameState { + /// Visible items to be painted. + items: SmallVec<[AnyElement; 32]>, + item_sizes: Vec, + item_origins: Vec, +} + +impl IntoElement for VirtualList { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for VirtualList { + type RequestLayoutState = VirtualListFrameState; + type PrepaintState = Option; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + cx: &mut WindowContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let style = self.base.interactivity().compute_style(global_id, None, cx); + let font_size = cx.text_style().font_size.to_pixels(cx.rem_size()); + + // Including the gap between items for calculate the item size + let gap = match self.axis { + Axis::Horizontal => style.gap.width, + Axis::Vertical => style.gap.height, + } + .to_pixels(font_size.into(), cx.rem_size()); + + // TODO: To cache the item_sizes, item_origins + // If there have 500,000 items, this method will speed about 500~600µs + // let start = std::time::Instant::now(); + // Prepare each item's size by axis + let item_sizes = match self.axis { + Axis::Horizontal => self + .item_sizes + .iter() + .enumerate() + .map(|(i, size)| { + if i == self.items_count - 1 { + size.width + } else { + size.width + gap + } + }) + .collect::>(), + Axis::Vertical => self + .item_sizes + .iter() + .enumerate() + .map(|(i, size)| { + if i == self.items_count - 1 { + size.height + } else { + size.height + gap + } + }) + .collect::>(), + }; + + // Prepare each item's origin by axis + let item_origins = match self.axis { + Axis::Horizontal => item_sizes + .iter() + .scan(px(0.), |cumulative_x, size| { + let x = *cumulative_x; + *cumulative_x += *size; + Some(x) + }) + .collect::>(), + Axis::Vertical => item_sizes + .iter() + .scan(px(0.), |cumulative_y, size| { + let y = *cumulative_y; + *cumulative_y += *size; + Some(y) + }) + .collect::>(), + }; + // println!("layout: {} {:?}", item_sizes.len(), start.elapsed()); + + let (layout_id, _) = self.base.request_layout(global_id, cx); + + ( + layout_id, + VirtualListFrameState { + items: SmallVec::new(), + item_sizes, + item_origins, + }, + ) + } + + fn prepaint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + layout: &mut Self::RequestLayoutState, + cx: &mut WindowContext, + ) -> Self::PrepaintState { + let style = self.base.interactivity().compute_style(global_id, None, cx); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let first_item_size = self.measure_item(cx); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.bottom_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); + + // Get border + padding pixel size + let padding_size = match self.axis { + Axis::Horizontal => border.left + padding.left + border.right + padding.right, + Axis::Vertical => border.top + padding.top + border.bottom + padding.bottom, + }; + + let item_sizes = &layout.item_sizes; + let item_origins = &layout.item_origins; + + let content_size = match self.axis { + Axis::Horizontal => Size { + width: px(item_sizes.iter().map(|size| size.0).sum::()) + padding_size, + height: (first_item_size.height + padding_size).max(padded_bounds.size.height), + }, + Axis::Vertical => Size { + width: (first_item_size.width + padding_size).max(padded_bounds.size.width), + height: px(item_sizes.iter().map(|size| size.0).sum::()) + padding_size, + }, + }; + + self.base.interactivity().prepaint( + global_id, + bounds, + content_size, + cx, + |style, _, hitbox, cx| { + let mut scroll_offset = self.scroll_handle.offset(); + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top), + bounds.bottom_right() - point(border.right + padding.right, border.bottom), + ); + + if self.items_count > 0 { + let is_scrolled = match self.axis { + Axis::Horizontal => !scroll_offset.x.is_zero(), + Axis::Vertical => !scroll_offset.y.is_zero(), + }; + + let min_scroll_offset = match self.axis { + Axis::Horizontal => padded_bounds.size.width - content_size.width, + Axis::Vertical => padded_bounds.size.height - content_size.height, + }; + + if is_scrolled { + match self.axis { + Axis::Horizontal if scroll_offset.x < min_scroll_offset => { + scroll_offset.x = min_scroll_offset; + } + Axis::Vertical if scroll_offset.y < min_scroll_offset => { + scroll_offset.y = min_scroll_offset; + } + _ => {} + } + } + + let (first_visible_element_ix, last_visible_element_ix) = match self.axis { + Axis::Horizontal => { + let mut cumulative_size = px(0.); + let mut first_visible_element_ix = 0; + for (i, &size) in item_sizes.iter().enumerate() { + cumulative_size += size; + if cumulative_size > -(scroll_offset.x + padding.left) { + first_visible_element_ix = i; + break; + } + } + + cumulative_size = px(0.); + let mut last_visible_element_ix = 0; + for (i, &size) in item_sizes.iter().enumerate() { + cumulative_size += size; + if cumulative_size > (-scroll_offset.x + padded_bounds.size.width) { + last_visible_element_ix = i + 1; + break; + } + } + if last_visible_element_ix == 0 { + last_visible_element_ix = self.items_count; + } else { + last_visible_element_ix += 1; + } + (first_visible_element_ix, last_visible_element_ix) + } + Axis::Vertical => { + let mut cumulative_size = px(0.); + let mut first_visible_element_ix = 0; + for (i, &size) in item_sizes.iter().enumerate() { + cumulative_size += size; + if cumulative_size > -(scroll_offset.y + padding.top) { + first_visible_element_ix = i; + break; + } + } + + cumulative_size = px(0.); + let mut last_visible_element_ix = 0; + for (i, &size) in item_sizes.iter().enumerate() { + cumulative_size += size; + if cumulative_size > (-scroll_offset.y + padded_bounds.size.height) + { + last_visible_element_ix = i + 1; + break; + } + } + if last_visible_element_ix == 0 { + last_visible_element_ix = self.items_count; + } else { + last_visible_element_ix += 1; + } + (first_visible_element_ix, last_visible_element_ix) + } + }; + + let visible_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.items_count); + + let items = (self.render_items)(visible_range.clone(), content_size, cx); + + let content_mask = ContentMask { bounds }; + cx.with_content_mask(Some(content_mask), |cx| { + for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { + let item_origin = match self.axis { + Axis::Horizontal => { + padded_bounds.origin + + point( + item_origins[ix] + scroll_offset.x, + padding.top + scroll_offset.y, + ) + } + Axis::Vertical => { + padded_bounds.origin + + point( + scroll_offset.x, + padding.top + item_origins[ix] + scroll_offset.y, + ) + } + }; + + let available_space = match self.axis { + Axis::Horizontal => size( + AvailableSpace::Definite(item_sizes[ix]), + AvailableSpace::Definite(padded_bounds.size.height), + ), + Axis::Vertical => size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_sizes[ix]), + ), + }; + + item.layout_as_root(available_space, cx); + item.prepaint_at(item_origin, cx); + layout.items.push(item); + } + }); + } + + hitbox + }, + ) + } + + fn paint( + &mut self, + global_id: Option<&GlobalElementId>, + bounds: Bounds, + layout: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, + cx: &mut WindowContext, + ) { + self.base + .interactivity() + .paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| { + for item in &mut layout.items { + item.paint(cx); + } + }) + } +}