refactor root component

This commit is contained in:
2026-01-16 15:29:34 +07:00
parent 4c4fe0cc0c
commit 81b1f2b293
21 changed files with 445 additions and 412 deletions

View File

@@ -30,8 +30,8 @@ use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification; use ui::notification::Notification;
use ui::popup_menu::PopupMenuExt; use ui::popup_menu::PopupMenuExt;
use ui::{ use ui::{
h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
StyledExt, WindowExtension,
}; };
use crate::emoji::EmojiPicker; use crate::emoji::EmojiPicker;

View File

@@ -25,7 +25,7 @@ use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::popup_menu::PopupMenuExt; 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::{ use crate::actions::{
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,

View File

@@ -16,7 +16,7 @@ use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{v_flex, ContextModal, Disableable, StyledExt}; use ui::{v_flex, Disableable, StyledExt, WindowExtension};
use crate::actions::CoopAuthUrlHandler; use crate::actions::CoopAuthUrlHandler;

View File

@@ -4,7 +4,7 @@ use assets::Assets;
use common::{APP_ID, CLIENT_NAME}; use common::{APP_ID, CLIENT_NAME};
use gpui::{ use gpui::{
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions, WindowOptions,
}; };
use ui::Root; use ui::Root;
@@ -58,6 +58,7 @@ fn main() {
window_background: WindowBackgroundAppearance::Opaque, window_background: WindowBackgroundAppearance::Opaque,
window_decorations: Some(WindowDecorations::Client), window_decorations: Some(WindowDecorations::Client),
window_bounds: Some(WindowBounds::Windowed(bounds)), window_bounds: Some(WindowBounds::Windowed(bounds)),
window_min_size: Some(Size::new(px(640.), px(480.))),
kind: WindowKind::Normal, kind: WindowKind::Normal,
app_id: Some(APP_ID.to_owned()), app_id: Some(APP_ID.to_owned()),
titlebar: Some(TitlebarOptions { titlebar: Some(TitlebarOptions {

View File

@@ -15,7 +15,7 @@ use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputState, TextInput}; use ui::input::{InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable}; use ui::{divider, v_flex, Disableable, IconName, Sizable, WindowExtension};
mod backup; mod backup;

View File

@@ -14,7 +14,7 @@ use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::skeleton::Skeleton; use ui::skeleton::Skeleton;
use ui::{h_flex, ContextModal, StyledExt}; use ui::{h_flex, StyledExt, WindowExtension};
use crate::views::screening; use crate::views::screening;

View File

@@ -20,7 +20,7 @@ use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::PopupMenuExt; 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}; use crate::actions::{RelayStatus, Reload};

View File

@@ -18,7 +18,7 @@ use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput}; 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; pub mod viewer;

View File

@@ -21,7 +21,7 @@ use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::notification::Notification; 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 { pub fn compose_button() -> impl IntoElement {
div().child( div().child(

View File

@@ -16,7 +16,7 @@ use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::notification::Notification; 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}; use crate::chatspace::{self};

View File

@@ -15,7 +15,7 @@ use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::indicator::Indicator; 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<Screening> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
cx.new(|cx| Screening::new(public_key, window, cx)) cx.new(|cx| Screening::new(public_key, window, cx))

View File

@@ -14,7 +14,7 @@ use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput}; 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<SetupRelay> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<SetupRelay> {
cx.new(|cx| SetupRelay::new(window, cx)) cx.new(|cx| SetupRelay::new(window, cx))

View File

@@ -18,7 +18,7 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator; 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}; use crate::actions::{reset, CoopAuthUrlHandler};

View File

@@ -16,7 +16,7 @@ use state::{tracker, NostrRegistry};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{v_flex, ContextModal, Disableable, IconName, Sizable}; use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension};
const AUTH_MESSAGE: &str = const AUTH_MESSAGE: &str =
"Approve the authentication request to allow Coop to continue sending or receiving events."; "Approve the authentication request to allow Coop to continue sending or receiving events.";
@@ -243,7 +243,7 @@ impl RelayAuth {
Ok(_) => { Ok(_) => {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
// Clear the current notification // Clear the current notification
window.clear_notification_by_id(SharedString::from(&challenge), cx); window.clear_notification(challenge, cx);
// Push a new notification // Push a new notification
window.push_notification(format!("{url} has been authenticated"), cx); window.push_notification(format!("{url} has been authenticated"), cx);

View File

@@ -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. /// Defines window shadow size for platforms that use client side decorations.
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); 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) { pub fn init(cx: &mut App) {
registry::init(cx); registry::init(cx);

View File

@@ -3,9 +3,9 @@ pub use focusable::FocusableCycle;
pub use icon::*; pub use icon::*;
pub use kbd::*; pub use kbd::*;
pub use menu::{context_menu, popup_menu}; pub use menu::{context_menu, popup_menu};
pub use root::{ContextModal, Root}; pub use root::Root;
pub use styled::*; pub use styled::*;
pub use window_border::{window_border, WindowBorder}; pub use window_ext::*;
pub use crate::Disableable; pub use crate::Disableable;
@@ -38,7 +38,7 @@ mod icon;
mod kbd; mod kbd;
mod root; mod root;
mod styled; mod styled;
mod window_border; mod window_ext;
/// Initialize the UI module. /// Initialize the UI module.
/// ///

View File

@@ -13,7 +13,7 @@ use theme::ActiveTheme;
use crate::actions::{Cancel, Confirm}; use crate::actions::{Cancel, Confirm};
use crate::animation::cubic_bezier; use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _}; 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"; const CONTEXT: &str = "Modal";
@@ -97,9 +97,9 @@ pub struct Modal {
button_props: ModalButtonProps, button_props: ModalButtonProps,
/// This will be change when open the modal, the focus handle is create when open the modal. /// This will be change when open the modal, the focus handle is create when open the modal.
pub(crate) focus_handle: FocusHandle, pub focus_handle: FocusHandle,
pub(crate) layer_ix: usize, pub layer_ix: usize,
pub(crate) overlay_visible: bool, pub overlay_visible: bool,
} }
impl Modal { impl Modal {
@@ -255,7 +255,7 @@ impl Modal {
self self
} }
pub(crate) fn has_overlay(&self) -> bool { pub fn has_overlay(&self) -> bool {
self.overlay 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 radius = (cx.theme().radius_lg * 2.).min(px(20.));
let view_size = window.viewport_size() let view_size = window.viewport_size()

View File

@@ -425,7 +425,7 @@ impl NotificationList {
cx.notify(); cx.notify();
} }
pub(crate) fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>) pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
where where
T: Into<ElementId>, T: Into<ElementId>,
{ {

View File

@@ -2,168 +2,63 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement, canvas, div, point, px, AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations,
IntoElement, ParentElement as _, Render, SharedString, Styled, Window, 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::input::InputState;
use crate::modal::Modal; use crate::modal::Modal;
use crate::notification::{Notification, NotificationList}; 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<F>(&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<Vec<Entity<Notification>>>;
/// Pushes a notification to the notification list.
fn push_notification(&mut self, note: impl Into<Notification>, 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<Entity<InputState>>;
/// 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<F>(&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<Notification>, 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<Vec<Entity<Notification>>> {
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<Entity<InputState>> {
Root::read(self, cx).focused_input.clone()
}
}
type Builder = Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>;
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ActiveModal { #[allow(clippy::type_complexity)]
pub struct ActiveModal {
focus_handle: FocusHandle, focus_handle: FocusHandle,
builder: Builder, /// The previous focused handle before opening the modal.
previous_focused_handle: Option<WeakFocusHandle>,
builder: Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>,
}
impl ActiveModal {
fn new(
focus_handle: FocusHandle,
previous_focused_handle: Option<WeakFocusHandle>,
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). /// 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. /// It is used to manage the Modal, and Notification.
pub struct Root { pub struct Root {
/// All active models
pub(crate) active_modals: Vec<ActiveModal>, pub(crate) active_modals: Vec<ActiveModal>,
pub notification: Entity<NotificationList>,
pub focused_input: Option<Entity<InputState>>, /// Notification layer
/// Used to store the focus handle of the previous view. pub(crate) notification: Entity<NotificationList>,
///
/// When the Modal closes, we will focus back to the previous view. /// Current focused input
previous_focus_handle: Option<FocusHandle>, pub(crate) focused_input: Option<Entity<InputState>>,
/// App view
view: AnyView, view: AnyView,
} }
impl Root { impl Root {
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
Self { Self {
previous_focus_handle: None,
focused_input: None, focused_input: None,
active_modals: Vec::new(), active_modals: Vec::new(),
notification: cx.new(|cx| NotificationList::new(window, cx)), notification: cx.new(|cx| NotificationList::new(window, cx)),
@@ -188,13 +83,11 @@ impl Root {
.read(cx) .read(cx)
} }
fn focus_back(&mut self, window: &mut Window, cx: &mut App) { pub fn view(&self) -> &AnyView {
if let Some(handle) = self.previous_focus_handle.clone() { &self.view
window.focus(&handle, cx);
}
} }
/// Render Notification layer. /// Render the notification layer.
pub fn render_notification_layer( pub fn render_notification_layer(
window: &mut Window, window: &mut Window,
cx: &mut App, 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<impl IntoElement> { pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
let root = window.root::<Root>()??; let root = window.root::<Root>()??;
let active_modals = root.read(cx).active_modals.clone(); let active_modals = root.read(cx).active_modals.clone();
if active_modals.is_empty() { if active_modals.is_empty() {
@@ -255,45 +147,219 @@ impl Root {
Some(div().children(modals)) Some(div().children(modals))
} }
/// Return the root view of the Root. /// Open a modal.
pub fn view(&self) -> &AnyView { pub fn open_modal<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
&self.view 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. /// Close the topmost modal.
pub fn replace_view(&mut self, view: AnyView) { pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.view = view; 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>) {
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<T>(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>)
where
T: Into<Notification>,
{
self.notification
.update(cx, |view, cx| view.push(note, window, cx));
cx.notify();
}
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<SharedString>,
{
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 { impl Render for Root {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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 font_family = cx.theme().font_family.clone();
let decorations = window.window_decorations(); let decorations = window.window_decorations();
window.set_rem_size(base_font_size); // Set the base font size
window.set_rem_size(rem_size);
// Set the client inset (linux only)
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
window_border().child(
div() div()
.id("root") .id("window")
.map(|this| match decorations { .size_full()
Decorations::Server => this, .bg(gpui::transparent_black())
Decorations::Client { tiling, .. } => this .map(|div| match decorations {
.when(!(tiling.top || tiling.right), |el| { Decorations::Server => div,
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) 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), |el| { .when(!(tiling.top || tiling.left), |div| {
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
}) })
.when(!(tiling.bottom || tiling.right), |el| { .when(!(tiling.bottom || tiling.right), |div| {
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
}) })
.when(!(tiling.bottom || tiling.left), |el| { .when(!(tiling.bottom || tiling.left), |div| {
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) 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(),
};
}), }),
}) })
.relative() .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() .size_full()
.font_family(font_family) .font_family(font_family)
.bg(cx.theme().background) .bg(cx.theme().background)
@@ -302,3 +368,50 @@ impl Render for Root {
) )
} }
} }
/// Get the window paddings.
pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
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<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
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)
}

View File

@@ -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<AnyElement>,
}
/// Get the window paddings.
pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
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<Item = AnyElement>) {
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<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
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)
}

120
crates/ui/src/window_ext.rs Normal file
View File

@@ -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<F>(&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<Vec<Entity<Notification>>>;
/// Pushes a notification to the notification list.
fn push_notification<T>(&mut self, note: T, cx: &mut App)
where
T: Into<Notification>;
/// Clears a notification by its ID.
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>;
/// 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<Entity<InputState>>;
/// 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<F>(&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<T>(&mut self, note: T, cx: &mut App)
where
T: Into<Notification>,
{
let note = note.into();
Root::update(self, cx, move |root, window, cx| {
root.push_notification(note, window, cx);
})
}
#[inline]
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>,
{
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<Vec<Entity<Notification>>> {
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<Entity<InputState>> {
Root::read(self, cx).focused_input.clone()
}
}