From 8e62f30c05f1df716779522a68ace85e69defd96 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 28 Jan 2026 13:40:55 +0700 Subject: [PATCH] add relay panel --- crates/coop/src/panels/greeter.rs | 84 +++++- crates/coop/src/panels/mod.rs | 1 + crates/coop/src/panels/profile.rs | 2 +- crates/coop/src/panels/relay_list.rs | 366 +++++++++++++++++++++++++++ crates/coop/src/sidebar/mod.rs | 18 +- crates/dock/src/lib.rs | 1 - crates/dock/src/platforms/linux.rs | 198 --------------- crates/dock/src/platforms/mod.rs | 3 - crates/dock/src/platforms/windows.rs | 147 ----------- crates/dock/src/stack_panel.rs | 3 +- crates/dock/src/tab_panel.rs | 7 +- 11 files changed, 452 insertions(+), 378 deletions(-) create mode 100644 crates/coop/src/panels/relay_list.rs delete mode 100644 crates/dock/src/platforms/linux.rs delete mode 100644 crates/dock/src/platforms/mod.rs delete mode 100644 crates/dock/src/platforms/windows.rs diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 230f569..ff8e889 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -5,12 +5,12 @@ use gpui::{ div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, }; -use state::NostrRegistry; +use state::{NostrRegistry, RelayState}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; -use crate::panels::{connect, import, profile}; +use crate::panels::{connect, import, profile, relay_list}; use crate::workspace::Workspace; pub fn init(window: &mut Window, cx: &mut App) -> Entity { @@ -71,6 +71,11 @@ impl Render for GreeterPanel { let nostr = NostrRegistry::global(cx); let identity = nostr.read(cx).identity(); + let relay_list_state = identity.read(cx).relay_list_state(); + let messaging_relay_state = identity.read(cx).messaging_relays_state(); + let required_actions = + relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet; + h_flex() .size_full() .items_center() @@ -78,15 +83,16 @@ impl Render for GreeterPanel { .p_2() .child( v_flex() - .gap_3() .h_full() + .w_112() + .gap_6() .items_center() .justify_center() .child( h_flex() - .mb_7() + .mb_4() .gap_2() - .w_96() + .w_full() .child( svg() .path("brand/coop.svg") @@ -110,11 +116,74 @@ impl Render for GreeterPanel { ), ), ) + .when(required_actions, |this| { + this.child( + v_flex() + .gap_2() + .w_full() + .items_start() + .child( + h_flex() + .gap_1() + .w_full() + .text_sm() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Required Actions")) + .child(div().flex_1().h_px().bg(cx.theme().border)), + ) + .child( + v_flex() + .w_full() + .items_start() + .justify_start() + .gap_2() + .when(relay_list_state == RelayState::NotSet, |this| { + this.child( + Button::new("connect") + .icon(Icon::new(IconName::Door)) + .label("Set up relay list") + .ghost() + .small() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + relay_list::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ) + }) + .when( + messaging_relay_state == RelayState::NotSet, + |this| { + this.child( + Button::new("import") + .icon(Icon::new(IconName::Usb)) + .label("Set up messaging relays") + .ghost() + .small() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + import::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ) + }, + ), + ), + ) + }) .when(!identity.read(cx).owned, |this| { this.child( v_flex() .gap_2() - .w_96() + .w_full() + .items_start() .child( h_flex() .gap_1() @@ -167,7 +236,8 @@ impl Render for GreeterPanel { .child( v_flex() .gap_2() - .w_96() + .w_full() + .items_start() .child( h_flex() .gap_1() diff --git a/crates/coop/src/panels/mod.rs b/crates/coop/src/panels/mod.rs index ab20f31..d88d325 100644 --- a/crates/coop/src/panels/mod.rs +++ b/crates/coop/src/panels/mod.rs @@ -2,3 +2,4 @@ pub mod connect; pub mod greeter; pub mod import; pub mod profile; +pub mod relay_list; diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index 04cf01a..8d48095 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -401,7 +401,7 @@ impl Render for ProfilePanel { .child(divider(cx)) .child( Button::new("submit") - .label("Continue") + .label("Update") .primary() .disabled(self.uploading) .on_click(cx.listener(move |this, _ev, window, cx| { diff --git a/crates/coop/src/panels/relay_list.rs b/crates/coop/src/panels/relay_list.rs new file mode 100644 index 0000000..111dca8 --- /dev/null +++ b/crates/coop/src/panels/relay_list.rs @@ -0,0 +1,366 @@ +use std::collections::HashSet; +use std::time::Duration; + +use anyhow::{anyhow, Error}; +use common::BOOTSTRAP_RELAYS; +use dock::panel::{Panel, PanelEvent}; +use gpui::prelude::FluentBuilder; +use gpui::{ + div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, + FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + Styled, Subscription, Task, TextAlign, UniformList, Window, +}; +use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; +use state::NostrRegistry; +use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants}; +use ui::input::{InputEvent, InputState, TextInput}; +use ui::{divider, h_flex, v_flex, IconName, Sizable, StyledExt}; + +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| RelayListPanel::new(window, cx)) +} + +#[derive(Debug)] +pub struct RelayListPanel { + name: SharedString, + focus_handle: FocusHandle, + + /// Relay URL input + input: Entity, + + /// Relay metadata input + metadata: Entity>, + + /// Error message + error: Option, + + // All relays + relays: HashSet<(RelayUrl, Option)>, + + // Event subscriptions + _subscriptions: SmallVec<[Subscription; 1]>, + + // Background tasks + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl RelayListPanel { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com")); + let metadata = cx.new(|_| None); + + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + let mut subscriptions = smallvec![]; + let mut tasks = smallvec![]; + + tasks.push( + // Load user's relays in the local database + cx.spawn_in(window, async move |this, cx| { + let result = cx + .background_spawn(async move { Self::load(&client).await }) + .await; + + if let Ok(relays) = result { + this.update(cx, |this, cx| { + this.relays.extend(relays); + cx.notify(); + }) + .ok(); + } + }), + ); + + subscriptions.push( + // Subscribe to user's input events + cx.subscribe_in(&input, window, move |this, _input, event, window, cx| { + if let InputEvent::PressEnter { .. } = event { + this.add(window, cx); + } + }), + ); + + Self { + name: "Update Relay List".into(), + focus_handle: cx.focus_handle(), + input, + metadata, + relays: HashSet::new(), + error: None, + _subscriptions: subscriptions, + _tasks: tasks, + } + } + + async fn load(client: &Client) -> Result)>, Error> { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + Ok(nip65::extract_owned_relay_list(event).collect()) + } else { + Err(anyhow!("Not found.")) + } + } + + fn add(&mut self, window: &mut Window, cx: &mut Context) { + let value = self.input.read(cx).value().to_string(); + let metadata = self.metadata.read(cx); + + if !value.starts_with("ws") { + self.set_error("Relay URl is invalid", window, cx); + return; + } + + if let Ok(url) = RelayUrl::parse(&value) { + if !self.relays.insert((url, metadata.to_owned())) { + self.input.update(cx, |this, cx| { + this.set_value("", window, cx); + }); + cx.notify(); + } + } else { + self.set_error("Relay URl is invalid", window, cx); + } + } + + fn remove(&mut self, url: &RelayUrl, cx: &mut Context) { + self.relays.retain(|(relay, _)| relay != url); + cx.notify(); + } + + fn set_error(&mut self, error: E, window: &mut Window, cx: &mut Context) + where + E: Into, + { + self.error = Some(error.into()); + cx.notify(); + + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + // Clear the error message after a delay + this.update(cx, |this, cx| { + this.error = None; + cx.notify(); + }) + .ok(); + }) + .detach(); + } + + pub fn set_relays(&mut self, window: &mut Window, cx: &mut Context) { + if self.relays.is_empty() { + self.set_error("You need to add at least 1 relay", window, cx); + return; + }; + + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let relays = self.relays.clone(); + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let event = EventBuilder::relay_list(relays).sign(&signer).await?; + + // Set relay list for current user + client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; + + Ok(()) + }); + + cx.spawn_in(window, async move |this, cx| { + match task.await { + Ok(_) => { + // TODO + } + Err(e) => { + this.update_in(cx, |this, window, cx| { + this.set_error(e.to_string(), window, cx); + }) + .ok(); + } + }; + }) + .detach(); + } + + fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> UniformList { + let relays = self.relays.clone(); + let total = relays.len(); + + uniform_list( + "relays", + total, + cx.processor(move |_v, range, _window, cx| { + let mut items = Vec::new(); + + for ix in range { + let Some((url, metadata)) = relays.iter().nth(ix) else { + continue; + }; + + items.push( + div() + .id(SharedString::from(url.to_string())) + .group("") + .w_full() + .h_9() + .py_0p5() + .child( + h_flex() + .px_2() + .flex() + .justify_between() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child( + div().text_sm().child(SharedString::from(url.to_string())), + ) + .child( + h_flex() + .gap_1() + .text_xs() + .map(|this| { + if let Some(metadata) = metadata { + this.child(SharedString::from( + metadata.to_string(), + )) + } else { + this.child(SharedString::from("Read+Write")) + } + }) + .child( + Button::new("remove_{ix}") + .icon(IconName::Close) + .xsmall() + .ghost() + .invisible() + .group_hover("", |this| this.visible()) + .on_click({ + let url = url.to_owned(); + cx.listener( + move |this, _ev, _window, cx| { + this.remove(&url, cx); + }, + ) + }), + ), + ), + ), + ) + } + + items + }), + ) + .h_full() + } + + fn render_empty(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + h_flex() + .mt_2() + .h_20() + .justify_center() + .border_2() + .border_dashed() + .border_color(cx.theme().border) + .rounded(cx.theme().radius_lg) + .text_sm() + .text_align(TextAlign::Center) + .child(SharedString::from("Please add some relays.")) + } +} + +impl Panel for RelayListPanel { + fn panel_id(&self) -> SharedString { + self.name.clone() + } + + fn title(&self, _cx: &App) -> AnyElement { + self.name.clone().into_any_element() + } +} + +impl EventEmitter for RelayListPanel {} + +impl Focusable for RelayListPanel { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for RelayListPanel { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .items_center() + .justify_center() + .p_2() + .gap_10() + .child( + div() + .text_center() + .font_semibold() + .line_height(relative(1.25)) + .child(SharedString::from("Update Relay List")), + ) + .child( + v_flex() + .w_112() + .gap_2() + .text_sm() + .child( + v_flex() + .gap_1p5() + .child( + h_flex() + .gap_1() + .w_full() + .child(TextInput::new(&self.input).small()) + .child( + Button::new("add") + .icon(IconName::Plus) + .label("Add") + .ghost() + .on_click(cx.listener(move |this, _, window, cx| { + this.add(window, cx); + })), + ), + ) + .when_some(self.error.as_ref(), |this, error| { + this.child( + div() + .italic() + .text_xs() + .text_color(cx.theme().danger_foreground) + .child(error.clone()), + ) + }), + ) + .map(|this| { + if !self.relays.is_empty() { + this.child(self.render_list(window, cx)) + } else { + this.child(self.render_empty(window, cx)) + } + }) + .child(divider(cx)) + .child( + Button::new("submit") + .label("Update") + .primary() + .on_click(cx.listener(move |this, _ev, window, cx| { + this.set_relays(window, cx); + })), + ), + ) + } +} diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index bf9a142..86b7081 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -7,9 +7,9 @@ use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEA use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, rems, uniform_list, App, AppContext, Context, Decorations, Entity, EventEmitter, - FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, - RetainAllImageCache, SharedString, Styled, Subscription, Task, Window, + deferred, div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, + SharedString, Styled, Subscription, Task, Window, }; use gpui_tokio::Tokio; use list_item::RoomListItem; @@ -17,7 +17,7 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION}; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TITLEBAR_HEIGHT}; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; @@ -578,9 +578,7 @@ impl Focusable for Sidebar { } impl Render for Sidebar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let decorations = window.window_decorations(); - + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let identity = nostr.read(cx).identity(); @@ -601,12 +599,6 @@ impl Render for Sidebar { .relative() .gap_2() .bg(cx.theme().surface_background) - .map(|this| match decorations { - Decorations::Server => this, - Decorations::Client { .. } => this - .rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - .rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING), - }) // Titlebar .child( h_flex() diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 198c221..972fb27 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -15,7 +15,6 @@ use crate::tab_panel::TabPanel; pub mod dock; pub mod panel; -mod platforms; pub mod resizable; pub mod stack_panel; pub mod tab; diff --git a/crates/dock/src/platforms/linux.rs b/crates/dock/src/platforms/linux.rs deleted file mode 100644 index 77084e4..0000000 --- a/crates/dock/src/platforms/linux.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::OnceLock; - -use gpui::prelude::FluentBuilder; -use gpui::{ - svg, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, - StatefulInteractiveElement, Styled, Window, -}; -use linicon::{lookup_icon, IconType}; -use theme::ActiveTheme; -use ui::{h_flex, Icon, IconName, Sizable}; - -#[derive(IntoElement)] -pub struct LinuxWindowControls {} - -impl LinuxWindowControls { - pub fn new() -> Self { - Self {} - } -} - -impl RenderOnce for LinuxWindowControls { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - 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) - } - }) - .child(WindowControl::new( - LinuxControl::Close, - IconName::WindowClose, - )) - } -} - -#[derive(IntoElement)] -pub struct WindowControl { - kind: LinuxControl, - fallback: IconName, -} - -impl WindowControl { - pub fn new(kind: LinuxControl, fallback: IconName) -> Self { - Self { kind, fallback } - } - - #[allow(dead_code)] - pub fn is_gnome(&self) -> bool { - matches!(detect_desktop_environment(), DesktopEnvironment::Gnome) - } -} - -impl RenderOnce for WindowControl { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.kind.as_icon_name()) - .group("") - .justify_center() - .items_center() - .rounded_full() - .size_6() - .map(|this| { - if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { - this.child( - svg() - .external_path(path.into_os_string().into_string().unwrap()) - .text_color(cx.theme().text) - .size_4(), - ) - } else { - this.child(Icon::new(self.fallback).flex_grow().small()) - } - }) - .on_mouse_move(|_ev, _window, cx| cx.stop_propagation()) - .on_click(move |_ev, window, cx| { - cx.stop_propagation(); - match self.kind { - LinuxControl::Minimize => window.minimize_window(), - LinuxControl::Restore => window.zoom_window(), - LinuxControl::Maximize => window.zoom_window(), - LinuxControl::Close => cx.quit(), - } - }) - } -} - -static DE: OnceLock = OnceLock::new(); -static LINUX_CONTROLS: OnceLock>> = OnceLock::new(); - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum DesktopEnvironment { - Gnome, - Kde, - Unknown, -} - -/// Detect the current desktop environment -pub fn detect_desktop_environment() -> &'static DesktopEnvironment { - DE.get_or_init(|| { - // Try to use environment variables first - if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") { - let desktop = output.to_lowercase(); - if desktop.contains("gnome") { - return DesktopEnvironment::Gnome; - } else if desktop.contains("kde") { - return DesktopEnvironment::Kde; - } - } - - // Fallback detection methods - if let Ok(output) = std::env::var("DESKTOP_SESSION") { - let session = output.to_lowercase(); - if session.contains("gnome") { - return DesktopEnvironment::Gnome; - } else if session.contains("kde") || session.contains("plasma") { - return DesktopEnvironment::Kde; - } - } - - DesktopEnvironment::Unknown - }) -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum LinuxControl { - Minimize, - Restore, - Maximize, - Close, -} - -impl LinuxControl { - pub fn as_icon_name(&self) -> &'static str { - match self { - LinuxControl::Close => "window-close", - LinuxControl::Minimize => "window-minimize", - LinuxControl::Maximize => "window-maximize", - LinuxControl::Restore => "window-restore", - } - } -} - -fn linux_controls() -> &'static HashMap> { - LINUX_CONTROLS.get_or_init(|| { - let mut icons = HashMap::new(); - icons.insert(LinuxControl::Close, None); - icons.insert(LinuxControl::Minimize, None); - icons.insert(LinuxControl::Maximize, None); - icons.insert(LinuxControl::Restore, None); - - let icon_names = [ - (LinuxControl::Close, vec!["window-close", "dialog-close"]), - ( - LinuxControl::Minimize, - vec!["window-minimize", "window-lower"], - ), - ( - LinuxControl::Maximize, - vec!["window-maximize", "window-expand"], - ), - ( - LinuxControl::Restore, - vec!["window-restore", "window-return"], - ), - ]; - - for (control, icon_names) in icon_names { - for icon_name in icon_names { - // Try GNOME-style naming first - let mut control_icon = lookup_icon(format!("{icon_name}-symbolic")) - .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); - - // If not found, try KDE-style naming - if control_icon.is_none() { - control_icon = lookup_icon(icon_name) - .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); - } - - if let Some(Ok(icon)) = control_icon { - icons.entry(control).and_modify(|v| *v = Some(icon.path)); - } - } - } - - icons - }) -} diff --git a/crates/dock/src/platforms/mod.rs b/crates/dock/src/platforms/mod.rs deleted file mode 100644 index f880e5d..0000000 --- a/crates/dock/src/platforms/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[cfg(target_os = "linux")] -pub mod linux; -pub mod windows; diff --git a/crates/dock/src/platforms/windows.rs b/crates/dock/src/platforms/windows.rs deleted file mode 100644 index f28180b..0000000 --- a/crates/dock/src/platforms/windows.rs +++ /dev/null @@ -1,147 +0,0 @@ -use gpui::prelude::FluentBuilder; -use gpui::{ - div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, - RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea, -}; -use theme::ActiveTheme; -use ui::h_flex; - -#[derive(IntoElement)] -pub struct WindowsWindowControls { - button_height: Pixels, -} - -impl WindowsWindowControls { - pub fn new(button_height: Pixels) -> Self { - Self { button_height } - } - - #[cfg(not(target_os = "windows"))] - fn get_font() -> &'static str { - "Segoe Fluent Icons" - } - - #[cfg(target_os = "windows")] - fn get_font() -> &'static str { - use windows::Wdk::System::SystemServices::RtlGetVersion; - - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { RtlGetVersion(&mut version) }; - - if status.is_ok() && version.dwBuildNumber >= 22000 { - "Segoe Fluent Icons" - } else { - "Segoe MDL2 Assets" - } - } -} - -impl RenderOnce for WindowsWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let close_button_hover_color = Rgba { - r: 232.0 / 255.0, - g: 17.0 / 255.0, - b: 32.0 / 255.0, - a: 1.0, - }; - - let button_hover_color = cx.theme().ghost_element_hover; - let button_active_color = cx.theme().ghost_element_active; - - div() - .id("windows-window-controls") - .font_family(Self::get_font()) - .flex() - .flex_row() - .justify_center() - .content_stretch() - .max_h(self.button_height) - .min_h(self.button_height) - .child(WindowsCaptionButton::new( - "minimize", - WindowsCaptionButtonIcon::Minimize, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "maximize-or-restore", - if window.is_maximized() { - WindowsCaptionButtonIcon::Restore - } else { - WindowsCaptionButtonIcon::Maximize - }, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "close", - WindowsCaptionButtonIcon::Close, - close_button_hover_color, - button_active_color, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum WindowsCaptionButtonIcon { - Minimize, - Restore, - Maximize, - Close, -} - -#[derive(IntoElement)] -struct WindowsCaptionButton { - id: ElementId, - icon: WindowsCaptionButtonIcon, - hover_background_color: Hsla, - active_background_color: Hsla, -} - -impl WindowsCaptionButton { - pub fn new( - id: impl Into, - icon: WindowsCaptionButtonIcon, - hover_background_color: impl Into, - active_background_color: impl Into, - ) -> Self { - Self { - id: id.into(), - icon, - hover_background_color: hover_background_color.into(), - active_background_color: active_background_color.into(), - } - } -} - -impl RenderOnce for WindowsCaptionButton { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .justify_center() - .content_center() - .occlude() - .w(px(36.)) - .h_full() - .text_size(px(10.0)) - .hover(|style| style.bg(self.hover_background_color)) - .active(|style| style.bg(self.active_background_color)) - .map(|this| match self.icon { - WindowsCaptionButtonIcon::Close => { - this.window_control_area(WindowControlArea::Close) - } - WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => { - this.window_control_area(WindowControlArea::Max) - } - WindowsCaptionButtonIcon::Minimize => { - this.window_control_area(WindowControlArea::Min) - } - }) - .child(match self.icon { - WindowsCaptionButtonIcon::Minimize => "\u{e921}", - WindowsCaptionButtonIcon::Restore => "\u{e923}", - WindowsCaptionButtonIcon::Maximize => "\u{e922}", - WindowsCaptionButtonIcon::Close => "\u{e8bb}", - }) - } -} diff --git a/crates/dock/src/stack_panel.rs b/crates/dock/src/stack_panel.rs index 62736a5..fe03f29 100644 --- a/crates/dock/src/stack_panel.rs +++ b/crates/dock/src/stack_panel.rs @@ -6,7 +6,7 @@ use gpui::{ Window, }; use smallvec::SmallVec; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; +use theme::ActiveTheme; use ui::{h_flex, AxisExt as _, Placement}; use super::{DockArea, PanelEvent}; @@ -386,7 +386,6 @@ impl Render for StackPanel { h_flex() .size_full() .overflow_hidden() - .rounded(CLIENT_SIDE_DECORATION_ROUNDING) .bg(cx.theme().elevated_surface_background) .child( ResizablePanelGroup::new("stack-panel-group") diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 21dc572..299c9f4 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, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, TITLEBAR_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}; @@ -594,7 +594,6 @@ impl TabPanel { .py_2() .pl_3() .pr_2() - .rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) .when(left_dock_button.is_some(), |this| this.pl_2()) .when(right_dock_button.is_some(), |this| this.pr_2()) .when(has_extend_dock_button, |this| { @@ -647,10 +646,6 @@ impl TabPanel { TabBar::new() .track_scroll(&self.tab_bar_scroll_handle) .h(TITLEBAR_HEIGHT) - .rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - .when(self.zoomed, |this| { - this.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) .when(has_extend_dock_button, |this| { this.prefix( h_flex()