diff --git a/crates/story/examples/tiles.rs b/crates/story/examples/tiles.rs index b56be5d6..7231abc5 100644 --- a/crates/story/examples/tiles.rs +++ b/crates/story/examples/tiles.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use gpui::*; +use prelude::FluentBuilder as _; use std::time::Duration; use story::{Assets, ButtonStory, IconStory, StoryContainer}; use ui::{ diff --git a/crates/story/src/main.rs b/crates/story/src/main.rs index ffd26ea3..459a4d12 100644 --- a/crates/story/src/main.rs +++ b/crates/story/src/main.rs @@ -343,6 +343,7 @@ impl StoryWorkspace { cx.spawn(|mut cx| async move { let options = WindowOptions { window_bounds: Some(WindowBounds::Windowed(window_bounds)), + #[cfg(not(target_os = "linux"))] titlebar: Some(TitlebarOptions { title: None, appears_transparent: true, diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index ed60faa5..d74533c7 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -7,6 +7,7 @@ mod styled; mod svg_img; mod time; mod title_bar; +mod window_border; pub mod accordion; pub mod animation; @@ -60,6 +61,7 @@ pub use styled::*; pub use time::*; pub use title_bar::*; pub use virtual_list::{h_virtual_list, v_virtual_list, VirtualList}; +pub use window_border::{window_border, WindowBorder}; pub use colors::*; pub use icon::*; diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 459411c0..ea02bfbf 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -3,7 +3,7 @@ use crate::{ modal::Modal, notification::{Notification, NotificationList}, theme::ActiveTheme, - Placement, + window_border, Placement, }; use gpui::{ canvas, div, prelude::FluentBuilder as _, AnyView, DefiniteLength, FocusHandle, @@ -402,12 +402,14 @@ impl Render for Root { let base_font_size = cx.theme().font_size; cx.set_rem_size(base_font_size); - div() - .id("root") - .size_full() - .font_family(".SystemUIFont") - .bg(cx.theme().background) - .text_color(cx.theme().foreground) - .child(self.view.clone()) + window_border().child( + div() + .id("root") + .size_full() + .font_family(".SystemUIFont") + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .child(self.view.clone()), + ) } } diff --git a/crates/ui/src/title_bar.rs b/crates/ui/src/title_bar.rs index f2c3d2fa..c76d5233 100644 --- a/crates/ui/src/title_bar.rs +++ b/crates/ui/src/title_bar.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _}; use gpui::{ div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Div, Element, Hsla, - InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, Stateful, + InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Stateful, StatefulInteractiveElement as _, Style, Styled, WindowContext, }; @@ -152,7 +152,11 @@ impl RenderOnce for ControlIcon { .items_center() .text_color(fg) .when(is_linux, |this| { - this.on_click(move |_, cx| match icon { + this.on_mouse_down(MouseButton::Left, move |_, cx| { + cx.prevent_default(); + cx.stop_propagation(); + }) + .on_click(move |_, cx| match icon { Self::Minimize => cx.minimize_window(), Self::Restore => cx.zoom_window(), Self::Maximize => cx.zoom_window(), @@ -221,43 +225,41 @@ impl RenderOnce for TitleBar { const HEIGHT: Pixels = px(34.); - div() - .flex_shrink_0() - .child( - self.base - .flex() - .flex_row() - .items_center() - .justify_between() - .h(HEIGHT) - .border_b_1() - .border_color(cx.theme().title_bar_border) - .bg(cx.theme().title_bar) - .when(cx.is_fullscreen(), |this| this.pl(px(12.))) - .on_double_click(|_, cx| cx.zoom_window()) - .child( - h_flex() - .h_full() - .justify_between() - .flex_shrink_0() - .flex_1() - .children(self.children), - ) - .child(WindowControls { - on_close_window: self.on_close_window, - }), - ) - .when(is_linux, |this| { - this.child( - div() - .top_0() - .left_0() - .absolute() - .size_full() + div().flex_shrink_0().child( + self.base + .flex() + .flex_row() + .items_center() + .justify_between() + .h(HEIGHT) + .border_b_1() + .border_color(cx.theme().title_bar_border) + .bg(cx.theme().title_bar) + .when(cx.is_fullscreen(), |this| this.pl(px(12.))) + .on_double_click(|_, cx| cx.zoom_window()) + .child( + h_flex() .h_full() - .child(TitleBarElement {}), + .justify_between() + .flex_shrink_0() + .flex_1() + .when(is_linux, |this| { + this.child( + div() + .top_0() + .left_0() + .absolute() + .size_full() + .h_full() + .child(TitleBarElement {}), + ) + }) + .children(self.children), ) - }) + .child(WindowControls { + on_close_window: self.on_close_window, + }), + ) } } @@ -322,7 +324,7 @@ impl Element for TitleBarElement { }); cx.on_mouse_event(move |ev: &MouseUpEvent, _, cx: &mut WindowContext| { - if ev.button == MouseButton::Left { + if bounds.contains(&ev.position) && ev.button == MouseButton::Right { cx.show_window_menu(ev.position); } }); diff --git a/crates/ui/src/window_border.rs b/crates/ui/src/window_border.rs new file mode 100644 index 00000000..2aa5d913 --- /dev/null +++ b/crates/ui/src/window_border.rs @@ -0,0 +1,172 @@ +// From: +// https://github.com/zed-industries/zed/blob/a8afc63a91f6b75528540dcffe73dc8ce0c92ad8/crates/gpui/examples/window_shadow.rs +use gpui::{ + canvas, div, point, prelude::FluentBuilder as _, px, AnyElement, Bounds, CursorStyle, + Decorations, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, + Point, RenderOnce, ResizeEdge, Size, Styled as _, WindowContext, +}; + +const SHADOW_SIZE: Pixels = Pixels(12.0); +const BORDER_SIZE: Pixels = Pixels(0.0); +pub(crate) const BORDER_RADIUS: Pixels = Pixels(0.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, +} + +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, cx: &mut WindowContext) -> impl IntoElement { + let decorations = cx.window_decorations(); + cx.set_client_inset(SHADOW_SIZE); + + 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, cx| { + cx.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + cx.window_bounds().get_bounds().size, + ), + false, + ) + }, + move |_bounds, hitbox, cx| { + let mouse = cx.mouse_position(); + let size = cx.window_bounds().get_bounds().size; + let Some(edge) = resize_edge(mouse, SHADOW_SIZE, size) else { + return; + }; + cx.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(BORDER_RADIUS) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(BORDER_RADIUS) + }) + .when(!tiling.top, |div| div.pt(SHADOW_SIZE)) + .when(!tiling.bottom, |div| div.pb(SHADOW_SIZE)) + .when(!tiling.left, |div| div.pl(SHADOW_SIZE)) + .when(!tiling.right, |div| div.pr(SHADOW_SIZE)) + .on_mouse_move(|_e, cx| cx.refresh()) + .on_mouse_down(MouseButton::Left, move |_, cx| { + let size = cx.window_bounds().get_bounds().size; + let pos = cx.mouse_position(); + + match resize_edge(pos, SHADOW_SIZE, size) { + Some(edge) => cx.start_window_resize(edge), + None => {} + }; + }), + }) + .size_full() + .child( + div() + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + // .border_color(cx.theme().window_border) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(BORDER_RADIUS) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(BORDER_RADIUS) + }) + .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) + .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) + .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) + .when(!tiling.right, |div| div.border_r(BORDER_SIZE)) + .when(!tiling.is_tiled(), |div| { + div.shadow(smallvec::smallvec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.3, + }, + blur_radius: SHADOW_SIZE / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + }]) + }), + }) + .on_mouse_move(|_e, 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) +}