From 377f1694200692abefb43f9d955b6d8d750e16fa Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 16 Dec 2024 08:53:55 +0700 Subject: [PATCH] wip: refactor --- Cargo.lock | 112 +- crates/app/src/views/app.rs | 4 +- crates/app/src/views/dock/chat/list.rs | 0 crates/app/src/views/dock/chat/messages.rs | 66 ++ crates/app/src/views/dock/chat/mod.rs | 19 +- crates/app/src/views/dock/inbox/chat.rs | 2 +- crates/ui/LICENSE | 40 +- crates/ui/src/dock/dock.rs | 16 +- crates/ui/src/dock/tab_panel.rs | 106 +- crates/ui/src/dropdown.rs | 12 +- crates/ui/src/lib.rs | 2 - crates/ui/src/list/list.rs | 4 +- crates/ui/src/scroll/scrollable_mask.rs | 2 +- crates/ui/src/scroll/scrollbar.rs | 2 +- crates/ui/src/tab/tab.rs | 13 +- crates/ui/src/table.rs | 1226 -------------------- 16 files changed, 245 insertions(+), 1381 deletions(-) delete mode 100644 crates/app/src/views/dock/chat/list.rs create mode 100644 crates/app/src/views/dock/chat/messages.rs delete mode 100644 crates/ui/src/table.rs diff --git a/Cargo.lock b/Cargo.lock index b9f570b..7e0a686 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,7 +158,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus 5.1.1", + "zbus 5.2.0", ] [[package]] @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "serde", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "jobserver", "libc", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "rustc-hash 1.1.0", ] @@ -1268,9 +1268,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1287,18 +1287,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1379,7 +1379,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "proc-macro2", "quote", @@ -2104,7 +2104,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" 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#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" 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#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "anyhow", "bytes", @@ -3065,7 +3065,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "anyhow", "bindgen", @@ -3234,7 +3234,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "aes", "async-trait", @@ -3265,7 +3265,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "async-trait", "flatbuffers", @@ -3276,7 +3276,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "async-utility", "heed", @@ -3288,7 +3288,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "async-utility", "async-wsocket", @@ -3304,7 +3304,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "async-utility", "lnurl-pay", @@ -3321,7 +3321,7 @@ dependencies = [ [[package]] name = "nostr-zapper" version = "0.37.0" -source = "git+https://github.com/rust-nostr/nostr#0d12a1d44eaa223d065d4136179296124d955765" +source = "git+https://github.com/rust-nostr/nostr#c616b2f598ef65e8d7e2b6b0959aa28e9c5d9d41" dependencies = [ "async-trait", "nostr", @@ -3916,7 +3916,7 @@ dependencies = [ "rustc-hash 2.1.0", "rustls", "socket2 0.5.8", - "thiserror 2.0.6", + "thiserror 2.0.7", "tokio", "tracing", ] @@ -3935,7 +3935,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.6", + "thiserror 2.0.7", "tinyvec", "tracing", "web-time", @@ -4090,9 +4090,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.22.5" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" dependencies = [ "bytemuck", "font-types", @@ -4100,9 +4100,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -4121,7 +4121,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "derive_refineable", ] @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "anyhow", "bytes", @@ -4396,9 +4396,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -4431,9 +4431,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" dependencies = [ "web-time", ] @@ -4662,7 +4662,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "anyhow", "serde", @@ -4670,24 +4670,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -4988,7 +4988,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "arrayvec", "log", @@ -5263,11 +5263,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.7", ] [[package]] @@ -5283,9 +5283,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", @@ -5783,7 +5783,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#8a85d6ef96cc9d38e1c0e7e420765cd4b31b6954" +source = "git+https://github.com/zed-industries/zed#af50261ae240a2c44bc742f434be46032e47256e" dependencies = [ "anyhow", "async-fs 1.6.0", @@ -6612,9 +6612,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" dependencies = [ "async-broadcast", "async-executor", @@ -6641,7 +6641,7 @@ dependencies = [ "windows-sys 0.59.0", "winnow", "xdg-home", - "zbus_macros 5.1.1", + "zbus_macros 5.2.0", "zbus_names 4.1.0", "zvariant 5.1.0", ] @@ -6661,9 +6661,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6804,9 +6804,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index f70aced..c7b2b71 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -17,7 +17,7 @@ use crate::states::account::AccountRegistry; #[derive(Clone, PartialEq, Eq, Deserialize)] pub struct AddPanel { pub title: Option, - pub receiver: PublicKey, + pub from: PublicKey, } impl_actions!(dock, [AddPanel]); @@ -98,7 +98,7 @@ impl AppView { } fn on_action_add_panel(&mut self, action: &AddPanel, cx: &mut ViewContext) { - let chat_panel = Arc::new(ChatPanel::new(action.receiver, cx)); + let chat_panel = Arc::new(ChatPanel::new(action.from, cx)); self.dock.update(cx, |dock_area, cx| { dock_area.add_panel(chat_panel, DockPlacement::Center, cx); diff --git a/crates/app/src/views/dock/chat/list.rs b/crates/app/src/views/dock/chat/list.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/app/src/views/dock/chat/messages.rs b/crates/app/src/views/dock/chat/messages.rs new file mode 100644 index 0000000..56a75ce --- /dev/null +++ b/crates/app/src/views/dock/chat/messages.rs @@ -0,0 +1,66 @@ +use gpui::*; +use nostr_sdk::prelude::*; + +use crate::get_client; + +pub struct Messages { + messages: Model>, +} + +impl Messages { + pub fn new(from: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self { + let messages = cx.new_model(|_| None); + let async_messages = messages.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(); + + let recv_filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(from) + .pubkey(public_key); + + let sender_filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(public_key) + .pubkey(from); + + let events = async_cx + .background_executor() + .spawn(async move { + client + .database() + .query(vec![recv_filter, sender_filter]) + .await + }) + .await; + + if let Ok(events) = events { + _ = async_cx.update_model(&async_messages, |a, b| { + *a = Some(events); + b.notify(); + }); + } + }) + .detach(); + + Self { messages } + } +} + +impl Render for Messages { + 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) + } +} diff --git a/crates/app/src/views/dock/chat/mod.rs b/crates/app/src/views/dock/chat/mod.rs index f4960d5..fc0203b 100644 --- a/crates/app/src/views/dock/chat/mod.rs +++ b/crates/app/src/views/dock/chat/mod.rs @@ -7,9 +7,10 @@ use coop_ui::{ Sizable, }; use gpui::*; +use messages::Messages; use nostr_sdk::*; -pub mod list; +pub mod messages; pub struct ChatPanel { // Panel @@ -18,13 +19,14 @@ pub struct ChatPanel { zoomable: bool, focus_handle: FocusHandle, // Chat Room - receiver: PublicKey, + messages: View, input: View, } impl ChatPanel { - pub fn new(receiver: PublicKey, cx: &mut WindowContext) -> View { + pub fn new(from: PublicKey, cx: &mut WindowContext) -> View { let input = cx.new_view(TextInput::new); + let messages = cx.new_view(|cx| Messages::new(from, cx)); input.update(cx, |input, _cx| { input.set_placeholder("Message"); @@ -35,7 +37,7 @@ impl ChatPanel { closeable: true, zoomable: true, focus_handle: cx.focus_handle(), - receiver, + messages, input, }) } @@ -89,14 +91,7 @@ impl Render for ChatPanel { .size_full() .flex() .flex_col() - .child( - div() - .flex_1() - .flex() - .items_center() - .justify_center() - .child(self.receiver.to_hex()), - ) + .child(self.messages.clone()) .child( div() .flex_shrink_0() diff --git a/crates/app/src/views/dock/inbox/chat.rs b/crates/app/src/views/dock/inbox/chat.rs index 5b293d0..609fc40 100644 --- a/crates/app/src/views/dock/inbox/chat.rs +++ b/crates/app/src/views/dock/inbox/chat.rs @@ -130,7 +130,7 @@ impl RenderOnce for ChatItem { .on_click(move |_, cx| { cx.dispatch_action(Box::new(AddPanel { title: self.title.clone(), - receiver: self.public_key, + from: self.public_key, })) }) } diff --git a/crates/ui/LICENSE b/crates/ui/LICENSE index 16fe87b..dd3ad77 100644 --- a/crates/ui/LICENSE +++ b/crates/ui/LICENSE @@ -1,3 +1,18 @@ +Copyright 2024 Longbridge + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -174,28 +189,3 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index d7e2386..98f7637 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -138,10 +138,18 @@ impl Dock { Self::subscribe_panel_events(dock_area.clone(), &panel, cx); if !open { - if let DockItem::Tabs { view, .. } = panel.clone() { - view.update(cx, |panel, cx| { - panel.set_collapsed(true, cx); - }); + match panel.clone() { + DockItem::Tabs { view, .. } => { + view.update(cx, |panel, cx| { + panel.set_collapsed(true, cx); + }); + } + DockItem::Split { items, .. } => { + for item in items { + item.set_collapsed(true, cx); + } + } + _ => {} } } diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 61eb1d5..2bed226 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use gpui::{ div, prelude::FluentBuilder, px, rems, AnchorCorner, AppContext, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, FocusableView, @@ -7,7 +5,12 @@ use gpui::{ SharedString, StatefulInteractiveElement, Styled, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; +use std::sync::Arc; +use super::{ + ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelStyle, PanelView, + StackPanel, ToggleZoom, +}; use crate::{ button::{Button, ButtonVariants as _}, dock::DockItemInfo, @@ -18,11 +21,6 @@ use crate::{ v_flex, AxisExt, IconName, Placement, Selectable, Sizable, }; -use super::{ - ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelStyle, PanelView, - StackPanel, ToggleZoom, -}; - #[derive(Clone, Copy)] struct TabState { closeable: bool, @@ -173,6 +171,15 @@ impl TabPanel { /// Add a panel to the end of the tabs pub fn add_panel(&mut self, panel: Arc, cx: &mut ViewContext) { + self.add_panel_with_active(panel, true, cx); + } + + fn add_panel_with_active( + &mut self, + panel: Arc, + active: bool, + cx: &mut ViewContext, + ) { assert_ne!( panel.panel_name(cx), "StackPanel", @@ -189,7 +196,9 @@ impl TabPanel { self.panels.push(panel); // set the active panel to the new panel - self.set_active_ix(self.panels.len() - 1, cx); + if active { + self.set_active_ix(self.panels.len() - 1, cx); + } cx.emit(PanelEvent::LayoutChanged); cx.notify(); } @@ -543,6 +552,7 @@ impl TabPanel { ) .children(self.panels.iter().enumerate().map(|(ix, panel)| { let mut active = ix == self.active_ix; + let disabled = self.is_collapsed; // Always not show active tab style, if the panel is collapsed if self.is_collapsed { @@ -552,31 +562,34 @@ impl TabPanel { Tab::new(("tab", ix), panel.title(cx)) .py_2() .selected(active) - .on_click(cx.listener(move |view, _, cx| { - view.set_active_ix(ix, cx); - })) - .when(state.draggable, |this| { - this.on_drag( - DragPanel::new(panel.clone(), view.clone()), - |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }, - ) - }) - .when(state.droppable, |this| { - this.drag_over::(|this, _, cx| { - this.rounded_l_none() - .border_l_2() - .border_r_0() - .border_color(cx.theme().drag_border) + .disabled(disabled) + .when(!disabled, |this| { + this.on_click(cx.listener(move |view, _, cx| { + view.set_active_ix(ix, cx); + })) + .when(state.draggable, |this| { + this.on_drag( + DragPanel::new(panel.clone(), view.clone()), + |drag, _, cx| { + cx.stop_propagation(); + cx.new_view(|_| drag.clone()) + }, + ) + }) + .when(state.droppable, |this| { + this.drag_over::(|this, _, cx| { + this.rounded_l_none() + .border_l_2() + .border_r_0() + .border_color(cx.theme().drag_border) + }) + .on_drop(cx.listener( + move |this, drag: &DragPanel, cx| { + this.will_split_placement = None; + this.on_drop(drag, Some(ix), true, cx) + }, + )) }) - .on_drop(cx.listener( - move |this, drag: &DragPanel, cx| { - this.will_split_placement = None; - this.on_drop(drag, Some(ix), cx) - }, - )) }) })) .child( @@ -597,7 +610,7 @@ impl TabPanel { None }; - this.on_drop(drag, ix, cx) + this.on_drop(drag, ix, false, cx) })) }), ) @@ -620,6 +633,10 @@ impl TabPanel { } fn render_active_panel(&self, state: TabState, cx: &mut ViewContext) -> impl IntoElement { + if self.is_collapsed { + return Empty {}.into_any_element(); + } + self.active_panel() .map(|panel| { div() @@ -658,7 +675,7 @@ impl TabPanel { }) .group_drag_over::("", |this| this.visible()) .on_drop(cx.listener(|this, drag: &DragPanel, cx| { - this.on_drop(drag, None, cx) + this.on_drop(drag, None, true, cx) })), ) }) @@ -688,17 +705,28 @@ impl TabPanel { cx.notify() } - fn on_drop(&mut self, drag: &DragPanel, ix: Option, cx: &mut ViewContext) { + /// Handle the drop event when dragging a panel + /// + /// - `active` - When true, the panel will be active after the drop + fn on_drop( + &mut self, + drag: &DragPanel, + ix: Option, + active: bool, + cx: &mut ViewContext, + ) { let panel = drag.panel.clone(); let is_same_tab = drag.tab_panel == *cx.view(); // If target is same tab, and it is only one panel, do nothing. if is_same_tab && ix.is_none() { - #[allow(clippy::if_same_then_else)] if self.will_split_placement.is_none() { return; - } else if self.panels.len() == 1 { - return; + } else { + #[allow(clippy::collapsible_else_if)] + if self.panels.len() == 1 { + return; + } } } @@ -721,7 +749,7 @@ impl TabPanel { } else if let Some(ix) = ix { self.insert_panel_at(panel, ix, cx) } else { - self.add_panel(panel, cx) + self.add_panel_with_active(panel, active, cx) } self.remove_self_if_empty(cx); diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 8927855..d72512f 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -81,7 +81,7 @@ pub trait DropdownDelegate: Sized { } fn perform_search(&mut self, _query: &str, _cx: &mut ViewContext>) -> Task<()> { - Task::Ready(Some(())) + Task::ready(()) } } @@ -178,11 +178,9 @@ where } fn perform_search(&mut self, query: &str, cx: &mut ViewContext>) -> Task<()> { - self.dropdown - .upgrade() - .map_or(Task::Ready(None), |dropdown| { - dropdown.update(cx, |_, cx| self.delegate.perform_search(query, cx)) - }) + self.dropdown.upgrade().map_or(Task::ready(()), |dropdown| { + dropdown.update(cx, |_, cx| self.delegate.perform_search(query, cx)) + }) } fn set_selected_index(&mut self, ix: Option, _: &mut ViewContext>) { @@ -285,7 +283,7 @@ impl DropdownDelegate for SearchableVec { .cloned() .collect(); - Task::Ready(Some(())) + Task::ready(()) } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index a07b075..b277573 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -42,7 +42,6 @@ pub mod skeleton; pub mod slider; pub mod switch; pub mod tab; -pub mod table; pub mod theme; pub mod tooltip; @@ -78,5 +77,4 @@ pub fn init(cx: &mut gpui::AppContext) { modal::init(cx); popover::init(cx); popup_menu::init(cx); - table::init(cx); } diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 0eca616..eb3240e 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -37,7 +37,7 @@ pub trait ListDelegate: Sized + 'static { /// When Query Input change, this method will be called. /// You can perform search here. fn perform_search(&mut self, query: &str, cx: &mut ViewContext>) -> Task<()> { - Task::Ready(Some(())) + Task::ready(()) } /// Return the number of items in the list. @@ -126,7 +126,7 @@ where enable_scrollbar: true, loading: false, size: Size::default(), - _search_task: Task::Ready(None), + _search_task: Task::ready(()), } } diff --git a/crates/ui/src/scroll/scrollable_mask.rs b/crates/ui/src/scroll/scrollable_mask.rs index 5e7b168..fc2bbe5 100644 --- a/crates/ui/src/scroll/scrollable_mask.rs +++ b/crates/ui/src/scroll/scrollable_mask.rs @@ -121,7 +121,7 @@ impl Element for ScrollableMask { bounds, border_widths: Edges::all(px(1.0)), border_color: color, - background: gpui::transparent_white(), + background: gpui::transparent_white().into(), corner_radii: Corners::all(px(0.)), }); } diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 9e30774..45cec31 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -597,7 +597,7 @@ impl Element for Scrollbar { cx.paint_quad(PaintQuad { bounds, corner_radii: (0.).into(), - background: gpui::transparent_black(), + background: gpui::transparent_black().into(), border_widths: if is_vertical { Edges { top: px(0.), diff --git a/crates/ui/src/tab/tab.rs b/crates/ui/src/tab/tab.rs index df3521d..89ea53a 100644 --- a/crates/ui/src/tab/tab.rs +++ b/crates/ui/src/tab/tab.rs @@ -42,6 +42,12 @@ impl Tab { self.suffix = Some(suffix.into()); self } + + /// Set disabled state to the tab + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } } impl Selectable for Tab { @@ -72,9 +78,11 @@ impl Styled for Tab { impl RenderOnce for Tab { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let (text_color, bg_color) = match (self.selected, self.disabled) { - (true, _) => (cx.theme().tab_active_foreground, cx.theme().tab_active), - (false, true) => (cx.theme().tab_foreground.opacity(0.5), cx.theme().tab), + (true, false) => (cx.theme().tab_active_foreground, cx.theme().tab_active), (false, false) => (cx.theme().muted_foreground, cx.theme().tab), + // disabled + (true, true) => (cx.theme().muted_foreground, cx.theme().tab_active), + (false, true) => (cx.theme().muted_foreground, cx.theme().tab), }; self.base @@ -89,7 +97,6 @@ impl RenderOnce for Tab { .border_color(cx.theme().transparent) .when(self.selected, |this| this.border_color(cx.theme().border)) .text_sm() - .when(self.disabled, |this| this) .when_some(self.prefix, |this, prefix| { this.child(prefix).text_color(text_color) }) diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs deleted file mode 100644 index 0b76564..0000000 --- a/crates/ui/src/table.rs +++ /dev/null @@ -1,1226 +0,0 @@ -use std::{cell::Cell, ops::Range, rc::Rc}; - -use crate::{ - context_menu::ContextMenuExt, - h_flex, - popup_menu::PopupMenu, - scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, - theme::ActiveTheme, - v_flex, Icon, IconName, Sizable, Size, StyleSized as _, -}; -use gpui::{ - actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, - DragMoveEvent, Edges, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, - InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, ParentElement, - Pixels, Point, Render, ScrollHandle, ScrollStrategy, SharedString, Stateful, - StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext, - VisualContext as _, WindowContext, -}; - -actions!( - table, - [ - Cancel, - SelectPrev, - SelectNext, - SelectPrevColumn, - SelectNextColumn - ] -); - -pub fn init(cx: &mut AppContext) { - let context = Some("Table"); - cx.bind_keys([ - KeyBinding::new("escape", Cancel, context), - KeyBinding::new("up", SelectPrev, context), - KeyBinding::new("down", SelectNext, context), - KeyBinding::new("left", SelectPrevColumn, context), - KeyBinding::new("right", SelectNextColumn, context), - ]); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ColFixed { - Left, -} - -#[derive(Debug, Clone, Copy)] -struct ColGroup { - width: Option, - bounds: Bounds, - sort: Option, - fixed: Option, - padding: Option>, -} - -#[derive(Clone)] -pub(crate) struct DragCol { - pub(crate) entity_id: EntityId, - pub(crate) name: SharedString, - pub(crate) width: Option, - pub(crate) col_ix: usize, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ColSort { - /// No sorting. - Default, - /// Sort in ascending order. - Ascending, - /// Sort in descending order. - Descending, -} - -impl Render for DragCol { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .px_4() - .py_1() - .bg(cx.theme().table_head) - .text_color(cx.theme().muted_foreground) - .opacity(0.9) - .border_1() - .border_color(cx.theme().border) - .shadow_md() - .when_some(self.width, |this, width| this.w(width)) - .min_w(px(100.)) - .max_w(px(450.)) - .child(self.name.clone()) - } -} - -#[derive(Clone, Render)] -pub struct ResizeCol(pub (EntityId, usize)); - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum SelectionState { - Column, - Row, -} - -#[derive(Clone)] -pub enum TableEvent { - SelectRow(usize), - SelectCol(usize), - ColWidthsChanged(Vec>), - MoveCol(usize, usize), -} - -#[derive(Clone, Copy, Default)] -struct FixedCols { - left: usize, -} - -pub struct Table { - focus_handle: FocusHandle, - delegate: D, - /// The bounds of the table container. - bounds: Bounds, - /// The bounds of the fixed head cols. - fixed_head_cols_bounds: Bounds, - /// The bounds of the table head content. - head_content_bounds: Bounds, - - col_groups: Vec, - fixed_cols: FixedCols, - - pub vertical_scroll_handle: UniformListScrollHandle, - pub scrollbar_state: Rc>, - pub horizontal_scroll_handle: ScrollHandle, - pub horizontal_scrollbar_state: Rc>, - - selected_row: Option, - selection_state: SelectionState, - right_clicked_row: Option, - selected_col: Option, - - /// The column index that is being resized. - resizing_col: Option, - - /// Set stripe style of the table. - stripe: bool, - /// Set to use border style of the table. - border: bool, - /// The cell size of the table. - size: Size, -} - -#[allow(unused)] -pub trait TableDelegate: Sized + 'static { - /// Return the number of columns in the table. - fn cols_count(&self, cx: &AppContext) -> usize; - /// Return the number of rows in the table. - fn rows_count(&self, cx: &AppContext) -> usize; - - /// Returns the name of the column at the given index. - fn col_name(&self, col_ix: usize, cx: &AppContext) -> SharedString; - - /// Returns whether the column at the given index can be resized. Default: true - fn can_resize_col(&self, col_ix: usize, cx: &AppContext) -> bool { - true - } - - /// Returns whether the column at the given index can be selected. Default: false - fn can_select_col(&self, col_ix: usize, cx: &AppContext) -> bool { - false - } - - /// Returns the width of the column at the given index. - /// Return None, use auto width. - /// - /// This is only called when the table initializes. - fn col_width(&self, col_ix: usize, cx: &AppContext) -> Option; - - /// Return the sort state of the column at the given index. - /// - /// This is only called when the table initializes. - fn col_sort(&self, col_ix: usize, cx: &AppContext) -> Option { - None - } - - /// Return the fixed side of the column at the given index. - fn col_fixed(&self, col_ix: usize, cx: &AppContext) -> Option { - None - } - - /// Return the padding of the column at the given index to override the default padding. - /// - /// Return None, use the default padding. - fn col_padding(&self, col_ix: usize, cx: &AppContext) -> Option> { - None - } - - /// Perform sort on the column at the given index. - fn perform_sort(&mut self, col_ix: usize, sort: ColSort, cx: &mut ViewContext>) {} - - /// Render the header cell at the given column index, default to the column name. - fn render_th(&self, col_ix: usize, cx: &mut ViewContext>) -> impl IntoElement { - div().size_full().child(self.col_name(col_ix, cx)) - } - - /// Render the row at the given row and column. - fn render_tr(&self, row_ix: usize, cx: &mut ViewContext>) -> Stateful
{ - h_flex().id(("table-row", row_ix)) - } - - /// Render the context menu for the row at the given row index. - fn context_menu(&self, row_ix: usize, menu: PopupMenu, cx: &WindowContext) -> PopupMenu { - menu - } - - /// Render cell at the given row and column. - fn render_td( - &self, - row_ix: usize, - col_ix: usize, - cx: &mut ViewContext>, - ) -> impl IntoElement; - - /// Return true to enable loop selection on the table. - /// - /// When the prev/next selection is out of the table bounds, the selection will loop to the other side. - /// - /// Default: true - fn can_loop_select(&self, _: &AppContext) -> bool { - true - } - - /// Return true to enable column order change. - fn can_move_col(&self, col_ix: usize, cx: &AppContext) -> bool { - false - } - - /// Move the column at the given `col_ix` to insert before the column at the given `to_ix`. - fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext>) {} - - /// Return a Element to show when table is empty. - fn render_empty(&self, cx: &mut ViewContext>) -> impl IntoElement { - h_flex() - .size_full() - .justify_center() - .py_6() - .text_color(cx.theme().muted_foreground.opacity(0.6)) - .child(Icon::new(IconName::Inbox).size_12()) - .into_any_element() - } - - /// Return true to enable load more data when scrolling to the bottom. - /// - /// Default: true - fn can_load_more(&self, cx: &AppContext) -> bool { - true - } - - /// Returns a threshold value (n rows), of course, when scrolling to the bottom, - /// the remaining number of rows triggers `load_more`. - /// - /// Default: 50 rows - fn load_more_threshold(&self) -> usize { - 50 - } - - /// Load more data when the table is scrolled to the bottom. - /// - /// This will performed in a background task. - /// - /// This is always called when the table is near the bottom, - /// so you must check if there is more data to load or lock the loading state. - fn load_more(&mut self, cx: &mut ViewContext>) {} - - /// Render the last empty column, default to empty. - fn render_last_empty_col(&mut self, cx: &mut ViewContext>) -> Div { - h_flex().w(px(100.)).h_full().flex_shrink_0() - } -} - -impl Table -where - D: TableDelegate, -{ - pub fn new(delegate: D, cx: &mut ViewContext) -> Self { - let mut this = Self { - focus_handle: cx.focus_handle(), - delegate, - col_groups: Vec::new(), - fixed_cols: FixedCols::default(), - horizontal_scroll_handle: ScrollHandle::new(), - vertical_scroll_handle: UniformListScrollHandle::new(), - scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), - horizontal_scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), - selection_state: SelectionState::Row, - selected_row: None, - right_clicked_row: None, - selected_col: None, - resizing_col: None, - bounds: Bounds::default(), - fixed_head_cols_bounds: Bounds::default(), - head_content_bounds: Bounds::default(), - stripe: false, - border: true, - size: Size::default(), - }; - - this.prepare_col_groups(cx); - this - } - - pub fn delegate(&self) -> &D { - &self.delegate - } - - pub fn delegate_mut(&mut self) -> &mut D { - &mut self.delegate - } - - /// Set to use stripe style of the table, default to false. - pub fn stripe(mut self, stripe: bool) -> Self { - self.stripe = stripe; - self - } - - pub fn set_stripe(&mut self, stripe: bool, cx: &mut ViewContext) { - self.stripe = stripe; - cx.notify(); - } - - /// Set to use border style of the table, default to true. - pub fn border(mut self, border: bool) -> Self { - self.border = border; - self - } - - /// Set the size to the table. - pub fn set_size(&mut self, size: Size, cx: &mut ViewContext) { - self.size = size; - cx.notify(); - } - - /// When we update columns or rows, we need to refresh the table. - pub fn refresh(&mut self, cx: &mut ViewContext) { - self.prepare_col_groups(cx); - } - - fn prepare_col_groups(&mut self, cx: &mut ViewContext) { - self.col_groups = (0..self.delegate.cols_count(cx)) - .map(|col_ix| ColGroup { - width: self.delegate.col_width(col_ix, cx), - padding: self.delegate.col_padding(col_ix, cx), - bounds: Bounds::default(), - sort: self.delegate.col_sort(col_ix, cx), - fixed: self.delegate.col_fixed(col_ix, cx), - }) - .collect(); - self.fixed_cols.left = self - .col_groups - .iter() - .filter(|col| col.fixed == Some(ColFixed::Left)) - .count(); - cx.notify(); - } - - fn scroll_to_row(&mut self, row_ix: usize, cx: &mut ViewContext) { - self.vertical_scroll_handle - .scroll_to_item(row_ix, ScrollStrategy::Top); - cx.notify(); - } - - /// Returns the selected row index. - pub fn selected_row(&self) -> Option { - self.selected_row - } - - /// Sets the selected row to the given index. - pub fn set_selected_row(&mut self, row_ix: usize, cx: &mut ViewContext) { - self.selection_state = SelectionState::Row; - self.right_clicked_row = None; - self.selected_row = Some(row_ix); - if let Some(row_ix) = self.selected_row { - self.vertical_scroll_handle - .scroll_to_item(row_ix, ScrollStrategy::Top); - } - cx.emit(TableEvent::SelectRow(row_ix)); - cx.notify(); - } - - /// Returns the selected column index. - pub fn selected_col(&self) -> Option { - self.selected_col - } - - /// Sets the selected col to the given index. - pub fn set_selected_col(&mut self, col_ix: usize, cx: &mut ViewContext) { - self.selection_state = SelectionState::Column; - self.selected_col = Some(col_ix); - if let Some(_col_ix) = self.selected_col { - // TODO: Fix scroll to selected col, this was not working after fixed col. - // if self.col_groups[col_ix].fixed.is_none() { - // self.horizontal_scroll_handle.scroll_to_item(col_ix); - // } - } - cx.emit(TableEvent::SelectCol(col_ix)); - cx.notify(); - } - - fn on_row_click( - &mut self, - mouse_button: MouseButton, - row_ix: usize, - cx: &mut ViewContext, - ) { - if mouse_button == MouseButton::Right { - self.right_clicked_row = Some(row_ix); - } else { - self.set_selected_row(row_ix, cx) - } - } - - fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext) { - if !self.delegate.can_select_col(col_ix, cx) { - return; - } - - self.set_selected_col(col_ix, cx) - } - - fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.selection_state = SelectionState::Row; - self.selected_row = None; - self.selected_col = None; - cx.notify(); - } - - fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - let mut selected_row = self.selected_row.unwrap_or(0); - let rows_count = self.delegate.rows_count(cx); - if selected_row > 0 { - selected_row -= 1; - } else if self.delegate.can_loop_select(cx) { - selected_row = rows_count - 1; - } - - self.set_selected_row(selected_row, cx); - } - - fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - let mut selected_row = self.selected_row.unwrap_or(0); - if selected_row < self.delegate.rows_count(cx) - 1 { - selected_row += 1; - } else if self.delegate.can_loop_select(cx) { - selected_row = 0; - } - - self.set_selected_row(selected_row, cx); - } - - fn action_select_prev_col(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext) { - let mut selected_col = self.selected_col.unwrap_or(0); - let cols_count = self.delegate.cols_count(cx); - if selected_col > 0 { - selected_col -= 1; - } else if self.delegate.can_loop_select(cx) { - selected_col = cols_count - 1; - } - self.set_selected_col(selected_col, cx); - } - - fn action_select_next_col(&mut self, _: &SelectNextColumn, cx: &mut ViewContext) { - let mut selected_col = self.selected_col.unwrap_or(0); - if selected_col < self.delegate.cols_count(cx) - 1 { - selected_col += 1; - } else if self.delegate.can_loop_select(cx) { - selected_col = 0; - } - - self.set_selected_col(selected_col, cx); - } - - /// Scroll table when mouse position is near the edge of the table bounds. - fn scroll_table_by_col_resizing( - &mut self, - pos: Point, - col_group: ColGroup, - _: &mut ViewContext, - ) { - let mut offset = self.horizontal_scroll_handle.offset(); - let col_bounds = col_group.bounds; - - if pos.x < self.bounds.left() && col_bounds.right() < self.bounds.left() + px(20.) { - offset.x += px(1.); - } else if pos.x > self.bounds.right() && col_bounds.right() > self.bounds.right() - px(20.) - { - offset.x -= px(1.); - } - - self.horizontal_scroll_handle.set_offset(offset); - } - - /// The `ix`` is the index of the col to resize, - /// and the `size` is the new size for the col. - fn resize_cols(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext) { - const MIN_WIDTH: Pixels = px(10.0); - const MAX_WIDTH: Pixels = px(1200.0); - - if !self.delegate.can_resize_col(ix, cx) { - return; - } - let size = size.floor(); - - let old_width = self.col_groups[ix].width.unwrap_or_default(); - let new_width = size; - if new_width < MIN_WIDTH { - return; - } - let changed_width = new_width - old_width; - // If change size is less than 1px, do nothing. - if changed_width > px(-1.0) && changed_width < px(1.0) { - return; - } - self.col_groups[ix].width = Some(new_width.min(MAX_WIDTH)); - - // Resize next col, table not need to resize the right cols. - // let next_width = self.col_groups[ix + 1].width.unwrap_or_default(); - // let next_width = (next_width - changed_width).max(MIN_WIDTH); - // self.col_groups[ix + 1].width = Some(next_width); - - cx.notify(); - } - - fn perform_sort(&mut self, col_ix: usize, cx: &mut ViewContext) { - let sort = self.col_groups.get(col_ix).and_then(|g| g.sort); - if sort.is_none() { - return; - } - - let sort = sort.unwrap(); - let sort = match sort { - ColSort::Ascending => ColSort::Default, - ColSort::Descending => ColSort::Ascending, - ColSort::Default => ColSort::Descending, - }; - - for (ix, col_group) in self.col_groups.iter_mut().enumerate() { - if ix == col_ix { - col_group.sort = Some(sort); - } else if col_group.sort.is_some() { - col_group.sort = Some(ColSort::Default); - } - } - - self.delegate_mut().perform_sort(col_ix, sort, cx); - - cx.notify(); - } - - fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext) { - if col_ix == to_ix { - return; - } - - self.delegate.move_col(col_ix, to_ix, cx); - let col_group = self.col_groups.remove(col_ix); - self.col_groups.insert(to_ix, col_group); - - cx.emit(TableEvent::MoveCol(col_ix, to_ix)); - cx.notify(); - } - - /// Dispatch delegate's `load_more` method when the visible range is near the end. - fn load_more(&mut self, visible_range: Range, cx: &mut ViewContext) { - if !self.delegate.can_load_more(cx) { - return; - } - - let row_count = self.delegate.rows_count(cx); - let load_more_count = self.delegate.load_more_threshold(); - - // Securely handle subtract logic to prevent attempt to subtract with overflow - if row_count >= load_more_count && visible_range.end >= row_count - load_more_count { - cx.spawn(|view, mut cx| async move { - cx.update(|cx| { - view.update(cx, |view, cx| { - view.delegate.load_more(cx); - }) - }) - }) - .detach() - } - } - - /// Returns the size of the content area. - fn head_content_bounds(&self) -> gpui::Bounds { - let has_fixed_cols = self.fixed_head_cols_bounds.size.width > px(0.0); - Bounds { - origin: if has_fixed_cols { - self.fixed_head_cols_bounds.origin - } else { - self.head_content_bounds.origin - }, - size: gpui::size( - self.fixed_head_cols_bounds.size.width + self.head_content_bounds.size.width, - self.head_content_bounds.size.height, - ), - } - } - - fn render_cell(&self, col_ix: usize, _cx: &mut ViewContext) -> Div { - let col_width = self.col_groups[col_ix].width; - let col_padding = self.col_groups[col_ix].padding; - - div() - .when_some(col_width, |this, width| this.w(width)) - .h_full() - .flex_shrink_0() - .overflow_hidden() - .whitespace_nowrap() - .table_cell_size(self.size) - .map(|this| match col_padding { - Some(padding) => this - .pl(padding.left) - .pr(padding.right) - .pt(padding.top) - .pb(padding.bottom), - None => this, - }) - } - - /// Show Column selection style, when the column is selected and the selection state is Column. - fn render_col_wrap(&self, col_ix: usize, cx: &mut ViewContext) -> Div { - let el = h_flex().h_full(); - - if self.delegate().can_select_col(col_ix, cx) - && self.selected_col == Some(col_ix) - && self.selection_state == SelectionState::Column - { - el.bg(cx.theme().table_active) - } else { - el - } - } - - fn render_scrollbar(&self, cx: &mut ViewContext) -> Option { - let state = self.scrollbar_state.clone(); - - Some( - div() - .absolute() - .top(self.size.table_row_height()) - .left_0() - .right_0() - .bottom_0() - .child(Scrollbar::uniform_scroll( - cx.view().entity_id(), - state, - self.vertical_scroll_handle.clone(), - )), - ) - } - - fn render_horizontal_scrollbar(&self, cx: &mut ViewContext) -> impl IntoElement { - let state = self.horizontal_scrollbar_state.clone(); - - div() - .absolute() - .top_0() - .left_0() - .right_0() - .bottom_0() - .size_full() - .child(Scrollbar::horizontal( - cx.view().entity_id(), - state, - self.horizontal_scroll_handle.clone(), - self.head_content_bounds().size, - )) - } - - fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext) -> impl IntoElement { - const HANDLE_SIZE: Pixels = px(2.); - - if !self.delegate.can_resize_col(ix, cx) { - return div().into_any_element(); - } - - let group_id = SharedString::from(format!("resizable-handle:{}", ix)); - - h_flex() - .id(("resizable-handle", ix)) - .group(group_id.clone()) - .occlude() - .cursor_col_resize() - .h_full() - .w(HANDLE_SIZE) - .ml(-(HANDLE_SIZE)) - .justify_end() - .items_center() - .child( - div() - .h_full() - .justify_center() - .bg(cx.theme().table_row_border) - .group_hover(group_id, |this| this.bg(cx.theme().border).h_full()) - .w(px(1.)), - ) - .on_drag_move(cx.listener(move |view, e: &DragMoveEvent, cx| { - match e.drag(cx) { - ResizeCol((entity_id, ix)) => { - if cx.entity_id() != *entity_id { - return; - } - - // sync col widths into real widths - for col_group in view.col_groups.iter_mut() { - col_group.width = Some(col_group.bounds.size.width); - } - - let ix = *ix; - view.resizing_col = Some(ix); - - let col_group = *view.col_groups.get(ix).expect("BUG: invalid col index"); - - view.resize_cols( - ix, - e.event.position.x - HANDLE_SIZE - col_group.bounds.left(), - cx, - ); - - // scroll the table if the drag is near the edge - view.scroll_table_by_col_resizing(e.event.position, col_group, cx); - } - }; - })) - .on_drag(ResizeCol((cx.entity_id(), ix)), |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|view, _, cx| { - if view.resizing_col.is_none() { - return; - } - - view.resizing_col = None; - - let new_widths = view.col_groups.iter().map(|g| g.width).collect(); - cx.emit(TableEvent::ColWidthsChanged(new_widths)); - cx.notify(); - }), - ) - .into_any_element() - } - - fn render_sort_icon( - &self, - col_ix: usize, - col_group: &ColGroup, - cx: &mut ViewContext, - ) -> Option { - let sort = col_group.sort?; - - let (icon, is_on) = match sort { - ColSort::Ascending => (IconName::SortAscending, true), - ColSort::Descending => (IconName::SortDescending, true), - ColSort::Default => (IconName::ChevronsUpDown, false), - }; - - Some( - div() - .id(("icon-sort", col_ix)) - .cursor_pointer() - .p(px(2.)) - .rounded_sm() - .map(|this| match is_on { - true => this, - false => this.opacity(0.5), - }) - .hover(|this| this.bg(cx.theme().secondary).opacity(7.)) - .active(|this| this.bg(cx.theme().secondary_active).opacity(1.)) - .on_click(cx.listener(move |table, _, cx| table.perform_sort(col_ix, cx))) - .child( - Icon::new(icon) - .size_3() - .text_color(cx.theme().secondary_foreground), - ), - ) - } - - /// Render the column header. - /// The children must be one by one items. - /// Because the horizontal scroll handle will use the child_item_bounds to - /// calculate the item position for itself's `scroll_to_item` method. - fn render_th(&self, col_ix: usize, cx: &mut ViewContext) -> impl IntoElement { - let entity_id = cx.entity_id(); - let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index"); - let moveable = self.delegate.can_move_col(col_ix, cx); - let paddings = self.delegate.col_padding(col_ix, cx); - let name = self.delegate.col_name(col_ix, cx); - - h_flex() - .child( - self.render_cell(col_ix, cx) - .id(("col-header", col_ix)) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, cx| { - this.on_col_head_click(col_ix, cx); - }), - ) - .child( - h_flex() - .size_full() - .justify_between() - .items_center() - .child(self.delegate.render_th(col_ix, cx)) - .when_some(paddings, |this, paddings| { - // Leave right space for the sort icon, if this column have custom padding - let offset_pr = - self.size.table_cell_padding().right - paddings.right; - this.pr(offset_pr.max(px(0.))) - }) - .children(self.render_sort_icon(col_ix, col_group, cx)), - ) - .when(moveable, |this| { - this.on_drag( - DragCol { - entity_id, - col_ix, - name, - width: col_group.width, - }, - |drag, _, cx| { - cx.stop_propagation(); - cx.new_view(|_| drag.clone()) - }, - ) - .drag_over::(|this, _, cx| { - this.rounded_l_none() - .border_l_2() - .border_r_0() - .border_color(cx.theme().drag_border) - }) - .on_drop(cx.listener( - move |table, drag: &DragCol, cx| { - // If the drag col is not the same as the drop col, then swap the cols. - if drag.entity_id != cx.entity_id() { - return; - } - - table.move_col(drag.col_ix, col_ix, cx); - }, - )) - }), - ) - // resize handle - .child(self.render_resize_handle(col_ix, cx)) - // to save the bounds of this col. - .child({ - let view = cx.view().clone(); - canvas( - move |bounds, cx| view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds), - |_, _, _| {}, - ) - .absolute() - .size_full() - }) - } - - fn render_table_head( - &mut self, - left_cols_count: usize, - cx: &mut ViewContext, - ) -> impl IntoElement { - let view = cx.view().clone(); - let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); - - h_flex() - .w_full() - .h(self.size.table_row_height()) - .flex_shrink_0() - .border_b_1() - .border_color(cx.theme().border) - .text_color(cx.theme().table_head_foreground) - .when(left_cols_count > 0, |this| { - let view = view.clone(); - // Render left fixed columns - this.child( - h_flex() - .id("table-head-fixed-left") - .h_full() - .bg(cx.theme().table_head) - .border_r_1() - .border_color(cx.theme().border) - .children( - self.col_groups - .iter() - .filter(|col| col.fixed == Some(ColFixed::Left)) - .enumerate() - .map(|(col_ix, _)| self.render_th(col_ix, cx)), - ) - .child( - canvas( - move |bounds, cx| { - view.update(cx, |r, _| r.fixed_head_cols_bounds = bounds) - }, - |_, _, _| {}, - ) - .absolute() - .size_full(), - ), - ) - }) - .child( - // Columns - h_flex() - .id("table-head") - .size_full() - .overflow_scroll() - .relative() - .track_scroll(&horizontal_scroll_handle) - .bg(cx.theme().table_head) - .child( - h_flex() - .relative() - .children( - self.col_groups - .iter() - .filter(|col| col.fixed.is_none()) - .enumerate() - .map(|(col_ix, _)| { - self.render_th(left_cols_count + col_ix, cx) - }), - ) - .child(self.delegate.render_last_empty_col(cx)) - .child( - canvas( - move |bounds, cx| { - view.update(cx, |r, _| r.head_content_bounds = bounds) - }, - |_, _, _| {}, - ) - .absolute() - .size_full(), - ), - ), - ) - } - - fn render_table_row( - &mut self, - row_ix: usize, - rows_count: usize, - left_cols_count: usize, - cols_count: usize, - cx: &mut ViewContext, - ) -> impl IntoElement { - let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); - let is_stripe_row = self.stripe && row_ix % 2 != 0; - let is_selected = self.selected_row == Some(row_ix); - let view = cx.view().clone(); - - if row_ix < rows_count { - self.delegate - .render_tr(row_ix, cx) - .context_menu(move |this, cx: &mut ViewContext| { - view.read(cx).delegate.context_menu(row_ix, this, cx) - }) - .w_full() - .h(self.size.table_row_height()) - .border_b_1() - .when(row_ix == rows_count, |this| { - this.border_color(gpui::transparent_white()) - }) - .border_color(cx.theme().table_row_border) - .when(is_stripe_row, |this| this.bg(cx.theme().table_even)) - .hover(|this| { - if is_selected || self.right_clicked_row == Some(row_ix) { - this - } else { - this.bg(cx.theme().table_hover) - } - }) - .children(if left_cols_count > 0 { - // Left fixed columns - Some( - h_flex() - .h_full() - .border_r_1() - .border_color(cx.theme().table_row_border) - .children((0..left_cols_count).map(|col_ix| { - self.render_col_wrap(col_ix, cx).child( - self.render_cell(col_ix, cx) - .child(self.delegate.render_td(row_ix, col_ix, cx)), - ) - })), - ) - } else { - None - }) - .child( - h_flex() - .flex_1() - .h_full() - .overflow_hidden() - .children((left_cols_count..cols_count).map(|col_ix| { - self - // Make the row scroll sync with the - // horizontal_scroll_handle to support horizontal scrolling. - .render_col_wrap(col_ix, cx) - .left(horizontal_scroll_handle.offset().x) - .child( - self.render_cell(col_ix, cx) - .child(self.delegate.render_td(row_ix, col_ix, cx)), - ) - })) - .child(self.delegate.render_last_empty_col(cx)), - ) - // Row selected style - .when_some(self.selected_row, |this, _| { - this.when( - is_selected && self.selection_state == SelectionState::Row, - |this| { - this.border_color(gpui::transparent_white()).child( - div() - .top(if row_ix == 0 { px(0.) } else { px(-1.) }) - .left(px(0.)) - .right(px(0.)) - .bottom_0() - .absolute() - .bg(cx.theme().table_active) - .border_1() - .border_color(cx.theme().table_active_border), - ) - }, - ) - }) - // Row right click row style - .when(self.right_clicked_row == Some(row_ix), |this| { - this.border_color(gpui::transparent_white()).child( - div() - .top(if row_ix == 0 { px(0.) } else { px(-1.) }) - .left(px(0.)) - .right(px(0.)) - .bottom_0() - .absolute() - .border_1() - .border_color(cx.theme().selection), - ) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, cx| { - this.on_row_click(MouseButton::Left, row_ix, cx); - }), - ) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, _, cx| { - this.on_row_click(MouseButton::Right, row_ix, cx); - }), - ) - } else { - // Render fake rows to fill the rest table space - self.delegate - .render_tr(row_ix, cx) - .w_full() - .h_full() - .border_t_1() - .border_color(cx.theme().table_row_border) - .when(is_stripe_row, |this| this.bg(cx.theme().table_even)) - .children((0..cols_count).map(|col_ix| { - h_flex() - .left(horizontal_scroll_handle.offset().x) - .child(self.render_cell(col_ix, cx)) - })) - .child(self.delegate.render_last_empty_col(cx)) - } - } -} - -impl Sizable for Table -where - D: TableDelegate, -{ - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} -impl FocusableView for Table -where - D: TableDelegate, -{ - fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for Table where D: TableDelegate {} - -impl Render for Table -where - D: TableDelegate, -{ - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let view = cx.view().clone(); - let vertical_scroll_handle = self.vertical_scroll_handle.clone(); - let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); - let cols_count: usize = self.delegate.cols_count(cx); - let left_cols_count = self.fixed_cols.left; - let rows_count = self.delegate.rows_count(cx); - - let row_height = self - .vertical_scroll_handle - .0 - .borrow() - .last_item_size - .map(|size| size.item.height); - let total_height = self - .vertical_scroll_handle - .0 - .borrow() - .base_handle - .bounds() - .size - .height; - - // Calculate the extra rows needed to fill the table for stripe style. - let mut extra_rows_needed = 0; - if let Some(row_height) = row_height { - if row_height > px(0.) { - let actual_height = row_height * rows_count as f32; - let remaining_height = total_height - actual_height; - if remaining_height > px(0.) { - extra_rows_needed = (remaining_height / row_height).ceil() as usize; - } - } - } - - let inner_table = v_flex() - .key_context("Table") - .id("table") - .track_focus(&self.focus_handle) - .on_action(cx.listener(Self::action_cancel)) - .on_action(cx.listener(Self::action_select_next)) - .on_action(cx.listener(Self::action_select_prev)) - .on_action(cx.listener(Self::action_select_next_col)) - .on_action(cx.listener(Self::action_select_prev_col)) - .size_full() - .overflow_hidden() - .child(self.render_table_head(left_cols_count, cx)) - .map(|this| { - if rows_count == 0 { - this.child(div().size_full().child(self.delegate.render_empty(cx))) - } else { - this.child( - h_flex().id("table-body").flex_grow().size_full().child( - uniform_list( - view, - "table-uniform-list", - rows_count + extra_rows_needed, - { - move |table, visible_range, cx| { - table.load_more(visible_range.clone(), cx); - - if visible_range.end > rows_count { - table.scroll_to_row( - std::cmp::min(visible_range.start, rows_count - 1), - cx, - ); - } - - // Render fake rows to fill the table - visible_range - .map(|row_ix| { - // Render real rows for available data - table.render_table_row( - row_ix, - rows_count, - left_cols_count, - cols_count, - cx, - ) - }) - .collect::>() - } - }, - ) - .flex_grow() - .size_full() - .with_sizing_behavior(ListSizingBehavior::Auto) - .track_scroll(vertical_scroll_handle) - .into_any_element(), - ), - ) - } - }); - - let view = cx.view().clone(); - div() - .size_full() - .when(self.border, |this| { - this.rounded_md().border_1().border_color(cx.theme().border) - }) - .bg(cx.theme().table) - .child(inner_table) - .child(ScrollableMask::new( - cx.view().clone(), - ScrollableAxis::Horizontal, - &horizontal_scroll_handle, - )) - .child(canvas( - move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds), - |_, _, _| {}, - )) - .child(self.render_horizontal_scrollbar(cx)) - .when(rows_count > 0, |this| { - this.children(self.render_scrollbar(cx)) - }) - // Click out to cancel right clicked row - .when(self.right_clicked_row.is_some(), |this| { - this.on_mouse_down_out(cx.listener(|this, _, cx| { - this.right_clicked_row = None; - cx.notify(); - })) - }) - } -}