From 46283404598e735d5a454eaf0562fac52cd9f8a8 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 30 Dec 2024 07:52:26 +0700 Subject: [PATCH] feat: update gpui-components --- crates/ui/src/drawer.rs | 247 ------------------------ crates/ui/src/lib.rs | 24 +-- crates/ui/src/modal.rs | 138 ++++++------- crates/ui/src/popup_menu.rs | 33 ++-- crates/ui/src/root.rs | 116 ++--------- crates/ui/src/scroll/scrollable_mask.rs | 65 +++---- crates/ui/src/theme.rs | 3 + crates/ui/src/title_bar.rs | 88 ++++----- crates/ui/src/window_border.rs | 201 +++++++++++++++++++ 9 files changed, 397 insertions(+), 518 deletions(-) delete mode 100644 crates/ui/src/drawer.rs create mode 100644 crates/ui/src/window_border.rs diff --git a/crates/ui/src/drawer.rs b/crates/ui/src/drawer.rs deleted file mode 100644 index 66d3985..0000000 --- a/crates/ui/src/drawer.rs +++ /dev/null @@ -1,247 +0,0 @@ -use std::{rc::Rc, time::Duration}; - -use gpui::{ - actions, anchored, div, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, - AnyElement, AppContext, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter, - FocusHandle, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, - Pixels, RenderOnce, Styled, WindowContext, -}; - -use crate::{ - button::{Button, ButtonVariants as _}, - h_flex, - modal::overlay_color, - root::ContextModal as _, - scroll::ScrollbarAxis, - theme::ActiveTheme, - title_bar::TITLE_BAR_HEIGHT, - v_flex, IconName, Placement, Sizable, StyledExt as _, -}; - -actions!(drawer, [Escape]); - -const CONTEXT: &str = "Drawer"; - -pub fn init(cx: &mut AppContext) { - cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) -} - -type OnClose = Rc; - -#[derive(IntoElement)] -pub struct Drawer { - pub(crate) focus_handle: FocusHandle, - placement: Placement, - size: DefiniteLength, - resizable: bool, - on_close: OnClose, - title: Option, - footer: Option, - content: Div, - margin_top: Pixels, - overlay: bool, -} - -impl Drawer { - pub fn new(cx: &mut WindowContext) -> Self { - Self { - focus_handle: cx.focus_handle(), - placement: Placement::Right, - size: DefiniteLength::Absolute(px(350.).into()), - resizable: true, - title: None, - footer: None, - content: v_flex().px_4().py_3(), - margin_top: TITLE_BAR_HEIGHT, - overlay: true, - on_close: Rc::new(|_, _| {}), - } - } - - /// Sets the title of the drawer. - pub fn title(mut self, title: impl IntoElement) -> Self { - self.title = Some(title.into_any_element()); - self - } - - /// Set the footer of the drawer. - pub fn footer(mut self, footer: impl IntoElement) -> Self { - self.footer = Some(footer.into_any_element()); - self - } - - /// Sets the size of the drawer, default is 350px. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } - - /// Sets the margin top of the drawer, default is 0px. - /// - /// This is used to let Drawer be placed below a Windows Title, you can give the height of the title bar. - pub fn margin_top(mut self, top: Pixels) -> Self { - self.margin_top = top; - self - } - - /// Sets the placement of the drawer, default is `Placement::Right`. - pub fn placement(mut self, placement: Placement) -> Self { - self.placement = placement; - self - } - - /// Sets the placement of the drawer, default is `Placement::Right`. - pub fn set_placement(&mut self, placement: Placement) { - self.placement = placement; - } - - /// Sets whether the drawer is resizable, default is `true`. - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; - self - } - - /// Set whether the drawer should have an overlay, default is `true`. - pub fn overlay(mut self, overlay: bool) -> Self { - self.overlay = overlay; - self - } - - /// Listen to the close event of the drawer. - pub fn on_close( - mut self, - on_close: impl Fn(&ClickEvent, &mut WindowContext) + 'static, - ) -> Self { - self.on_close = Rc::new(on_close); - self - } -} - -impl EventEmitter for Drawer {} -impl ParentElement for Drawer { - fn extend(&mut self, elements: impl IntoIterator) { - self.content.extend(elements); - } -} -impl Styled for Drawer { - fn style(&mut self) -> &mut gpui::StyleRefinement { - self.content.style() - } -} - -impl RenderOnce for Drawer { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let placement = self.placement; - let titlebar_height = self.margin_top; - let size = cx.viewport_size(); - let on_close = self.on_close.clone(); - - anchored() - .position(point(px(0.), titlebar_height)) - .snap_to_window() - .child( - div() - .occlude() - .w(size.width) - .h(size.height - titlebar_height) - .bg(overlay_color(self.overlay, cx)) - .when(self.overlay, |this| { - this.on_mouse_down(MouseButton::Left, { - let on_close = self.on_close.clone(); - move |_, cx| { - on_close(&ClickEvent::default(), cx); - cx.close_drawer(); - } - }) - }) - .child( - v_flex() - .id("drawer") - .key_context(CONTEXT) - .track_focus(&self.focus_handle) - .on_action({ - let on_close = self.on_close.clone(); - move |_: &Escape, cx| { - on_close(&ClickEvent::default(), cx); - cx.close_drawer(); - } - }) - .absolute() - .occlude() - .bg(cx.theme().background) - .border_color(cx.theme().border) - .shadow_xl() - .map(|this| { - // Set the size of the drawer. - if placement.is_horizontal() { - this.h_full().w(self.size) - } else { - this.w_full().h(self.size) - } - }) - .map(|this| match self.placement { - Placement::Top => this.top_0().left_0().right_0().border_b_1(), - Placement::Right => this.top_0().right_0().bottom_0().border_l_1(), - Placement::Bottom => { - this.bottom_0().left_0().right_0().border_t_1() - } - Placement::Left => this.top_0().left_0().bottom_0().border_r_1(), - }) - .child( - // TitleBar - h_flex() - .justify_between() - .px_4() - .py_2() - .w_full() - .child(self.title.unwrap_or(div().into_any_element())) - .child( - Button::new("close") - .small() - .ghost() - .icon(IconName::Close) - .on_click(move |_, cx| { - on_close(&ClickEvent::default(), cx); - cx.close_drawer(); - }), - ), - ) - .child( - // Body - div().flex_1().overflow_hidden().child( - v_flex() - .scrollable( - cx.parent_view_id().unwrap_or_default(), - ScrollbarAxis::Vertical, - ) - .child(self.content), - ), - ) - .when_some(self.footer, |this, footer| { - // Footer - this.child( - h_flex() - .justify_between() - .px_4() - .py_3() - .w_full() - .child(footer), - ) - }) - .with_animation( - "slide", - Animation::new(Duration::from_secs_f64(0.15)), - move |this, delta| { - let y = px(-100.) + delta * px(100.); - this.map(|this| match placement { - Placement::Top => this.top(y), - Placement::Right => this.right(y), - Placement::Bottom => this.bottom(y), - Placement::Left => this.left(y), - }) - }, - ), - ), - ) - } -} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index b6efa16..27128a3 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,11 +1,3 @@ -mod colors; -mod event; -mod focusable; -mod icon; -mod root; -mod styled; -mod title_bar; - pub mod accordion; pub mod animation; pub mod badge; @@ -18,7 +10,6 @@ pub mod color_picker; pub mod context_menu; pub mod divider; pub mod dock; -pub mod drawer; pub mod dropdown; pub mod history; pub mod indicator; @@ -45,14 +36,24 @@ pub mod theme; pub mod tooltip; pub use crate::Disableable; + +pub use colors::*; pub use event::InteractiveElementExt; pub use focusable::FocusableCycle; +pub use icon::*; pub use root::{ContextModal, Root}; pub use styled::*; pub use title_bar::*; +pub use window_border::{window_border, WindowBorder}; -pub use colors::*; -pub use icon::*; +mod colors; +mod event; +mod focusable; +mod icon; +mod root; +mod styled; +mod title_bar; +mod window_border; use rust_embed::RustEmbed; @@ -67,7 +68,6 @@ pub struct Assets; pub fn init(cx: &mut gpui::AppContext) { theme::init(cx); dock::init(cx); - drawer::init(cx); dropdown::init(cx); input::init(cx); number_input::init(cx); diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 921ce4d..3935987 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -1,11 +1,10 @@ -use std::{rc::Rc, time::Duration}; - use gpui::{ - actions, anchored, div, hsla, prelude::FluentBuilder, px, relative, Animation, + actions, anchored, div, hsla, point, prelude::FluentBuilder, px, relative, Animation, AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled, WindowContext, }; +use std::{rc::Rc, time::Duration}; use crate::{ animation::cubic_bezier, @@ -37,7 +36,6 @@ pub struct Modal { show_close: bool, overlay: bool, keyboard: bool, - /// 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, @@ -164,7 +162,12 @@ impl RenderOnce for Modal { fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement { let layer_ix = self.layer_ix; let on_close = self.on_close.clone(); - let view_size = cx.viewport_size(); + let window_paddings = crate::window_border::window_paddings(cx); + let view_size = cx.viewport_size() + - gpui::size( + window_paddings.left + window_paddings.right, + window_paddings.top + window_paddings.bottom, + ); let bounds = Bounds { origin: Point::default(), size: view_size, @@ -173,78 +176,83 @@ impl RenderOnce for Modal { let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top; let x = bounds.center().x - self.width / 2.; - anchored().snap_to_window().child( - div() - .occlude() - .w(view_size.width) - .h(view_size.height) - .when(self.overlay_visible, |this| { - this.bg(overlay_color(self.overlay, cx)) - }) - .when(self.overlay, |this| { - this.on_mouse_down(MouseButton::Left, { + anchored() + .position(point(window_paddings.left, window_paddings.top)) + .snap_to_window() + .child( + div() + .occlude() + .w(view_size.width) + .h(view_size.height) + .when(self.overlay_visible, |this| { + this.bg(overlay_color(self.overlay, cx)) + }) + .on_mouse_down(MouseButton::Left, { let on_close = self.on_close.clone(); move |_, cx| { on_close(&ClickEvent::default(), cx); cx.close_modal(); } }) - }) - .child( - self.base - .id(SharedString::from(format!("modal-{layer_ix}"))) - .key_context(CONTEXT) - .track_focus(&self.focus_handle) - .when(self.keyboard, |this| { - this.on_action({ - let on_close = self.on_close.clone(); - move |_: &Escape, cx| { - // FIXME: - // - // Here some Modal have no focus_handle, so it will not work will Escape key. - // But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work. - on_close(&ClickEvent::default(), cx); - cx.close_modal(); - } + .child( + self.base + .id(SharedString::from(format!("modal-{layer_ix}"))) + .key_context(CONTEXT) + .track_focus(&self.focus_handle) + .when(self.keyboard, |this| { + this.on_action({ + let on_close = self.on_close.clone(); + move |_: &Escape, cx| { + // FIXME: + // + // Here some Modal have no focus_handle, so it will not work will Escape key. + // But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work. + on_close(&ClickEvent::default(), cx); + cx.close_modal(); + } + }) }) - }) - .absolute() - .occlude() - .relative() - .left(x) - .top(y) - .w(self.width) - .when_some(self.max_width, |this, w| this.max_w(w)) - .when_some(self.title, |this, title| { - this.child(div().line_height(relative(1.)).child(title)) - }) - .when(self.show_close, |this| { - this.child( - Button::new(SharedString::from(format!("modal-close-{layer_ix}"))) + .absolute() + .occlude() + .relative() + .left(x) + .top(y) + .w(self.width) + .when_some(self.max_width, |this, w| this.max_w(w)) + .when_some(self.title, |this, title| { + this.child(div().line_height(relative(1.)).child(title)) + }) + .when(self.show_close, |this| { + this.child( + Button::new(SharedString::from(format!( + "modal-close-{layer_ix}" + ))) .absolute() .top_2() .right_2() .small() .ghost() .icon(IconName::Close) - .on_click(move |_, cx| { - on_close(&ClickEvent::default(), cx); - cx.close_modal(); - }), - ) - }) - .child(self.content) - .children(self.footer) - .with_animation( - "slide-down", - Animation::new(Duration::from_secs_f64(0.25)) - .with_easing(cubic_bezier(0.32, 0.72, 0., 1.)), - move |this, delta| { - let y_offset = px(0.) + delta * px(30.); - this.top(y + y_offset).opacity(delta) - }, - ), - ), - ) + .on_click( + move |_, cx| { + on_close(&ClickEvent::default(), cx); + cx.close_modal(); + }, + ), + ) + }) + .child(self.content) + .children(self.footer) + .with_animation( + "slide-down", + Animation::new(Duration::from_secs_f64(0.25)) + .with_easing(cubic_bezier(0.32, 0.72, 0., 1.)), + move |this, delta| { + let y_offset = px(0.) + delta * px(30.); + this.top(y + y_offset).opacity(delta) + }, + ), + ), + ) } } diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index c61427d..70cbca4 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -1,16 +1,6 @@ -use std::cell::Cell; -use std::ops::Deref; -use std::rc::Rc; - -use gpui::{ - actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter, - FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, - SharedString, View, ViewContext, VisualContext as _, WindowContext, -}; -use gpui::{ - anchored, canvas, rems, AnyElement, Bounds, Corner, Edges, FocusableView, Keystroke, - ScrollHandle, StatefulInteractiveElement, Styled, WeakView, -}; +use gpui::*; +use prelude::FluentBuilder; +use std::{cell::Cell, ops::Deref, rc::Rc}; use crate::scroll::{Scrollbar, ScrollbarState}; use crate::StyledExt; @@ -386,9 +376,10 @@ impl PopupMenu { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { let count = self.clickable_menu_items().count(); if count > 0 { + let last_ix = count.saturating_sub(1); let ix = self .selected_index - .map(|index| if index == count - 1 { 0 } else { index + 1 }) + .map(|index| if index == last_ix { 0 } else { index + 1 }) .unwrap_or(0); self.selected_index = Some(ix); @@ -399,10 +390,18 @@ impl PopupMenu { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { let count = self.clickable_menu_items().count(); if count > 0 { + let last_ix = count.saturating_sub(1); + let ix = self .selected_index - .map(|index| if index == count - 1 { 0 } else { index - 1 }) - .unwrap_or(count - 1); + .map(|index| { + if index == last_ix { + 0 + } else { + index.saturating_sub(1) + } + }) + .unwrap_or(last_ix); self.selected_index = Some(ix); cx.notify(); } @@ -473,7 +472,9 @@ impl PopupMenu { } impl FluentBuilder for PopupMenu {} + impl EventEmitter for PopupMenu {} + impl FocusableView for PopupMenu { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 5be3966..9573fca 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -1,9 +1,3 @@ -use crate::{ - drawer::Drawer, - modal::Modal, - notification::{Notification, NotificationList}, - theme::ActiveTheme, -}; use gpui::{ div, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, View, ViewContext, VisualContext as _, WindowContext, @@ -13,19 +7,15 @@ use std::{ rc::Rc, }; +use crate::{ + modal::Modal, + notification::{Notification, NotificationList}, + theme::ActiveTheme, + window_border, +}; + /// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality. pub trait ContextModal: Sized { - /// Opens a Drawer. - fn open_drawer(&mut self, build: F) - where - F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static; - - /// Return true, if there is an active Drawer. - fn has_active_drawer(&self) -> bool; - - /// Closes the active Drawer. - fn close_drawer(&mut self); - /// Opens a Modal. fn open_modal(&mut self, build: F) where @@ -48,38 +38,6 @@ pub trait ContextModal: Sized { } impl ContextModal for WindowContext<'_> { - fn open_drawer(&mut self, build: F) - where - F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static, - { - Root::update(self, move |root, cx| { - if root.active_drawer.is_none() { - root.previous_focus_handle = cx.focused(); - } - - let focus_handle = cx.focus_handle(); - focus_handle.focus(cx); - - root.active_drawer = Some(ActiveDrawer { - focus_handle, - builder: Rc::new(build), - }); - cx.notify(); - }) - } - - fn has_active_drawer(&self) -> bool { - Root::read(self).active_drawer.is_some() - } - - fn close_drawer(&mut self) { - Root::update(self, |root, cx| { - root.active_drawer = None; - root.focus_back(cx); - cx.notify(); - }) - } - fn open_modal(&mut self, build: F) where F: Fn(Modal, &mut WindowContext) -> Modal + 'static, @@ -149,21 +107,10 @@ impl ContextModal for WindowContext<'_> { } } impl ContextModal for ViewContext<'_, V> { - fn open_drawer(&mut self, build: F) - where - F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static, - { - self.deref_mut().open_drawer(build) - } - fn has_active_modal(&self) -> bool { self.deref().has_active_modal() } - fn close_drawer(&mut self) { - self.deref_mut().close_drawer() - } - fn open_modal(&mut self, build: F) where F: Fn(Modal, &mut WindowContext) -> Modal + 'static, @@ -171,10 +118,6 @@ impl ContextModal for ViewContext<'_, V> { self.deref_mut().open_modal(build) } - fn has_active_drawer(&self) -> bool { - self.deref().has_active_drawer() - } - /// Close the last active modal. fn close_modal(&mut self) { self.deref_mut().close_modal() @@ -205,21 +148,13 @@ pub struct Root { /// Used to store the focus handle of the previous view. /// When the Modal, Drawer closes, we will focus back to the previous view. previous_focus_handle: Option, - active_drawer: Option, active_modals: Vec, pub notification: View, view: AnyView, } -type DrawerBuilder = Rc Drawer + 'static>; type ModelBuilder = Rc Modal + 'static>; -#[derive(Clone)] -struct ActiveDrawer { - focus_handle: FocusHandle, - builder: DrawerBuilder, -} - #[derive(Clone)] struct ActiveModal { focus_handle: FocusHandle, @@ -230,7 +165,6 @@ impl Root { pub fn new(view: AnyView, cx: &mut ViewContext) -> Self { Self { previous_focus_handle: None, - active_drawer: None, active_modals: Vec::new(), notification: cx.new_view(NotificationList::new), view, @@ -277,25 +211,6 @@ impl Root { Some(div().child(root.read(cx).notification.clone())) } - /// Render the Drawer layer. - pub fn render_drawer_layer(cx: &mut WindowContext) -> Option { - let root = cx - .window_handle() - .downcast::() - .and_then(|w| w.root_view(cx).ok()) - .expect("The window root view should be of type `ui::Root`."); - - if let Some(active_drawer) = root.read(cx).active_drawer.clone() { - let mut drawer = Drawer::new(cx); - drawer = (active_drawer.builder)(drawer, cx); - drawer.focus_handle = active_drawer.focus_handle.clone(); - - return Some(div().child(drawer)); - } - - None - } - /// Render the Modal layer. pub fn render_modal_layer(cx: &mut WindowContext) -> Option { let root = cx @@ -347,12 +262,15 @@ 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") + .relative() + .size_full() + .font_family(".SystemUIFont") + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .child(self.view.clone()), + ) } } diff --git a/crates/ui/src/scroll/scrollable_mask.rs b/crates/ui/src/scroll/scrollable_mask.rs index fc2bbe5..129dbc7 100644 --- a/crates/ui/src/scroll/scrollable_mask.rs +++ b/crates/ui/src/scroll/scrollable_mask.rs @@ -1,18 +1,10 @@ use gpui::{ - px, relative, AnyView, Bounds, ContentMask, Corners, Edges, Element, ElementId, + px, relative, Axis, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point, - Position, ScrollHandle, ScrollWheelEvent, Style, WindowContext, + Position, ScrollHandle, ScrollWheelEvent, Size, Style, WindowContext, }; -/// The scroll axis direction. -#[allow(dead_code)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ScrollableAxis { - /// Horizontal scroll. - Horizontal, - /// Vertical scroll. - Vertical, -} +use crate::AxisExt; /// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. /// @@ -20,21 +12,17 @@ pub enum ScrollableAxis { /// You can use this `scroll_handle` to control what you want to scroll. /// This is only can handle once axis scrolling. pub struct ScrollableMask { - view: AnyView, - axis: ScrollableAxis, + view_id: EntityId, + axis: Axis, scroll_handle: ScrollHandle, debug: Option, } impl ScrollableMask { /// Create a new scrollable mask element. - pub fn new( - view: impl Into, - axis: ScrollableAxis, - scroll_handle: &ScrollHandle, - ) -> Self { + pub fn new(view_id: EntityId, axis: Axis, scroll_handle: &ScrollHandle) -> Self { Self { - view: view.into(), + view_id, scroll_handle: scroll_handle.clone(), axis, debug: None, @@ -75,7 +63,7 @@ impl Element for ScrollableMask { position: Position::Absolute, flex_grow: 1.0, flex_shrink: 1.0, - size: gpui::Size { + size: Size { width: relative(1.).into(), height: relative(1.).into(), }, @@ -127,32 +115,37 @@ impl Element for ScrollableMask { } cx.on_mouse_event({ + let view_id = self.view_id; + let is_horizontal = self.axis.is_horizontal(); + let scroll_handle = self.scroll_handle.clone(); let hitbox = hitbox.clone(); let mouse_position = cx.mouse_position(); - let scroll_handle = self.scroll_handle.clone(); - let old_offset = scroll_handle.offset(); - let view_id = self.view.entity_id(); - let is_horizontal = self.axis == ScrollableAxis::Horizontal; + let last_offset = scroll_handle.offset(); move |event: &ScrollWheelEvent, phase, cx| { if bounds.contains(&mouse_position) && phase.bubble() && hitbox.is_hovered(cx) { - let delta = event.delta.pixel_delta(line_height); + let mut offset = scroll_handle.offset(); + let mut delta = event.delta.pixel_delta(line_height); - if is_horizontal && !delta.x.is_zero() { - // When is horizontal scroll, move the horizontal scroll handle to make scrolling. - let mut offset = scroll_handle.offset(); + // Limit for only one way scrolling at same time. + // When use MacBook touchpad we may get both x and y delta, + // only allows the one that more to scroll. + if !delta.x.is_zero() && !delta.y.is_zero() { + if delta.x.abs() > delta.y.abs() { + delta.y = px(0.); + } else { + delta.x = px(0.); + } + } + + if is_horizontal { offset.x += delta.x; - scroll_handle.set_offset(offset); - } - - if !is_horizontal && !delta.y.is_zero() { - // When is vertical scroll, move the vertical scroll handle to make scrolling. - let mut offset = scroll_handle.offset(); + } else { offset.y += delta.y; - scroll_handle.set_offset(offset); } - if old_offset != scroll_handle.offset() { + if last_offset != offset { + scroll_handle.set_offset(offset); cx.notify(Some(view_id)); cx.stop_propagation(); } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index f51fce4..d493066 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -154,6 +154,7 @@ pub struct ThemeColor { pub accordion_hover: Hsla, pub background: Hsla, pub border: Hsla, + pub window_border: Hsla, pub card: Hsla, pub card_foreground: Hsla, pub destructive: Hsla, @@ -229,6 +230,7 @@ impl ThemeColor { accordion_hover: hsl(240.0, 4.8, 95.9).opacity(0.7), background: hsl(0.0, 0.0, 100.), border: hsl(240.0, 5.9, 90.0), + window_border: hsl(240.0, 5.9, 78.0), card: hsl(0.0, 0.0, 100.0), card_foreground: hsl(240.0, 10.0, 3.9), destructive: hsl(0.0, 84.2, 60.2), @@ -304,6 +306,7 @@ impl ThemeColor { accordion_hover: hsl(240.0, 3.7, 15.9).opacity(0.7), background: hsl(0.0, 0.0, 8.0), border: hsl(240.0, 3.7, 16.9), + window_border: hsl(240.0, 3.7, 28.0), card: hsl(0.0, 0.0, 8.0), card_foreground: hsl(0.0, 0.0, 78.0), destructive: hsl(0.0, 62.8, 30.6), diff --git a/crates/ui/src/title_bar.rs b/crates/ui/src/title_bar.rs index 8643609..e016f1f 100644 --- a/crates/ui/src/title_bar.rs +++ b/crates/ui/src/title_bar.rs @@ -1,13 +1,15 @@ +use gpui::{ + div, prelude::FluentBuilder as _, px, relative, AnyElement, ClickEvent, Div, Element, Hsla, + InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Stateful, + StatefulInteractiveElement as _, Style, Styled, WindowContext, +}; 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, - StatefulInteractiveElement as _, Style, Styled, WindowContext, -}; +pub const HEIGHT: Pixels = px(34.); pub const TITLE_BAR_HEIGHT: Pixels = px(35.); + #[cfg(target_os = "macos")] const TITLE_BAR_LEFT_PADDING: Pixels = px(80.); #[cfg(not(target_os = "macos"))] @@ -158,7 +160,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(), @@ -225,45 +231,41 @@ impl RenderOnce for TitleBar { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let is_linux = cfg!(target_os = "linux"); - const HEIGHT: Pixels = px(34.); - - div() - .flex_shrink_0() - .child( - self.base - .flex() - .flex_row() - .items_center() - .justify_between() - .h(HEIGHT) - .bg(cx.theme().title_bar) - .border_b_1() - .border_color(cx.theme().title_bar_border.opacity(0.7)) - .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.opacity(0.7)) + .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, + }), + ) } } diff --git a/crates/ui/src/window_border.rs b/crates/ui/src/window_border.rs new file mode 100644 index 0000000..ff098e1 --- /dev/null +++ b/crates/ui/src/window_border.rs @@ -0,0 +1,201 @@ +// 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, Edges, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, + Pixels, Point, RenderOnce, ResizeEdge, Size, Styled as _, WindowContext, +}; + +use crate::theme::ActiveTheme; + +#[cfg(not(target_os = "linux"))] +pub(crate) const SHADOW_SIZE: Pixels = Pixels(0.0); +#[cfg(target_os = "linux")] +pub(crate) const SHADOW_SIZE: Pixels = Pixels(12.0); + +pub(crate) const BORDER_SIZE: Pixels = Pixels(1.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, +} + +/// Get the window paddings. +pub fn window_paddings(cx: &WindowContext) -> Edges { + match cx.window_decorations() { + Decorations::Server => Edges::all(px(0.0)), + Decorations::Client { tiling } => { + let mut paddings = Edges::all(SHADOW_SIZE); + 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, 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(); + + if let Some(edge) = resize_edge(pos, SHADOW_SIZE, size) { + cx.start_window_resize(edge) + }; + }), + }) + .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) +}