diff --git a/Cargo.lock b/Cargo.lock index 7c6d5e2..d81ea63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1302,7 +1302,6 @@ dependencies = [ "gpui_tokio", "indexset", "itertools 0.13.0", - "key_store", "log", "nostr-connect", "nostr-sdk", @@ -3341,22 +3340,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "key_store" -version = "0.3.0" -dependencies = [ - "anyhow", - "common", - "futures", - "gpui", - "log", - "nostr-sdk", - "serde", - "serde_json", - "smallvec", - "smol", -] - [[package]] name = "khronos-egl" version = "6.0.0" diff --git a/assets/icons/chevron-down.svg b/assets/icons/chevron-down.svg new file mode 100644 index 0000000..d8788af --- /dev/null +++ b/assets/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/fistbump-fill.svg b/assets/icons/fistbump-fill.svg new file mode 100644 index 0000000..ad34489 --- /dev/null +++ b/assets/icons/fistbump-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/fistbump.svg b/assets/icons/fistbump.svg new file mode 100644 index 0000000..4a3d0dc --- /dev/null +++ b/assets/icons/fistbump.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/inbox-fill.svg b/assets/icons/inbox-fill.svg new file mode 100644 index 0000000..719c050 --- /dev/null +++ b/assets/icons/inbox-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/inbox.svg b/assets/icons/inbox.svg new file mode 100644 index 0000000..5569c57 --- /dev/null +++ b/assets/icons/inbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 3946225..d4eebf0 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -309,27 +309,21 @@ impl ChatRegistry { .map(|this| this.downgrade()) } - /// Get all rooms. - pub fn rooms(&self, _cx: &App) -> &Vec> { - &self.rooms - } - - /// Get all ongoing rooms. - pub fn ongoing_rooms(&self, cx: &App) -> Vec> { + /// Get all rooms based on the filter. + pub fn rooms(&self, filter: &RoomKind, cx: &App) -> Vec> { self.rooms .iter() - .filter(|room| room.read(cx).kind == RoomKind::Ongoing) + .filter(|room| &room.read(cx).kind == filter) .cloned() .collect() } - /// Get all request rooms. - pub fn request_rooms(&self, cx: &App) -> Vec> { + /// Count the number of rooms based on the filter. + pub fn count(&self, filter: &RoomKind, cx: &App) -> usize { self.rooms .iter() - .filter(|room| room.read(cx).kind != RoomKind::Ongoing) - .cloned() - .collect() + .filter(|room| &room.read(cx).kind == filter) + .count() } /// Add a new room to the start of list. diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index bfa6c11..9cad0a5 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -26,6 +26,3 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; /// Default timeout (in seconds) for Nostr Connect (Bunker) pub const BUNKER_TIMEOUT: u64 = 30; - -/// Default width of the sidebar. -pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.; diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index d110f69..94a62ac 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -35,7 +35,6 @@ theme = { path = "../theme" } common = { path = "../common" } state = { path = "../state" } device = { path = "../device" } -key_store = { path = "../key_store" } chat = { path = "../chat" } chat_ui = { path = "../chat_ui" } settings = { path = "../settings" } diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index a6cbdf8..37c2cb9 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,6 +1,4 @@ -use gpui::{actions, App}; -use key_store::{KeyItem, KeyStore}; -use state::NostrRegistry; +use gpui::actions; // Sidebar actions actions!(sidebar, [Reload, RelayStatus]); @@ -19,30 +17,3 @@ actions!( Quit ] ); - -pub fn reset(cx: &mut App) { - let backend = KeyStore::global(cx).read(cx).backend(); - let client = NostrRegistry::global(cx).read(cx).client(); - - cx.spawn(async move |cx| { - // Remove the signer - client.unset_signer().await; - - // Delete user's credentials - backend - .delete_credentials(&KeyItem::User.to_string(), cx) - .await - .ok(); - - // Remove bunker's credentials if available - backend - .delete_credentials(&KeyItem::Bunker.to_string(), cx) - .await - .ok(); - - cx.update(|cx| { - cx.restart(); - }); - }) - .detach(); -} diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 1a0fd73..139bba6 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -82,9 +82,6 @@ fn main() { // Initialize theme registry theme::init(cx); - // Initialize backend for keys storage - key_store::init(cx); - // Initialize the nostr client state::init(cx); diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index a66598f..1a9798d 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -37,20 +37,13 @@ impl Panel for GreeterPanel { } fn title(&self, cx: &App) -> AnyElement { - h_flex() - .gap_1p5() + div() .child( svg() .path("brand/coop.svg") .size_4() .text_color(cx.theme().text_muted), ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(self.name.clone()), - ) .into_any_element() } } diff --git a/crates/coop/src/sidebar/list_item.rs b/crates/coop/src/sidebar/list_item.rs index 01d94eb..a49f78f 100644 --- a/crates/coop/src/sidebar/list_item.rs +++ b/crates/coop/src/sidebar/list_item.rs @@ -125,14 +125,7 @@ impl RenderOnce for RoomListItem { .flex_shrink_0() .text_xs() .text_color(cx.theme().text_placeholder) - .when_some(self.created_at, |this, created_at| this.child(created_at)) - .when_some(self.kind, |this, kind| { - this.when(kind == RoomKind::Request, |this| { - this.child( - div().size_1().rounded_full().bg(cx.theme().icon_accent), - ) - }) - }), + .when_some(self.created_at, |this, created_at| this.child(created_at)), ), ) .hover(|this| this.bg(cx.theme().elevated_surface_background)) diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 82e1199..487a3ec 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -1,36 +1,23 @@ use std::ops::Range; -use std::time::Duration; -use anyhow::{anyhow, Error}; -use chat::{ChatEvent, ChatRegistry, Room}; -use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use chat::{ChatEvent, ChatRegistry, RoomKind}; +use common::RenderedTimestamp; use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, + deferred, div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, - Subscription, Task, Window, + Subscription, Window, }; -use gpui_tokio::Tokio; use list_item::RoomListItem; -use nostr_sdk::prelude::*; -use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; -use ui::avatar::Avatar; +use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; -use ui::input::{InputEvent, InputState, TextInput}; -use ui::{h_flex, v_flex, IconName, Sizable, StyledExt, WindowExtension}; - -use crate::dialogs::compose::compose_button; +use ui::{h_flex, v_flex, IconName, Selectable, Sizable, StyledExt}; mod list_item; -const FIND_DELAY: u64 = 600; -const FIND_LIMIT: usize = 20; - pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Sidebar::new(window, cx)) } @@ -38,48 +25,25 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { /// Sidebar. pub struct Sidebar { name: SharedString, - - /// Focus handle for the sidebar focus_handle: FocusHandle, /// Image cache image_cache: Entity, - /// Search results - search_results: Entity>>>, + /// Whether there are new chat requests + new_requests: bool, - /// Async search operation - search_task: Option>, - - /// Search input state - find_input: Entity, - - /// Debounced delay for search input - find_debouncer: DebouncedDelay, - - /// Whether searching is in progress - finding: bool, - - /// New request flag - new_request: bool, + /// Chatroom filter + filter: Entity, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 2]>, + _subscriptions: SmallVec<[Subscription; 1]>, } impl Sidebar { fn new(window: &mut Window, cx: &mut Context) -> Self { - let search_results = cx.new(|_| None); - - // Define the find input state - let find_input = cx.new(|cx| { - InputState::new(window, cx) - .placeholder("Find or start a conversation") - .clean_on_escape() - }); - - // Get the chat registry let chat = ChatRegistry::global(cx); + let filter = cx.new(|_| RoomKind::Ongoing); let mut subscriptions = smallvec![]; @@ -87,52 +51,23 @@ impl Sidebar { // Subscribe for registry new events cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| { if event == &ChatEvent::Ping { - this.new_request = true; + this.new_requests = true; cx.notify(); }; }), ); - subscriptions.push( - // Subscribe for find input events - cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { - let delay = Duration::from_millis(FIND_DELAY); - - match event { - InputEvent::PressEnter { .. } => { - this.search(window, cx); - } - InputEvent::Change => { - if state.read(cx).value().is_empty() { - // Clear the result when input is empty - this.clear(window, cx); - } else { - // Run debounced search - this.find_debouncer - .fire_new(delay, window, cx, |this, window, cx| { - this.debounced_search(window, cx) - }); - } - } - _ => {} - }; - }), - ); - Self { name: "Sidebar".into(), focus_handle: cx.focus_handle(), image_cache: RetainAllImageCache::new(cx), - find_debouncer: DebouncedDelay::new(), - finding: false, - new_request: false, - find_input, - search_results, - search_task: None, + new_requests: false, + filter, _subscriptions: subscriptions, } } + /* async fn nip50(client: &Client, query: &str) -> Result, Error> { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; @@ -405,31 +340,30 @@ impl Sidebar { cx.notify(); }); } + */ - fn open(&mut self, id: u64, window: &mut Window, cx: &mut Context) { + /// Get the active filter. + fn current_filter(&self, kind: &RoomKind, cx: &Context) -> bool { + self.filter.read(cx) == kind + } + + /// Set the active filter for the sidebar. + fn set_filter(&mut self, kind: RoomKind, cx: &mut Context) { + self.filter.update(cx, |this, cx| { + *this = kind; + cx.notify(); + }); + self.new_requests = false; + } + + /// Open a room. + fn open(&mut self, id: u64, _window: &mut Window, cx: &mut Context) { let chat = ChatRegistry::global(cx); - match chat.read(cx).room(&id, cx) { - Some(room) => { - chat.update(cx, |this, cx| { - this.emit_room(room, cx); - }); - } - None => { - if let Some(room) = self - .search_results - .read(cx) - .as_ref() - .and_then(|results| results.iter().find(|this| this.read(cx).id == id)) - .map(|this| this.downgrade()) - { - chat.update(cx, |this, cx| { - this.emit_room(room, cx); - }); - // Clear all search results - self.clear(window, cx); - } - } + if let Some(room) = chat.read(cx).room(&id, cx) { + chat.update(cx, |this, cx| { + this.emit_room(room, cx); + }); } } @@ -439,15 +373,8 @@ impl Sidebar { _window: &Window, cx: &Context, ) -> Vec { - // Get rooms from search results first - let rooms = match self.search_results.read(cx).as_ref() { - Some(results) => results, - None => { - // Fallback to chat registry if there are no search results - let chat = ChatRegistry::global(cx); - chat.read(cx).rooms(cx) - } - }; + let chat = ChatRegistry::global(cx); + let rooms = chat.read(cx).rooms(self.filter.read(cx), cx); rooms .get(range.clone()) @@ -456,8 +383,8 @@ impl Sidebar { .enumerate() .map(|(ix, item)| { let room = item.read(cx); - let id = room.id; let public_key = room.display_member(cx).public_key(); + let id = room.id; let handler = cx.listener({ move |this, _ev, window, cx| { @@ -494,73 +421,89 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); - let chat = ChatRegistry::global(cx); let loading = chat.read(cx).loading(); - - // Get total rooms - let total_rooms = match self.search_results.read(cx).as_ref() { - Some(results) => results.len(), - None => chat.read(cx).rooms(cx).len(), - }; + let total_rooms = chat.read(cx).count(self.filter.read(cx), cx); v_flex() .image_cache(self.image_cache.clone()) .size_full() .relative() .gap_2() - .bg(cx.theme().surface_background) - // Titlebar .child( h_flex() - .px_2p5() + .h(TABBAR_HEIGHT) + .w_full() + .border_b_1() + .border_color(cx.theme().border) .child( h_flex() - .w_full() + .flex_1() + .h_full() .gap_2() - .justify_between() - .when_some(identity.read(cx).public_key, |this, public_key| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - - this.child( - h_flex() - .gap_1p5() - .text_xs() - .text_color(cx.theme().text_muted) - .child(Avatar::new(profile.avatar()).size(rems(1.25))) - .child(profile.name()), - ) - }) - .child(compose_button()), + .p_2() + .justify_center() + .child( + Button::new("all") + .map(|this| { + if self.current_filter(&RoomKind::Ongoing, cx) { + this.icon(IconName::InboxFill) + } else { + this.icon(IconName::Inbox) + } + }) + .label("Inbox") + .tooltip("All ongoing conversations") + .xsmall() + .bold() + .ghost() + .flex_1() + .rounded_none() + .selected(self.current_filter(&RoomKind::Ongoing, cx)) + .on_click(cx.listener(|this, _, _, cx| { + this.set_filter(RoomKind::Ongoing, cx); + })), + ) + .child( + Button::new("requests") + .map(|this| { + if self.current_filter(&RoomKind::Request, cx) { + this.icon(IconName::FistbumpFill) + } else { + this.icon(IconName::Fistbump) + } + }) + .label("Requests") + .tooltip("Incoming new conversations") + .xsmall() + .bold() + .ghost() + .flex_1() + .rounded_none() + .selected(!self.current_filter(&RoomKind::Ongoing, cx)) + .when(self.new_requests, |this| { + this.child( + div().size_1().rounded_full().bg(cx.theme().cursor), + ) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.set_filter(RoomKind::default(), cx); + })), + ), ) - .h(TITLEBAR_HEIGHT), - ) - // Search Input - .child( - div().px_2p5().child( - TextInput::new(&self.find_input) - .small() - .cleanable() - .appearance(true) - .text_xs() - .flex_none() - .map(|this| { - if !self.find_input.read(cx).loading { - this.suffix( - Button::new("find") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - } else { - this - } - }), - ), + .child( + h_flex() + .h_full() + .px_2() + .border_l_1() + .border_color(cx.theme().border) + .child( + Button::new("option") + .icon(IconName::Ellipsis) + .small() + .ghost(), + ), + ), ) .when(!loading && total_rooms == 0, |this| { this.child( @@ -590,7 +533,6 @@ impl Render for Sidebar { )), ) }) - // Chat Rooms .child( v_flex() .px_1p5() diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index dc6e71f..949dbeb 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,33 +1,36 @@ use std::sync::Arc; +use std::time::Duration; -use chat::{ChatEvent, ChatRegistry}; -use chat_ui::{CopyPublicKey, OpenPublicKey}; -use common::DEFAULT_SIDEBAR_WIDTH; +use chat::{ChatEvent, ChatRegistry, Room}; +use common::DebouncedDelay; use dock::dock::DockPlacement; use dock::panel::PanelView; use dock::{ClosePanel, DockArea, DockItem}; +use gpui::prelude::FluentBuilder; use gpui::{ - div, px, relative, App, AppContext, Axis, ClipboardItem, Context, Entity, InteractiveElement, - IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, + div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, + ParentElement, Render, SharedString, Styled, Subscription, Task, Window, }; -use nostr_connect::prelude::*; +use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; -use theme::{ActiveTheme, Theme, ThemeRegistry}; +use state::NostrRegistry; +use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use titlebar::TitleBar; +use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::modal::ModalButtonProps; -use ui::{h_flex, v_flex, Root, Sizable, WindowExtension}; +use ui::input::{InputEvent, InputState, TextInput}; +use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension}; -use crate::actions::{reset, KeyringPopup, Logout, Themes}; -use crate::dialogs::profile; use crate::panels::greeter; use crate::sidebar; +const FIND_DELAY: u64 = 600; +const FIND_LIMIT: usize = 20; + pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } -#[derive(Debug)] pub struct Workspace { /// App's Title Bar titlebar: Entity, @@ -35,17 +38,46 @@ pub struct Workspace { /// App's Dock Area dock: Entity, + /// Search results + search_results: Entity>>>, + + /// Async search operation + search_task: Option>, + + /// Search input state + find_input: Entity, + + /// Debounced delay for search input + find_debouncer: DebouncedDelay, + + /// Whether searching is in progress + finding: bool, + /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 3]>, + _subscriptions: SmallVec<[Subscription; 4]>, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); + + // App's titlebar let titlebar = cx.new(|_| TitleBar::new()); + + // App's dock area let dock = cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); + // Define the find input state + let find_input = cx.new(|cx| { + InputState::new(window, cx) + .placeholder("Find or start a conversation") + .clean_on_escape() + }); + + // Define the search results states + let search_results = cx.new(|_| None); + let mut subscriptions = smallvec![]; subscriptions.push( @@ -91,7 +123,7 @@ impl Workspace { subscriptions.push( // Observe the chat registry cx.observe(&chat, move |this, chat, cx| { - let ids = this.get_all_panels(cx); + let ids = this.panel_ids(cx); chat.update(cx, |this, cx| { this.refresh_rooms(ids, cx); @@ -99,6 +131,32 @@ impl Workspace { }), ); + subscriptions.push( + // Subscribe for find input events + cx.subscribe_in(&find_input, window, |this, state, event, window, cx| { + let delay = Duration::from_millis(FIND_DELAY); + + match event { + InputEvent::PressEnter { .. } => { + // this.search(window, cx); + } + InputEvent::Change => { + if state.read(cx).value().is_empty() { + // Clear the result when input is empty + // this.clear(window, cx); + } else { + // Run debounced search + //this.find_debouncer + // .fire_new(delay, window, cx, |this, window, cx| { + // this.debounced_search(window, cx) + // }); + } + } + _ => {} + }; + }), + ); + // Set the default layout for app's dock cx.defer_in(window, |this, window, cx| { this.set_layout(window, cx); @@ -107,6 +165,11 @@ impl Workspace { Self { titlebar, dock, + find_debouncer: DebouncedDelay::new(), + finding: false, + find_input, + search_results, + search_task: None, _subscriptions: subscriptions, } } @@ -126,6 +189,19 @@ impl Workspace { } } + fn panel_ids(&self, cx: &App) -> Option> { + let ids: Vec = self + .dock + .read(cx) + .items + .panel_ids(cx) + .into_iter() + .filter_map(|panel| panel.parse::().ok()) + .collect(); + + Some(ids) + } + fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { let weak_dock = self.dock.downgrade(); @@ -150,123 +226,59 @@ impl Workspace { // Update the dock layout self.dock.update(cx, |this, cx| { - this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx); + this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx); this.set_center(center, window, cx); }); } - fn on_themes(&mut self, _ev: &Themes, window: &mut Window, cx: &mut Context) { - window.open_modal(cx, move |this, _window, cx| { - let registry = ThemeRegistry::global(cx); - let themes = registry.read(cx).themes(); + fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + let nostr = NostrRegistry::global(cx); + let identity = nostr.read(cx).identity(); - this.title("Select theme") - .show_close(true) - .overlay_closable(true) - .child(v_flex().gap_2().pb_4().children({ - let mut items = Vec::with_capacity(themes.len()); + h_flex() + .h(TITLEBAR_HEIGHT) + .flex_1() + .justify_between() + .gap_2() + .when_some(identity.read(cx).public_key, |this, public_key| { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&public_key, cx); - for (name, theme) in themes.iter() { - items.push( - h_flex() - .h_10() - .justify_between() - .child( - v_flex() - .child( - div() - .text_sm() - .text_color(cx.theme().text) - .line_height(relative(1.3)) - .child(theme.name.clone()), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(theme.author.clone()), - ), - ) - .child( - Button::new(format!("change-{name}")) - .label("Set") - .small() - .ghost() - .on_click({ - let theme = theme.clone(); - move |_ev, window, cx| { - Theme::apply_theme(theme.clone(), Some(window), cx); - } - }), - ), - ); - } - - items - })) - }) - } - - fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context) { - reset(cx); - } - - fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context) { - let public_key = ev.0; - let view = profile::init(public_key, window, cx); - - window.open_modal(cx, move |this, _window, _cx| { - this.alert() - .show_close(true) - .overlay_closable(true) - .child(view.clone()) - .button_props(ModalButtonProps::default().ok_text("View on njump.me")) - .on_ok(move |_, _window, cx| { - let bech32 = public_key.to_bech32().unwrap(); - let url = format!("https://njump.me/{bech32}"); - - // Open the URL in the default browser - cx.open_url(&url); - - // false to keep the modal open - false - }) - }); - } - - fn on_copy_pubkey(&mut self, ev: &CopyPublicKey, window: &mut Window, cx: &mut Context) { - let Ok(bech32) = ev.0.to_bech32(); - cx.write_to_clipboard(ClipboardItem::new_string(bech32)); - window.push_notification("Copied", cx); - } - - fn on_keyring(&mut self, _ev: &KeyringPopup, window: &mut Window, cx: &mut Context) { - window.open_modal(cx, move |this, _window, _cx| { - this.show_close(true) - .title(SharedString::from("Keyring is disabled")) - .child( - v_flex() - .gap_2() - .pb_4() - .text_sm() - .child(SharedString::from("Coop cannot access the Keyring Service on your system. By design, Coop uses Keyring to store your credentials.")) - .child(SharedString::from("Without access to Keyring, Coop will store your credentials as plain text.")) - .child(SharedString::from("If you want to store your credentials in the Keyring, please enable Keyring and allow Coop to access it.")), + this.child( + h_flex() + .gap_0p5() + .child(Avatar::new(profile.avatar()).size(rems(1.25))) + .child( + Icon::new(IconName::ChevronDown) + .small() + .text_color(cx.theme().text_muted), + ), ) - }); + }) } - fn get_all_panels(&self, cx: &App) -> Option> { - let ids: Vec = self - .dock - .read(cx) - .items - .panel_ids(cx) - .into_iter() - .filter_map(|panel| panel.parse::().ok()) - .collect(); + fn titlebar_center(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + h_flex().flex_1().child( + TextInput::new(&self.find_input) + .xsmall() + .cleanable() + .appearance(true) + .bordered(false) + .text_xs() + .when(!self.find_input.read(cx).loading, |this| { + this.prefix( + Button::new("find") + .icon(IconName::Search) + .tooltip("Press Enter to search") + .transparent() + .small(), + ) + }), + ) + } - Some(ids) + fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context) -> impl IntoElement { + h_flex().flex_1() } } @@ -275,13 +287,18 @@ impl Render for Workspace { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); + // Titlebar elements + let left = self.titlebar_left(window, cx).into_any_element(); + let center = self.titlebar_center(window, cx).into_any_element(); + let right = self.titlebar_right(window, cx).into_any_element(); + + // Update title bar children + self.titlebar.update(cx, |this, _cx| { + this.set_children(vec![left, center, right]); + }); + div() .id(SharedString::from("workspace")) - .on_action(cx.listener(Self::on_themes)) - .on_action(cx.listener(Self::on_sign_out)) - .on_action(cx.listener(Self::on_open_pubkey)) - .on_action(cx.listener(Self::on_copy_pubkey)) - .on_action(cx.listener(Self::on_keyring)) .relative() .size_full() .child( diff --git a/crates/dock/src/tab/mod.rs b/crates/dock/src/tab/mod.rs index cda8983..3a37ed9 100644 --- a/crates/dock/src/tab/mod.rs +++ b/crates/dock/src/tab/mod.rs @@ -3,7 +3,7 @@ use gpui::{ div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, Window, }; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::{Selectable, Sizable, Size}; pub mod tab_bar; @@ -105,37 +105,37 @@ impl Sizable for Tab { impl RenderOnce for Tab { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let (text_color, bg_color, hover_bg_color, border_color) = + let (text_color, hover_text_color, bg_color, border_color) = match (self.selected, self.disabled) { (true, false) => ( - cx.theme().text, + cx.theme().tab_active_foreground, + cx.theme().tab_hover_foreground, cx.theme().tab_active_background, - cx.theme().tab_hover_background, cx.theme().border, ), (false, false) => ( - cx.theme().text_muted, + cx.theme().tab_inactive_foreground, + cx.theme().tab_hover_foreground, cx.theme().ghost_element_background, - cx.theme().tab_hover_background, cx.theme().border_transparent, ), (true, true) => ( - cx.theme().text_muted, + cx.theme().tab_inactive_foreground, + cx.theme().tab_hover_foreground, cx.theme().ghost_element_background, - cx.theme().tab_hover_background, cx.theme().border_disabled, ), (false, true) => ( - cx.theme().text_muted, + cx.theme().tab_inactive_foreground, + cx.theme().tab_hover_foreground, cx.theme().ghost_element_background, - cx.theme().tab_hover_background, cx.theme().border_disabled, ), }; self.base .id(self.ix) - .h(TITLEBAR_HEIGHT) + .h(TABBAR_HEIGHT) .px_4() .relative() .flex() @@ -151,7 +151,7 @@ impl RenderOnce for Tab { .border_r(px(1.)) .border_color(border_color) .when(!self.selected && !self.disabled, |this| { - this.hover(|this| this.text_color(text_color).bg(hover_bg_color)) + this.hover(|this| this.text_color(hover_text_color)) }) .when_some(self.prefix, |this, prefix| { this.child(prefix).text_color(text_color) diff --git a/crates/dock/src/tab/tab_bar.rs b/crates/dock/src/tab/tab_bar.rs index 24b296a..798093c 100644 --- a/crates/dock/src/tab/tab_bar.rs +++ b/crates/dock/src/tab/tab_bar.rs @@ -91,18 +91,12 @@ impl Sizable for TabBar { } impl RenderOnce for TabBar { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let focused = window.is_window_active(); - + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { self.base .group("tab-bar") .relative() .refine_style(&self.style) - .bg(cx.theme().elevated_surface_background) - .when(!focused, |this| { - // TODO: add specific styles for unfocused tab bar - this.bg(cx.theme().elevated_surface_background) - }) + .bg(cx.theme().surface_background) .child( div() .id("border-bottom") diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 299c9f4..b62e5a0 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -7,7 +7,7 @@ use gpui::{ MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window, }; -use theme::{ActiveTheme, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TABBAR_HEIGHT}; use ui::button::{Button, ButtonVariants as _}; use ui::popup_menu::{PopupMenu, PopupMenuExt}; use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; @@ -645,7 +645,7 @@ impl TabPanel { TabBar::new() .track_scroll(&self.tab_bar_scroll_handle) - .h(TITLEBAR_HEIGHT) + .h(TABBAR_HEIGHT) .when(has_extend_dock_button, |this| { this.prefix( h_flex() @@ -656,7 +656,7 @@ impl TabPanel { .border_b_1() .h_full() .border_color(cx.theme().border) - .bg(cx.theme().elevated_surface_background) + .bg(cx.theme().surface_background) .px_2() .children(left_dock_button) .children(bottom_dock_button), diff --git a/crates/key_store/Cargo.toml b/crates/key_store/Cargo.toml deleted file mode 100644 index 616a7f9..0000000 --- a/crates/key_store/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "key_store" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -common = { path = "../common" } - -gpui.workspace = true -nostr-sdk.workspace = true - -anyhow.workspace = true -smallvec.workspace = true -smol.workspace = true -log.workspace = true -futures.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/key_store/src/backend.rs b/crates/key_store/src/backend.rs deleted file mode 100644 index 6e83dd7..0000000 --- a/crates/key_store/src/backend.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::any::Any; -use std::collections::HashMap; -use std::fmt::Display; -use std::future::Future; -use std::path::PathBuf; -use std::pin::Pin; - -use anyhow::Result; -use common::config_dir; -use futures::FutureExt as _; -use gpui::AsyncApp; -use nostr_sdk::prelude::*; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Credential { - public_key: PublicKey, - secret: String, -} - -impl Credential { - pub fn new(user: String, secret: Vec) -> Self { - Self { - public_key: PublicKey::parse(&user).unwrap(), - secret: String::from_utf8(secret).unwrap(), - } - } - - pub fn public_key(&self) -> PublicKey { - self.public_key - } - - pub fn secret(&self) -> &str { - &self.secret - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum KeyItem { - User, - Bunker, -} - -impl Display for KeyItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::User => write!(f, "coop-user"), - Self::Bunker => write!(f, "coop-bunker"), - } - } -} - -impl From for String { - fn from(item: KeyItem) -> Self { - item.to_string() - } -} - -pub trait KeyBackend: Any + Send + Sync { - fn name(&self) -> &str; - - /// Reads the credentials from the provider. - #[allow(clippy::type_complexity)] - fn read_credentials<'a>( - &'a self, - url: &'a str, - cx: &'a AsyncApp, - ) -> Pin)>>> + 'a>>; - - /// Writes the credentials to the provider. - fn write_credentials<'a>( - &'a self, - url: &'a str, - username: &'a str, - password: &'a [u8], - cx: &'a AsyncApp, - ) -> Pin> + 'a>>; - - /// Deletes the credentials from the provider. - fn delete_credentials<'a>( - &'a self, - url: &'a str, - cx: &'a AsyncApp, - ) -> Pin> + 'a>>; -} - -/// A credentials provider that stores credentials in the system keychain. -pub struct KeyringProvider; - -impl KeyBackend for KeyringProvider { - fn name(&self) -> &str { - "keyring" - } - - fn read_credentials<'a>( - &'a self, - url: &'a str, - cx: &'a AsyncApp, - ) -> Pin)>>> + 'a>> { - async move { cx.update(|cx| cx.read_credentials(url)).await }.boxed_local() - } - - fn write_credentials<'a>( - &'a self, - url: &'a str, - username: &'a str, - password: &'a [u8], - cx: &'a AsyncApp, - ) -> Pin> + 'a>> { - async move { - cx.update(move |cx| cx.write_credentials(url, username, password)) - .await - } - .boxed_local() - } - - fn delete_credentials<'a>( - &'a self, - url: &'a str, - cx: &'a AsyncApp, - ) -> Pin> + 'a>> { - async move { cx.update(move |cx| cx.delete_credentials(url)).await }.boxed_local() - } -} - -/// A credentials provider that stores credentials in a local file. -pub struct FileProvider { - path: PathBuf, -} - -impl FileProvider { - pub fn new() -> Self { - let path = config_dir().join(".keys"); - - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - - Self { path } - } - - pub fn load_credentials(&self) -> Result)>> { - let json = std::fs::read(&self.path)?; - let credentials: HashMap)> = serde_json::from_slice(&json)?; - - Ok(credentials) - } - - pub fn save_credentials(&self, credentials: &HashMap)>) -> Result<()> { - let json = serde_json::to_string(credentials)?; - std::fs::write(&self.path, json)?; - - Ok(()) - } -} - -impl Default for FileProvider { - fn default() -> Self { - Self::new() - } -} - -impl KeyBackend for FileProvider { - fn name(&self) -> &str { - "file" - } - - fn read_credentials<'a>( - &'a self, - url: &'a str, - _cx: &'a AsyncApp, - ) -> Pin)>>> + 'a>> { - async move { - Ok(self - .load_credentials() - .unwrap_or_default() - .get(url) - .cloned()) - } - .boxed_local() - } - - fn write_credentials<'a>( - &'a self, - url: &'a str, - username: &'a str, - password: &'a [u8], - _cx: &'a AsyncApp, - ) -> Pin> + 'a>> { - async move { - let mut credentials = self.load_credentials().unwrap_or_default(); - credentials.insert(url.to_string(), (username.to_string(), password.to_vec())); - - self.save_credentials(&credentials) - } - .boxed_local() - } - - fn delete_credentials<'a>( - &'a self, - url: &'a str, - _cx: &'a AsyncApp, - ) -> Pin> + 'a>> { - async move { - let mut credentials = self.load_credentials()?; - credentials.remove(url); - - self.save_credentials(&credentials) - } - .boxed_local() - } -} diff --git a/crates/key_store/src/lib.rs b/crates/key_store/src/lib.rs deleted file mode 100644 index 4a096a6..0000000 --- a/crates/key_store/src/lib.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::sync::{Arc, LazyLock}; - -pub use backend::*; -use gpui::{App, AppContext, Context, Entity, Global, Task}; -use smallvec::{smallvec, SmallVec}; - -mod backend; - -static DISABLE_KEYRING: LazyLock = - LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty())); - -pub fn init(cx: &mut App) { - KeyStore::set_global(cx.new(KeyStore::new), cx); -} - -struct GlobalKeyStore(Entity); - -impl Global for GlobalKeyStore {} - -pub struct KeyStore { - /// Key Store for storing credentials - pub backend: Arc, - - /// Whether the keystore has been initialized - pub initialized: bool, - - /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl KeyStore { - /// Retrieve the global keys state - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Set the global keys instance - pub(crate) fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalKeyStore(state)); - } - - /// Create a new keys instance - pub(crate) fn new(cx: &mut Context) -> Self { - // Use the file system for keystore in development or when the user specifies it - let use_file_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING; - - // Construct the key backend - let backend: Arc = if use_file_keystore { - Arc::new(FileProvider::default()) - } else { - Arc::new(KeyringProvider) - }; - - // Only used for testing keyring availability on the user's system - let read_credential = cx.read_credentials("Coop"); - let mut tasks = smallvec![]; - - tasks.push( - // Verify the keyring availability - cx.spawn(async move |this, cx| { - let result = read_credential.await; - - this.update(cx, |this, cx| { - if let Err(e) = result { - log::error!("Keyring error: {e}"); - // For Linux: - // The user has not installed secret service on their system - // Fall back to the file provider - this.backend = Arc::new(FileProvider::default()); - } - this.initialized = true; - cx.notify(); - }) - .ok(); - }), - ); - - Self { - backend, - initialized: false, - _tasks: tasks, - } - } - - /// Returns the key backend. - pub fn backend(&self) -> Arc { - Arc::clone(&self.backend) - } - - /// Returns true if the keystore is a file key backend. - pub fn is_using_file_keystore(&self) -> bool { - self.backend.name() == "file" - } -} diff --git a/crates/theme/src/colors.rs b/crates/theme/src/colors.rs index 55a72a0..8cf3e79 100644 --- a/crates/theme/src/colors.rs +++ b/crates/theme/src/colors.rs @@ -75,8 +75,10 @@ pub struct ThemeColors { // Tab colors pub tab_inactive_background: Hsla, - pub tab_hover_background: Hsla, + pub tab_inactive_foreground: Hsla, pub tab_active_background: Hsla, + pub tab_active_foreground: Hsla, + pub tab_hover_foreground: Hsla, // Scrollbar colors pub scrollbar_thumb_background: Hsla, @@ -107,7 +109,7 @@ impl ThemeColors { background: neutral().dark().step_1(), surface_background: neutral().dark().step_2(), elevated_surface_background: neutral().dark().step_3(), - panel_background: neutral().dark().step_1(), + panel_background: neutral().dark().step_3(), overlay: neutral().dark_alpha().step_3(), border: neutral().dark().step_6(), @@ -163,8 +165,10 @@ impl ThemeColors { ghost_element_disabled: neutral().dark_alpha().step_2(), tab_inactive_background: neutral().dark().step_2(), - tab_hover_background: neutral().dark().step_3(), - tab_active_background: neutral().dark().step_1(), + tab_inactive_foreground: neutral().dark().step_11(), + tab_active_background: neutral().dark().step_3(), + tab_active_foreground: neutral().dark().step_12(), + tab_hover_foreground: brand().dark().step_9(), scrollbar_thumb_background: neutral().dark_alpha().step_3(), scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(), @@ -176,8 +180,8 @@ impl ThemeColors { cursor: hsl(200., 100., 50.), selection: hsl(200., 100., 50.).alpha(0.25), - titlebar: neutral().dark().step_2(), - titlebar_inactive: neutral().dark().step_3(), + titlebar: neutral().dark_alpha().step_1(), + titlebar_inactive: neutral().dark_alpha().step_2(), } } } diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index dcc25ba..368a504 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -29,6 +29,15 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); /// Defines window titlebar height pub const TITLEBAR_HEIGHT: Pixels = px(36.0); +/// Defines tabbar height +pub const TABBAR_HEIGHT: Pixels = px(30.); + +/// Defines default sidebar width +pub const SIDEBAR_WIDTH: Pixels = px(240.); + +/// Defines search input width +pub const SEARCH_INPUT_WIDTH: Pixels = px(420.); + pub fn init(cx: &mut App) { registry::init(cx); diff --git a/crates/titlebar/src/lib.rs b/crates/titlebar/src/lib.rs index 5e6c3b6..4a3a583 100644 --- a/crates/titlebar/src/lib.rs +++ b/crates/titlebar/src/lib.rs @@ -1,12 +1,11 @@ -use std::mem; - use gpui::prelude::FluentBuilder; #[cfg(target_os = "linux")] use gpui::MouseButton; +#[cfg(not(target_os = "windows"))] +use gpui::Pixels; use gpui::{ - div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, - ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window, - WindowControlArea, + px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, + ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea, }; use smallvec::{smallvec, SmallVec}; use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING}; @@ -14,19 +13,18 @@ use ui::h_flex; #[cfg(target_os = "linux")] use crate::platforms::linux::LinuxWindowControls; +use crate::platforms::mac::TRAFFIC_LIGHT_PADDING; use crate::platforms::windows::WindowsWindowControls; mod platforms; +/// Titlebar pub struct TitleBar { + /// Children elements of the title bar. children: SmallVec<[AnyElement; 2]>, - should_move: bool, -} -impl Default for TitleBar { - fn default() -> Self { - Self::new() - } + /// Whether the title bar is currently being moved. + should_move: bool, } impl TitleBar { @@ -38,16 +36,16 @@ impl TitleBar { } #[cfg(not(target_os = "windows"))] - pub fn height(window: &mut Window) -> Pixels { + pub fn height(&self, window: &mut Window) -> Pixels { (1.75 * window.rem_size()).max(px(34.)) } #[cfg(target_os = "windows")] - pub fn height(_window: &mut Window) -> Pixels { + pub fn height(&self, _window: &mut Window) -> Pixels { px(32.) } - pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { + pub fn titlebar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { if cfg!(any(target_os = "linux", target_os = "freebsd")) { if window.is_window_active() && !self.should_move { cx.theme().titlebar @@ -67,20 +65,21 @@ impl TitleBar { } } -impl ParentElement for TitleBar { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) +impl Default for TitleBar { + fn default() -> Self { + Self::new() } } impl Render for TitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let height = self.height(window); + let color = self.titlebar_color(window, cx); + let children = std::mem::take(&mut self.children); + #[cfg(target_os = "linux")] let supported_controls = window.window_controls(); let decorations = window.window_decorations(); - let height = Self::height(window); - let color = self.title_bar_color(window, cx); - let children = mem::take(&mut self.children); h_flex() .window_control_area(WindowControlArea::Drag) @@ -90,11 +89,7 @@ impl Render for TitleBar { if window.is_fullscreen() { this.px_2() } else if cx.theme().platform.is_mac() { - this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING)) - .pr_2() - .when(children.len() <= 1, |this| { - this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING)) - }) + this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING)) } else { this.px_2() } @@ -114,11 +109,8 @@ impl Render for TitleBar { .border_color(cx.theme().border) .content_stretch() .child( - div() + h_flex() .id("title-bar") - .flex() - .flex_row() - .items_center() .justify_between() .w_full() .when(cx.theme().platform.is_mac(), |this| { diff --git a/crates/titlebar/src/platforms/linux.rs b/crates/titlebar/src/platforms/linux.rs index c0d2281..1f9731d 100644 --- a/crates/titlebar/src/platforms/linux.rs +++ b/crates/titlebar/src/platforms/linux.rs @@ -25,20 +25,26 @@ impl LinuxWindowControls { impl RenderOnce for LinuxWindowControls { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { + let supported_controls = window.window_controls(); + h_flex() .id("linux-window-controls") .gap_2() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .child(WindowControl::new( - LinuxControl::Minimize, - IconName::WindowMinimize, - )) - .child({ - if window.is_maximized() { - WindowControl::new(LinuxControl::Restore, IconName::WindowRestore) - } else { - WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize) - } + .when(supported_controls.minimize, |this| { + this.child(WindowControl::new( + LinuxControl::Minimize, + IconName::WindowMinimize, + )) + }) + .when(supported_controls.maximize, |this| { + this.child({ + if window.is_maximized() { + WindowControl::new(LinuxControl::Restore, IconName::WindowRestore) + } else { + WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize) + } + }) }) .child( WindowControl::new(LinuxControl::Close, IconName::WindowClose) diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index 4954ab3..6e67c3d 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -12,6 +12,7 @@ pub enum IconName { ArrowLeft, ArrowRight, Boom, + ChevronDown, CaretDown, CaretRight, CaretUp, @@ -27,6 +28,8 @@ pub enum IconName { Eye, Info, Invite, + Inbox, + InboxFill, Link, Loader, Moon, @@ -52,6 +55,8 @@ pub enum IconName { WindowMaximize, WindowMinimize, WindowRestore, + Fistbump, + FistbumpFill, Zoom, } @@ -61,6 +66,7 @@ impl IconName { Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowRight => "icons/arrow-right.svg", Self::Boom => "icons/boom.svg", + Self::ChevronDown => "icons/chevron-down.svg", Self::CaretDown => "icons/caret-down.svg", Self::CaretRight => "icons/caret-right.svg", Self::CaretUp => "icons/caret-up.svg", @@ -76,6 +82,8 @@ impl IconName { Self::Eye => "icons/eye.svg", Self::Info => "icons/info.svg", Self::Invite => "icons/invite.svg", + Self::Inbox => "icons/inbox.svg", + Self::InboxFill => "icons/inbox-fill.svg", Self::Link => "icons/link.svg", Self::Loader => "icons/loader.svg", Self::Moon => "icons/moon.svg", @@ -101,6 +109,8 @@ impl IconName { Self::WindowMaximize => "icons/window-maximize.svg", Self::WindowMinimize => "icons/window-minimize.svg", Self::WindowRestore => "icons/window-restore.svg", + Self::Fistbump => "icons/fistbump.svg", + Self::FistbumpFill => "icons/fistbump-fill.svg", Self::Zoom => "icons/zoom.svg", } .into() diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs index 9f5d702..d38d031 100644 --- a/crates/ui/src/input/text_input.rs +++ b/crates/ui/src/input/text_input.rs @@ -145,6 +145,7 @@ impl Styled for TextInput { impl RenderOnce for TextInput { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { const LINE_HEIGHT: Rems = Rems(1.25); + let font = window.text_style().font(); let font_size = window.text_style().font_size.to_pixels(window.rem_size()); @@ -155,6 +156,7 @@ impl RenderOnce for TextInput { }); let state = self.state.read(cx); + let focused = state.focus_handle.is_focused(window) && !state.disabled; let gap_x = match self.size { Size::Small => px(4.), @@ -266,7 +268,16 @@ impl RenderOnce for TextInput { .when_some(self.height, |this, height| this.h(height)) }) .when(self.appearance, |this| { - this.bg(bg).rounded(cx.theme().radius) + this.bg(bg) + .rounded(cx.theme().radius) + .when(self.bordered, |this| { + this.border_color(cx.theme().border) + .border_1() + .when(cx.theme().shadow, |this| this.shadow_xs()) + .when(focused && self.focus_bordered, |this| { + this.border_color(cx.theme().border_focused) + }) + }) }) .items_center() .gap(gap_x) diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index f24ff3f..476afce 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -2,10 +2,10 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - canvas, div, point, px, rgba, AnyView, App, AppContext, Bounds, Context, CursorStyle, + canvas, div, point, px, size, AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, Edges, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, - WeakFocusHandle, Window, + Tiling, WeakFocusHandle, Window, }; use theme::{ ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING, @@ -241,7 +241,10 @@ impl Render for Root { window.set_rem_size(rem_size); // Set the client inset (linux only) - window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW); + match decorations { + Decorations::Client { .. } => window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW), + Decorations::Server => window.set_client_inset(px(0.0)), + } div() .id("window") @@ -267,7 +270,7 @@ impl Render for Root { let size = window.window_bounds().get_bounds().size; let Some(edge) = - resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size) + resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size, tiling) else { return; }; @@ -314,7 +317,9 @@ impl Render for Root { let size = window.window_bounds().get_bounds().size; let pos = e.position; - if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { + if let Some(edge) = + resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size, tiling) + { window.start_window_resize(edge) }; }), @@ -324,9 +329,7 @@ impl Render for Root { .map(|div| match decorations { Decorations::Server => div, Decorations::Client { tiling } => div - .when(!cx.theme().platform.is_mac(), |div| { - div.border_color(rgba(0x64748b33)) - }) + .border_color(cx.theme().border) .when(!(tiling.top || tiling.right), |div| { div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) }) @@ -401,25 +404,59 @@ pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges { } /// Get the window resize edge. -fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { - let edge = if pos.y < shadow_size && pos.x < shadow_size { - ResizeEdge::TopLeft - } else if pos.y < shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::TopRight - } else if pos.y < shadow_size { - ResizeEdge::Top - } else if pos.y > size.height - shadow_size && pos.x < shadow_size { - ResizeEdge::BottomLeft - } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::BottomRight - } else if pos.y > size.height - shadow_size { - ResizeEdge::Bottom - } else if pos.x < shadow_size { - ResizeEdge::Left - } else if pos.x > size.width - shadow_size { - ResizeEdge::Right - } else { +fn resize_edge( + pos: Point, + shadow_size: Pixels, + window_size: Size, + tiling: Tiling, +) -> Option { + let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5); + if bounds.contains(&pos) { return None; - }; - Some(edge) + } + + let corner_size = size(shadow_size * 1.5, shadow_size * 1.5); + let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size); + if !tiling.top && top_left_bounds.contains(&pos) { + return Some(ResizeEdge::TopLeft); + } + + let top_right_bounds = Bounds::new( + Point::new(window_size.width - corner_size.width, px(0.)), + corner_size, + ); + if !tiling.top && top_right_bounds.contains(&pos) { + return Some(ResizeEdge::TopRight); + } + + let bottom_left_bounds = Bounds::new( + Point::new(px(0.), window_size.height - corner_size.height), + corner_size, + ); + if !tiling.bottom && bottom_left_bounds.contains(&pos) { + return Some(ResizeEdge::BottomLeft); + } + + let bottom_right_bounds = Bounds::new( + Point::new( + window_size.width - corner_size.width, + window_size.height - corner_size.height, + ), + corner_size, + ); + if !tiling.bottom && bottom_right_bounds.contains(&pos) { + return Some(ResizeEdge::BottomRight); + } + + if !tiling.top && pos.y < shadow_size { + Some(ResizeEdge::Top) + } else if !tiling.bottom && pos.y > window_size.height - shadow_size { + Some(ResizeEdge::Bottom) + } else if !tiling.left && pos.x < shadow_size { + Some(ResizeEdge::Left) + } else if !tiling.right && pos.x > window_size.width - shadow_size { + Some(ResizeEdge::Right) + } else { + None + } }