diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 7b5b90c..9bbd428 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -30,8 +30,8 @@ use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; use ui::{ - h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, - StyledExt, + h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, + WindowExtension, }; use crate::emoji::EmojiPicker; diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index e290321..6f85925 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -25,7 +25,7 @@ use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; +use ui::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; use crate::actions::{ reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index 324b167..b93ad3c 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -16,7 +16,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; -use ui::{v_flex, ContextModal, Disableable, StyledExt}; +use ui::{v_flex, Disableable, StyledExt, WindowExtension}; use crate::actions::CoopAuthUrlHandler; diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index a1fb4de..3777c3a 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -4,7 +4,7 @@ use assets::Assets; use common::{APP_ID, CLIENT_NAME}; use gpui::{ point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, - TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, + Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use ui::Root; @@ -58,6 +58,7 @@ fn main() { window_background: WindowBackgroundAppearance::Opaque, window_decorations: Some(WindowDecorations::Client), window_bounds: Some(WindowBounds::Windowed(bounds)), + window_min_size: Some(Size::new(px(640.), px(480.))), kind: WindowKind::Normal, app_id: Some(APP_ID.to_owned()), titlebar: Some(TitlebarOptions { diff --git a/crates/coop/src/new_identity/mod.rs b/crates/coop/src/new_identity/mod.rs index 8a1c3aa..89d7611 100644 --- a/crates/coop/src/new_identity/mod.rs +++ b/crates/coop/src/new_identity/mod.rs @@ -15,7 +15,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; -use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable}; +use ui::{divider, v_flex, Disableable, IconName, Sizable, WindowExtension}; mod backup; diff --git a/crates/coop/src/sidebar/list_item.rs b/crates/coop/src/sidebar/list_item.rs index a018f38..b68369a 100644 --- a/crates/coop/src/sidebar/list_item.rs +++ b/crates/coop/src/sidebar/list_item.rs @@ -14,7 +14,7 @@ use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; use ui::skeleton::Skeleton; -use ui::{h_flex, ContextModal, StyledExt}; +use ui::{h_flex, StyledExt, WindowExtension}; use crate::views::screening; diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index d8f4ef4..a64842c 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -20,7 +20,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Selectable, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; use crate::actions::{RelayStatus, Reload}; diff --git a/crates/coop/src/user/mod.rs b/crates/coop/src/user/mod.rs index 8bdabc2..54b1ea1 100644 --- a/crates/coop/src/user/mod.rs +++ b/crates/coop/src/user/mod.rs @@ -18,7 +18,7 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputState, TextInput}; -use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; pub mod viewer; diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index f38b00d..4fcec5a 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -21,7 +21,7 @@ use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::notification::Notification; -use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension}; pub fn compose_button() -> impl IntoElement { div().child( diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index d7d9c8f..ba29e34 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -16,7 +16,7 @@ use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::notification::Notification; -use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; +use ui::{divider, h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; use crate::chatspace::{self}; diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index 8ab5443..f61a949 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -15,7 +15,7 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Screening::new(public_key, window, cx)) diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index a79b969..e237b80 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -14,7 +14,7 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; -use ui::{h_flex, v_flex, ContextModal, IconName, Sizable}; +use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| SetupRelay::new(window, cx)) diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index a79aa1c..4a77d82 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -18,7 +18,7 @@ use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension}; use crate::actions::{reset, CoopAuthUrlHandler}; diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index b753261..99aa2f1 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -16,7 +16,7 @@ use state::{tracker, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::notification::Notification; -use ui::{v_flex, ContextModal, Disableable, IconName, Sizable}; +use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension}; const AUTH_MESSAGE: &str = "Approve the authentication request to allow Coop to continue sending or receiving events."; @@ -243,7 +243,7 @@ impl RelayAuth { Ok(_) => { this.update_in(cx, |this, window, cx| { // Clear the current notification - window.clear_notification_by_id(SharedString::from(&challenge), cx); + window.clear_notification(challenge, cx); // Push a new notification window.push_notification(format!("{url} has been authenticated"), cx); diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 14a06c3..66b65b9 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -21,6 +21,9 @@ pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); /// Defines window shadow size for platforms that use client side decorations. pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); +/// Defines window border size for platforms that use client side decorations. +pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); + pub fn init(cx: &mut App) { registry::init(cx); diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 5cbbe7e..5875b15 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -3,9 +3,9 @@ pub use focusable::FocusableCycle; pub use icon::*; pub use kbd::*; pub use menu::{context_menu, popup_menu}; -pub use root::{ContextModal, Root}; +pub use root::Root; pub use styled::*; -pub use window_border::{window_border, WindowBorder}; +pub use window_ext::*; pub use crate::Disableable; @@ -38,7 +38,7 @@ mod icon; mod kbd; mod root; mod styled; -mod window_border; +mod window_ext; /// Initialize the UI module. /// diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 6678b88..226dcee 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -13,7 +13,7 @@ use theme::ActiveTheme; use crate::actions::{Cancel, Confirm}; use crate::animation::cubic_bezier; use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _}; -use crate::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; +use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; const CONTEXT: &str = "Modal"; @@ -97,9 +97,9 @@ pub struct Modal { button_props: ModalButtonProps, /// This will be change when open the modal, the focus handle is create when open the modal. - pub(crate) focus_handle: FocusHandle, - pub(crate) layer_ix: usize, - pub(crate) overlay_visible: bool, + pub focus_handle: FocusHandle, + pub layer_ix: usize, + pub overlay_visible: bool, } impl Modal { @@ -255,7 +255,7 @@ impl Modal { self } - pub(crate) fn has_overlay(&self) -> bool { + pub fn has_overlay(&self) -> bool { self.overlay } } @@ -341,7 +341,7 @@ impl RenderOnce for Modal { } }); - let window_paddings = crate::window_border::window_paddings(window, cx); + let window_paddings = crate::root::window_paddings(window, cx); let radius = (cx.theme().radius_lg * 2.).min(px(20.)); let view_size = window.viewport_size() diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 8c8b2e4..6b60319 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -425,7 +425,7 @@ impl NotificationList { cx.notify(); } - pub(crate) fn close(&mut self, key: T, window: &mut Window, cx: &mut Context) + pub fn close(&mut self, key: T, window: &mut Window, cx: &mut Context) where T: Into, { diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 3dbddbc..0998591 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -2,168 +2,63 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement, - IntoElement, ParentElement as _, Render, SharedString, Styled, Window, + canvas, div, point, px, 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, +}; +use theme::{ + ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING, + CLIENT_SIDE_DECORATION_SHADOW, }; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; use crate::input::InputState; use crate::modal::Modal; use crate::notification::{Notification, NotificationList}; -use crate::window_border; - -/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality. -pub trait ContextModal: Sized { - /// Opens a Modal. - fn open_modal(&mut self, cx: &mut App, build: F) - where - F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static; - - /// Return true, if there is an active Modal. - fn has_active_modal(&mut self, cx: &mut App) -> bool; - - /// Closes the last active Modal. - fn close_modal(&mut self, cx: &mut App); - - /// Closes all active Modals. - fn close_all_modals(&mut self, cx: &mut App); - - /// Returns number of notifications. - fn notifications(&mut self, cx: &mut App) -> Rc>>; - - /// Pushes a notification to the notification list. - fn push_notification(&mut self, note: impl Into, cx: &mut App); - - /// Clears a notification by its ID. - fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App); - - /// Clear all notifications - fn clear_notifications(&mut self, cx: &mut App); - - /// Return current focused Input entity. - fn focused_input(&mut self, cx: &mut App) -> Option>; - - /// Returns true if there is a focused Input entity. - fn has_focused_input(&mut self, cx: &mut App) -> bool; -} - -impl ContextModal for Window { - fn open_modal(&mut self, cx: &mut App, build: F) - where - F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, - { - Root::update(self, cx, move |root, window, cx| { - // Only save focus handle if there are no active modals. - // This is used to restore focus when all modals are closed. - if root.active_modals.is_empty() { - root.previous_focus_handle = window.focused(cx); - } - - let focus_handle = cx.focus_handle(); - focus_handle.focus(window, cx); - - root.active_modals.push(ActiveModal { - focus_handle, - builder: Rc::new(build), - }); - - cx.notify(); - }) - } - - fn has_active_modal(&mut self, cx: &mut App) -> bool { - !Root::read(self, cx).active_modals.is_empty() - } - - fn close_modal(&mut self, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.active_modals.pop(); - - if let Some(top_modal) = root.active_modals.last() { - // Focus the next modal. - top_modal.focus_handle.focus(window, cx); - } else { - // Restore focus if there are no more modals. - root.focus_back(window, cx); - } - cx.notify(); - }) - } - - fn close_all_modals(&mut self, cx: &mut App) { - Root::update(self, cx, |root, window, cx| { - root.active_modals.clear(); - root.focus_back(window, cx); - cx.notify(); - }) - } - - fn push_notification(&mut self, note: impl Into, cx: &mut App) { - let note = note.into(); - Root::update(self, cx, move |root, window, cx| { - root.notification - .update(cx, |view, cx| view.push(note, window, cx)); - cx.notify(); - }) - } - - fn clear_notifications(&mut self, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.notification - .update(cx, |view, cx| view.clear(window, cx)); - cx.notify(); - }) - } - - fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.notification.update(cx, |view, cx| { - view.close(id.clone(), window, cx); - }); - cx.notify(); - }) - } - - fn notifications(&mut self, cx: &mut App) -> Rc>> { - let entity = Root::read(self, cx).notification.clone(); - Rc::new(entity.read(cx).notifications()) - } - - fn has_focused_input(&mut self, cx: &mut App) -> bool { - Root::read(self, cx).focused_input.is_some() - } - - fn focused_input(&mut self, cx: &mut App) -> Option> { - Root::read(self, cx).focused_input.clone() - } -} - -type Builder = Rc Modal + 'static>; #[derive(Clone)] -pub(crate) struct ActiveModal { +#[allow(clippy::type_complexity)] +pub struct ActiveModal { focus_handle: FocusHandle, - builder: Builder, + /// The previous focused handle before opening the modal. + previous_focused_handle: Option, + builder: Rc Modal + 'static>, +} + +impl ActiveModal { + fn new( + focus_handle: FocusHandle, + previous_focused_handle: Option, + builder: impl Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + ) -> Self { + Self { + focus_handle, + previous_focused_handle, + builder: Rc::new(builder), + } + } } /// Root is a view for the App window for as the top level view (Must be the first view in the window). /// /// It is used to manage the Modal, and Notification. pub struct Root { + /// All active models pub(crate) active_modals: Vec, - pub notification: Entity, - pub focused_input: Option>, - /// Used to store the focus handle of the previous view. - /// - /// When the Modal closes, we will focus back to the previous view. - previous_focus_handle: Option, + + /// Notification layer + pub(crate) notification: Entity, + + /// Current focused input + pub(crate) focused_input: Option>, + + /// App view view: AnyView, } impl Root { pub fn new(view: AnyView, window: &mut Window, cx: &mut Context) -> Self { Self { - previous_focus_handle: None, focused_input: None, active_modals: Vec::new(), notification: cx.new(|cx| NotificationList::new(window, cx)), @@ -188,13 +83,11 @@ impl Root { .read(cx) } - fn focus_back(&mut self, window: &mut Window, cx: &mut App) { - if let Some(handle) = self.previous_focus_handle.clone() { - window.focus(&handle, cx); - } + pub fn view(&self) -> &AnyView { + &self.view } - /// Render Notification layer. + /// Render the notification layer. pub fn render_notification_layer( window: &mut Window, cx: &mut App, @@ -210,10 +103,9 @@ impl Root { ) } - /// Render the Modal layer. + /// Render the modal layer. pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option { let root = window.root::()??; - let active_modals = root.read(cx).active_modals.clone(); if active_modals.is_empty() { @@ -255,50 +147,271 @@ impl Root { Some(div().children(modals)) } - /// Return the root view of the Root. - pub fn view(&self) -> &AnyView { - &self.view + /// Open a modal. + pub fn open_modal(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + { + let previous_focused_handle = window.focused(cx).map(|h| h.downgrade()); + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + + self.active_modals.push(ActiveModal::new( + focus_handle, + previous_focused_handle, + builder, + )); + + cx.notify(); } - /// Replace the root view of the Root. - pub fn replace_view(&mut self, view: AnyView) { - self.view = view; + /// Close the topmost modal. + pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context) { + self.focused_input = None; + + if let Some(handle) = self + .active_modals + .pop() + .and_then(|d| d.previous_focused_handle) + .and_then(|h| h.upgrade()) + { + window.focus(&handle, cx); + } + + cx.notify(); + } + + /// Close all modals. + pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context) { + self.focused_input = None; + self.active_modals.clear(); + + let previous_focused_handle = self + .active_modals + .first() + .and_then(|d| d.previous_focused_handle.clone()); + + if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) { + window.focus(&handle, cx); + } + + cx.notify(); + } + + /// Check if there are any active modals. + pub fn has_active_modals(&self) -> bool { + !self.active_modals.is_empty() + } + + /// Push a notification to the notification layer. + pub fn push_notification(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>) + where + T: Into, + { + self.notification + .update(cx, |view, cx| view.push(note, window, cx)); + cx.notify(); + } + + pub fn clear_notification(&mut self, id: T, window: &mut Window, cx: &mut Context) + where + T: Into, + { + self.notification + .update(cx, |view, cx| view.close(id.into(), window, cx)); + cx.notify(); + } + + /// Clear all notifications from the notification layer. + pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) { + self.notification + .update(cx, |view, cx| view.clear(window, cx)); + cx.notify(); } } impl Render for Root { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let base_font_size = cx.theme().font_size; + let rem_size = cx.theme().font_size; let font_family = cx.theme().font_family.clone(); let decorations = window.window_decorations(); - window.set_rem_size(base_font_size); + // Set the base font size + window.set_rem_size(rem_size); - window_border().child( - div() - .id("root") - .map(|this| match decorations { - Decorations::Server => this, - Decorations::Client { tiling, .. } => this - .when(!(tiling.top || tiling.right), |el| { - el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |el| { - el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |el| { - el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |el| { - el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }), - }) - .relative() - .size_full() - .font_family(font_family) - .bg(cx.theme().background) - .text_color(cx.theme().text) - .child(self.view.clone()), - ) + // Set the client inset (linux only) + window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW); + + div() + .id("window") + .size_full() + .bg(gpui::transparent_black()) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .bg(gpui::transparent_black()) + .child( + canvas( + |_bounds, window, _cx| { + window.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + window.window_bounds().get_bounds().size, + ), + HitboxBehavior::Normal, + ) + }, + move |_bounds, hitbox, window, _cx| { + let mouse = window.mouse_position(); + let size = window.window_bounds().get_bounds().size; + + let Some(edge) = + resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size) + else { + return; + }; + + window.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => { + CursorStyle::ResizeUpDown + } + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW)) + .on_mouse_down(MouseButton::Left, move |e, window, _cx| { + let size = window.window_bounds().get_bounds().size; + let pos = e.position; + + match resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { + Some(edge) => window.start_window_resize(edge), + None => window.start_window_move(), + }; + }), + }) + .child( + div() + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .border_color(cx.theme().window_border) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| { + div.border_t(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.bottom, |div| { + div.border_b(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.left, |div| { + div.border_l(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.right, |div| { + div.border_r(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.is_tiled(), |div| { + div.shadow(vec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + }]) + }), + }) + .on_mouse_move(|_e, _, cx| { + cx.stop_propagation(); + }) + .size_full() + .font_family(font_family) + .bg(cx.theme().background) + .text_color(cx.theme().text) + .child(self.view.clone()), + ) } } + +/// Get the window paddings. +pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges { + match window.window_decorations() { + Decorations::Server => Edges::all(px(0.0)), + Decorations::Client { tiling } => { + let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW); + if tiling.top { + paddings.top = px(0.0); + } + if tiling.bottom { + paddings.bottom = px(0.0); + } + if tiling.left { + paddings.left = px(0.0); + } + if tiling.right { + paddings.right = px(0.0); + } + paddings + } + } +} + +/// 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 { + return None; + }; + Some(edge) +} diff --git a/crates/ui/src/window_border.rs b/crates/ui/src/window_border.rs deleted file mode 100644 index 8caddf3..0000000 --- a/crates/ui/src/window_border.rs +++ /dev/null @@ -1,204 +0,0 @@ -use gpui::prelude::FluentBuilder as _; -use gpui::{ - canvas, div, point, px, AnyElement, App, Bounds, CursorStyle, Decorations, Edges, - HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, - Point, RenderOnce, ResizeEdge, Size, Styled as _, Window, -}; -use theme::{CLIENT_SIDE_DECORATION_ROUNDING, CLIENT_SIDE_DECORATION_SHADOW}; - -const WINDOW_BORDER_WIDTH: Pixels = px(1.0); - -/// Create a new window border. -pub fn window_border() -> WindowBorder { - WindowBorder::new() -} - -/// Window border use to render a custom window border and shadow for Linux. -#[derive(IntoElement, Default)] -pub struct WindowBorder { - children: Vec, -} - -/// Get the window paddings. -pub fn window_paddings(window: &Window, _cx: &App) -> Edges { - match window.window_decorations() { - Decorations::Server => Edges::all(px(0.0)), - Decorations::Client { tiling } => { - let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW); - if tiling.top { - paddings.top = px(0.0); - } - if tiling.bottom { - paddings.bottom = px(0.0); - } - if tiling.left { - paddings.left = px(0.0); - } - if tiling.right { - paddings.right = px(0.0); - } - paddings - } - } -} - -impl WindowBorder { - pub fn new() -> Self { - Self { - ..Default::default() - } - } -} - -impl ParentElement for WindowBorder { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for WindowBorder { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let decorations = window.window_decorations(); - window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW); - - div() - .id("window-backdrop") - .bg(gpui::transparent_black()) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .bg(gpui::transparent_black()) - .child( - canvas( - |_bounds, window, _cx| { - window.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - window.window_bounds().get_bounds().size, - ), - HitboxBehavior::Normal, - ) - }, - move |_bounds, hitbox, window, _cx| { - let mouse = window.mouse_position(); - let size = window.window_bounds().get_bounds().size; - let Some(edge) = - resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size) - else { - return; - }; - window.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => { - CursorStyle::ResizeUpDown - } - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW)) - .on_mouse_down(MouseButton::Left, move |_, window, _cx| { - let size = window.window_bounds().get_bounds().size; - let pos = window.mouse_position(); - - if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { - window.start_window_resize(edge) - }; - }), - }) - .size_full() - .child( - div() - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling } => div - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.border_t(WINDOW_BORDER_WIDTH)) - .when(!tiling.bottom, |div| div.border_b(WINDOW_BORDER_WIDTH)) - .when(!tiling.left, |div| div.border_l(WINDOW_BORDER_WIDTH)) - .when(!tiling.right, |div| div.border_r(WINDOW_BORDER_WIDTH)) - .when(!tiling.is_tiled(), |div| { - div.shadow(vec![gpui::BoxShadow { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 0.3, - }, - blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2., - spread_radius: px(0.), - offset: point(px(0.0), px(0.0)), - }]) - }), - }) - .on_mouse_move(|_e, _window, cx| { - cx.stop_propagation(); - }) - .bg(gpui::transparent_black()) - .size_full() - .children(self.children), - ) - } -} - -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 { - return None; - }; - Some(edge) -} diff --git a/crates/ui/src/window_ext.rs b/crates/ui/src/window_ext.rs new file mode 100644 index 0000000..dd71dd7 --- /dev/null +++ b/crates/ui/src/window_ext.rs @@ -0,0 +1,120 @@ +use std::rc::Rc; + +use gpui::{App, Entity, SharedString, Window}; + +use crate::input::InputState; +use crate::modal::Modal; +use crate::notification::Notification; +use crate::Root; + +/// Extension trait for [`Window`] to add modal, notification .. functionality. +pub trait WindowExtension: Sized { + /// Opens a Modal. + fn open_modal(&mut self, cx: &mut App, builder: F) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static; + + /// Return true, if there is an active Modal. + fn has_active_modal(&mut self, cx: &mut App) -> bool; + + /// Closes the last active Modal. + fn close_modal(&mut self, cx: &mut App); + + /// Closes all active Modals. + fn close_all_modals(&mut self, cx: &mut App); + + /// Returns number of notifications. + fn notifications(&mut self, cx: &mut App) -> Rc>>; + + /// Pushes a notification to the notification list. + fn push_notification(&mut self, note: T, cx: &mut App) + where + T: Into; + + /// Clears a notification by its ID. + fn clear_notification(&mut self, id: T, cx: &mut App) + where + T: Into; + + /// Clear all notifications + fn clear_notifications(&mut self, cx: &mut App); + + /// Return current focused Input entity. + fn focused_input(&mut self, cx: &mut App) -> Option>; + + /// Returns true if there is a focused Input entity. + fn has_focused_input(&mut self, cx: &mut App) -> bool; +} + +impl WindowExtension for Window { + #[inline] + fn open_modal(&mut self, cx: &mut App, builder: F) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + { + Root::update(self, cx, move |root, window, cx| { + root.open_modal(builder, window, cx); + }) + } + + #[inline] + fn has_active_modal(&mut self, cx: &mut App) -> bool { + Root::read(self, cx).has_active_modals() + } + + #[inline] + fn close_modal(&mut self, cx: &mut App) { + Root::update(self, cx, move |root, window, cx| { + root.close_modal(window, cx); + }) + } + + #[inline] + fn close_all_modals(&mut self, cx: &mut App) { + Root::update(self, cx, |root, window, cx| { + root.close_all_modals(window, cx); + }) + } + + #[inline] + fn push_notification(&mut self, note: T, cx: &mut App) + where + T: Into, + { + let note = note.into(); + Root::update(self, cx, move |root, window, cx| { + root.push_notification(note, window, cx); + }) + } + + #[inline] + fn clear_notification(&mut self, id: T, cx: &mut App) + where + T: Into, + { + let id = id.into(); + Root::update(self, cx, move |root, window, cx| { + root.clear_notification(id, window, cx); + }) + } + + #[inline] + fn clear_notifications(&mut self, cx: &mut App) { + Root::update(self, cx, move |root, window, cx| { + root.clear_notifications(window, cx); + }) + } + + fn notifications(&mut self, cx: &mut App) -> Rc>> { + let entity = Root::read(self, cx).notification.clone(); + Rc::new(entity.read(cx).notifications()) + } + + fn has_focused_input(&mut self, cx: &mut App) -> bool { + Root::read(self, cx).focused_input.is_some() + } + + fn focused_input(&mut self, cx: &mut App) -> Option> { + Root::read(self, cx).focused_input.clone() + } +}