feat: update gpui-components

This commit is contained in:
2024-12-30 07:52:26 +07:00
parent 1c752accb2
commit 4628340459
9 changed files with 397 additions and 518 deletions

View File

@@ -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<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
#[derive(IntoElement)]
pub struct Drawer {
pub(crate) focus_handle: FocusHandle,
placement: Placement,
size: DefiniteLength,
resizable: bool,
on_close: OnClose,
title: Option<AnyElement>,
footer: Option<AnyElement>,
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<DefiniteLength>) -> 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<DismissEvent> for Drawer {}
impl ParentElement for Drawer {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
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),
})
},
),
),
)
}
}

View File

@@ -1,11 +1,3 @@
mod colors;
mod event;
mod focusable;
mod icon;
mod root;
mod styled;
mod title_bar;
pub mod accordion; pub mod accordion;
pub mod animation; pub mod animation;
pub mod badge; pub mod badge;
@@ -18,7 +10,6 @@ pub mod color_picker;
pub mod context_menu; pub mod context_menu;
pub mod divider; pub mod divider;
pub mod dock; pub mod dock;
pub mod drawer;
pub mod dropdown; pub mod dropdown;
pub mod history; pub mod history;
pub mod indicator; pub mod indicator;
@@ -45,14 +36,24 @@ pub mod theme;
pub mod tooltip; pub mod tooltip;
pub use crate::Disableable; pub use crate::Disableable;
pub use colors::*;
pub use event::InteractiveElementExt; pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle; pub use focusable::FocusableCycle;
pub use icon::*;
pub use root::{ContextModal, Root}; pub use root::{ContextModal, Root};
pub use styled::*; pub use styled::*;
pub use title_bar::*; pub use title_bar::*;
pub use window_border::{window_border, WindowBorder};
pub use colors::*; mod colors;
pub use icon::*; mod event;
mod focusable;
mod icon;
mod root;
mod styled;
mod title_bar;
mod window_border;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
@@ -67,7 +68,6 @@ pub struct Assets;
pub fn init(cx: &mut gpui::AppContext) { pub fn init(cx: &mut gpui::AppContext) {
theme::init(cx); theme::init(cx);
dock::init(cx); dock::init(cx);
drawer::init(cx);
dropdown::init(cx); dropdown::init(cx);
input::init(cx); input::init(cx);
number_input::init(cx); number_input::init(cx);

View File

@@ -1,11 +1,10 @@
use std::{rc::Rc, time::Duration};
use gpui::{ 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, AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla,
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
RenderOnce, SharedString, Styled, WindowContext, RenderOnce, SharedString, Styled, WindowContext,
}; };
use std::{rc::Rc, time::Duration};
use crate::{ use crate::{
animation::cubic_bezier, animation::cubic_bezier,
@@ -37,7 +36,6 @@ pub struct Modal {
show_close: bool, show_close: bool,
overlay: bool, overlay: bool,
keyboard: bool, keyboard: bool,
/// 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(crate) focus_handle: FocusHandle,
pub(crate) layer_ix: usize, pub(crate) layer_ix: usize,
@@ -164,7 +162,12 @@ impl RenderOnce for Modal {
fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement { fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement {
let layer_ix = self.layer_ix; let layer_ix = self.layer_ix;
let on_close = self.on_close.clone(); 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 { let bounds = Bounds {
origin: Point::default(), origin: Point::default(),
size: view_size, size: view_size,
@@ -173,7 +176,10 @@ impl RenderOnce for Modal {
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top; let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
let x = bounds.center().x - self.width / 2.; let x = bounds.center().x - self.width / 2.;
anchored().snap_to_window().child( anchored()
.position(point(window_paddings.left, window_paddings.top))
.snap_to_window()
.child(
div() div()
.occlude() .occlude()
.w(view_size.width) .w(view_size.width)
@@ -181,15 +187,13 @@ impl RenderOnce for Modal {
.when(self.overlay_visible, |this| { .when(self.overlay_visible, |this| {
this.bg(overlay_color(self.overlay, cx)) this.bg(overlay_color(self.overlay, cx))
}) })
.when(self.overlay, |this| { .on_mouse_down(MouseButton::Left, {
this.on_mouse_down(MouseButton::Left, {
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
move |_, cx| { move |_, cx| {
on_close(&ClickEvent::default(), cx); on_close(&ClickEvent::default(), cx);
cx.close_modal(); cx.close_modal();
} }
}) })
})
.child( .child(
self.base self.base
.id(SharedString::from(format!("modal-{layer_ix}"))) .id(SharedString::from(format!("modal-{layer_ix}")))
@@ -220,17 +224,21 @@ impl RenderOnce for Modal {
}) })
.when(self.show_close, |this| { .when(self.show_close, |this| {
this.child( this.child(
Button::new(SharedString::from(format!("modal-close-{layer_ix}"))) Button::new(SharedString::from(format!(
"modal-close-{layer_ix}"
)))
.absolute() .absolute()
.top_2() .top_2()
.right_2() .right_2()
.small() .small()
.ghost() .ghost()
.icon(IconName::Close) .icon(IconName::Close)
.on_click(move |_, cx| { .on_click(
move |_, cx| {
on_close(&ClickEvent::default(), cx); on_close(&ClickEvent::default(), cx);
cx.close_modal(); cx.close_modal();
}), },
),
) )
}) })
.child(self.content) .child(self.content)

View File

@@ -1,16 +1,6 @@
use std::cell::Cell; use gpui::*;
use std::ops::Deref; use prelude::FluentBuilder;
use std::rc::Rc; use std::{cell::Cell, ops::Deref, 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 crate::scroll::{Scrollbar, ScrollbarState}; use crate::scroll::{Scrollbar, ScrollbarState};
use crate::StyledExt; use crate::StyledExt;
@@ -386,9 +376,10 @@ impl PopupMenu {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let count = self.clickable_menu_items().count(); let count = self.clickable_menu_items().count();
if count > 0 { if count > 0 {
let last_ix = count.saturating_sub(1);
let ix = self let ix = self
.selected_index .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); .unwrap_or(0);
self.selected_index = Some(ix); self.selected_index = Some(ix);
@@ -399,10 +390,18 @@ impl PopupMenu {
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
let count = self.clickable_menu_items().count(); let count = self.clickable_menu_items().count();
if count > 0 { if count > 0 {
let last_ix = count.saturating_sub(1);
let ix = self let ix = self
.selected_index .selected_index
.map(|index| if index == count - 1 { 0 } else { index - 1 }) .map(|index| {
.unwrap_or(count - 1); if index == last_ix {
0
} else {
index.saturating_sub(1)
}
})
.unwrap_or(last_ix);
self.selected_index = Some(ix); self.selected_index = Some(ix);
cx.notify(); cx.notify();
} }
@@ -473,7 +472,9 @@ impl PopupMenu {
} }
impl FluentBuilder for PopupMenu {} impl FluentBuilder for PopupMenu {}
impl EventEmitter<DismissEvent> for PopupMenu {} impl EventEmitter<DismissEvent> for PopupMenu {}
impl FocusableView for PopupMenu { impl FocusableView for PopupMenu {
fn focus_handle(&self, _: &AppContext) -> FocusHandle { fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone() self.focus_handle.clone()

View File

@@ -1,9 +1,3 @@
use crate::{
drawer::Drawer,
modal::Modal,
notification::{Notification, NotificationList},
theme::ActiveTheme,
};
use gpui::{ use gpui::{
div, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, div, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, Styled,
View, ViewContext, VisualContext as _, WindowContext, View, ViewContext, VisualContext as _, WindowContext,
@@ -13,19 +7,15 @@ use std::{
rc::Rc, rc::Rc,
}; };
use crate::{
modal::Modal,
notification::{Notification, NotificationList},
theme::ActiveTheme,
window_border,
};
/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality. /// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
pub trait ContextModal: Sized { pub trait ContextModal: Sized {
/// Opens a Drawer.
fn open_drawer<F>(&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. /// Opens a Modal.
fn open_modal<F>(&mut self, build: F) fn open_modal<F>(&mut self, build: F)
where where
@@ -48,38 +38,6 @@ pub trait ContextModal: Sized {
} }
impl ContextModal for WindowContext<'_> { impl ContextModal for WindowContext<'_> {
fn open_drawer<F>(&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<F>(&mut self, build: F) fn open_modal<F>(&mut self, build: F)
where where
F: Fn(Modal, &mut WindowContext) -> Modal + 'static, F: Fn(Modal, &mut WindowContext) -> Modal + 'static,
@@ -149,21 +107,10 @@ impl ContextModal for WindowContext<'_> {
} }
} }
impl<V> ContextModal for ViewContext<'_, V> { impl<V> ContextModal for ViewContext<'_, V> {
fn open_drawer<F>(&mut self, build: F)
where
F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static,
{
self.deref_mut().open_drawer(build)
}
fn has_active_modal(&self) -> bool { fn has_active_modal(&self) -> bool {
self.deref().has_active_modal() self.deref().has_active_modal()
} }
fn close_drawer(&mut self) {
self.deref_mut().close_drawer()
}
fn open_modal<F>(&mut self, build: F) fn open_modal<F>(&mut self, build: F)
where where
F: Fn(Modal, &mut WindowContext) -> Modal + 'static, F: Fn(Modal, &mut WindowContext) -> Modal + 'static,
@@ -171,10 +118,6 @@ impl<V> ContextModal for ViewContext<'_, V> {
self.deref_mut().open_modal(build) self.deref_mut().open_modal(build)
} }
fn has_active_drawer(&self) -> bool {
self.deref().has_active_drawer()
}
/// Close the last active modal. /// Close the last active modal.
fn close_modal(&mut self) { fn close_modal(&mut self) {
self.deref_mut().close_modal() self.deref_mut().close_modal()
@@ -205,21 +148,13 @@ pub struct Root {
/// Used to store the focus handle of the previous view. /// Used to store the focus handle of the previous view.
/// When the Modal, Drawer closes, we will focus back to the previous view. /// When the Modal, Drawer closes, we will focus back to the previous view.
previous_focus_handle: Option<FocusHandle>, previous_focus_handle: Option<FocusHandle>,
active_drawer: Option<ActiveDrawer>,
active_modals: Vec<ActiveModal>, active_modals: Vec<ActiveModal>,
pub notification: View<NotificationList>, pub notification: View<NotificationList>,
view: AnyView, view: AnyView,
} }
type DrawerBuilder = Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>;
type ModelBuilder = Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>; type ModelBuilder = Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>;
#[derive(Clone)]
struct ActiveDrawer {
focus_handle: FocusHandle,
builder: DrawerBuilder,
}
#[derive(Clone)] #[derive(Clone)]
struct ActiveModal { struct ActiveModal {
focus_handle: FocusHandle, focus_handle: FocusHandle,
@@ -230,7 +165,6 @@ impl Root {
pub fn new(view: AnyView, cx: &mut ViewContext<Self>) -> Self { pub fn new(view: AnyView, cx: &mut ViewContext<Self>) -> Self {
Self { Self {
previous_focus_handle: None, previous_focus_handle: None,
active_drawer: None,
active_modals: Vec::new(), active_modals: Vec::new(),
notification: cx.new_view(NotificationList::new), notification: cx.new_view(NotificationList::new),
view, view,
@@ -277,25 +211,6 @@ impl Root {
Some(div().child(root.read(cx).notification.clone())) Some(div().child(root.read(cx).notification.clone()))
} }
/// Render the Drawer layer.
pub fn render_drawer_layer(cx: &mut WindowContext) -> Option<impl IntoElement> {
let root = cx
.window_handle()
.downcast::<Root>()
.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. /// Render the Modal layer.
pub fn render_modal_layer(cx: &mut WindowContext) -> Option<impl IntoElement> { pub fn render_modal_layer(cx: &mut WindowContext) -> Option<impl IntoElement> {
let root = cx let root = cx
@@ -347,12 +262,15 @@ impl Render for Root {
let base_font_size = cx.theme().font_size; let base_font_size = cx.theme().font_size;
cx.set_rem_size(base_font_size); cx.set_rem_size(base_font_size);
window_border().child(
div() div()
.id("root") .id("root")
.relative()
.size_full() .size_full()
.font_family(".SystemUIFont") .font_family(".SystemUIFont")
.bg(cx.theme().background) .bg(cx.theme().background)
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.child(self.view.clone()) .child(self.view.clone()),
)
} }
} }

View File

@@ -1,18 +1,10 @@
use gpui::{ 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, 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. use crate::AxisExt;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollableAxis {
/// Horizontal scroll.
Horizontal,
/// Vertical scroll.
Vertical,
}
/// Make a scrollable mask element to cover the parent view with the mouse wheel event listening. /// 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. /// You can use this `scroll_handle` to control what you want to scroll.
/// This is only can handle once axis scrolling. /// This is only can handle once axis scrolling.
pub struct ScrollableMask { pub struct ScrollableMask {
view: AnyView, view_id: EntityId,
axis: ScrollableAxis, axis: Axis,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
debug: Option<Hsla>, debug: Option<Hsla>,
} }
impl ScrollableMask { impl ScrollableMask {
/// Create a new scrollable mask element. /// Create a new scrollable mask element.
pub fn new( pub fn new(view_id: EntityId, axis: Axis, scroll_handle: &ScrollHandle) -> Self {
view: impl Into<AnyView>,
axis: ScrollableAxis,
scroll_handle: &ScrollHandle,
) -> Self {
Self { Self {
view: view.into(), view_id,
scroll_handle: scroll_handle.clone(), scroll_handle: scroll_handle.clone(),
axis, axis,
debug: None, debug: None,
@@ -75,7 +63,7 @@ impl Element for ScrollableMask {
position: Position::Absolute, position: Position::Absolute,
flex_grow: 1.0, flex_grow: 1.0,
flex_shrink: 1.0, flex_shrink: 1.0,
size: gpui::Size { size: Size {
width: relative(1.).into(), width: relative(1.).into(),
height: relative(1.).into(), height: relative(1.).into(),
}, },
@@ -127,32 +115,37 @@ impl Element for ScrollableMask {
} }
cx.on_mouse_event({ 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 hitbox = hitbox.clone();
let mouse_position = cx.mouse_position(); let mouse_position = cx.mouse_position();
let scroll_handle = self.scroll_handle.clone(); let last_offset = scroll_handle.offset();
let old_offset = scroll_handle.offset();
let view_id = self.view.entity_id();
let is_horizontal = self.axis == ScrollableAxis::Horizontal;
move |event: &ScrollWheelEvent, phase, cx| { move |event: &ScrollWheelEvent, phase, cx| {
if bounds.contains(&mouse_position) && phase.bubble() && hitbox.is_hovered(cx) { if bounds.contains(&mouse_position) && phase.bubble() && hitbox.is_hovered(cx) {
let 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(); let mut offset = scroll_handle.offset();
let mut delta = event.delta.pixel_delta(line_height);
// 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; offset.x += delta.x;
scroll_handle.set_offset(offset); } else {
}
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();
offset.y += delta.y; 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.notify(Some(view_id));
cx.stop_propagation(); cx.stop_propagation();
} }

View File

@@ -154,6 +154,7 @@ pub struct ThemeColor {
pub accordion_hover: Hsla, pub accordion_hover: Hsla,
pub background: Hsla, pub background: Hsla,
pub border: Hsla, pub border: Hsla,
pub window_border: Hsla,
pub card: Hsla, pub card: Hsla,
pub card_foreground: Hsla, pub card_foreground: Hsla,
pub destructive: Hsla, pub destructive: Hsla,
@@ -229,6 +230,7 @@ impl ThemeColor {
accordion_hover: hsl(240.0, 4.8, 95.9).opacity(0.7), accordion_hover: hsl(240.0, 4.8, 95.9).opacity(0.7),
background: hsl(0.0, 0.0, 100.), background: hsl(0.0, 0.0, 100.),
border: hsl(240.0, 5.9, 90.0), 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: hsl(0.0, 0.0, 100.0),
card_foreground: hsl(240.0, 10.0, 3.9), card_foreground: hsl(240.0, 10.0, 3.9),
destructive: hsl(0.0, 84.2, 60.2), 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), accordion_hover: hsl(240.0, 3.7, 15.9).opacity(0.7),
background: hsl(0.0, 0.0, 8.0), background: hsl(0.0, 0.0, 8.0),
border: hsl(240.0, 3.7, 16.9), 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: hsl(0.0, 0.0, 8.0),
card_foreground: hsl(0.0, 0.0, 78.0), card_foreground: hsl(0.0, 0.0, 78.0),
destructive: hsl(0.0, 62.8, 30.6), destructive: hsl(0.0, 62.8, 30.6),

View File

@@ -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 std::rc::Rc;
use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _}; 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.); pub const TITLE_BAR_HEIGHT: Pixels = px(35.);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
const TITLE_BAR_LEFT_PADDING: Pixels = px(80.); const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -158,7 +160,11 @@ impl RenderOnce for ControlIcon {
.items_center() .items_center()
.text_color(fg) .text_color(fg)
.when(is_linux, |this| { .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::Minimize => cx.minimize_window(),
Self::Restore => cx.zoom_window(), Self::Restore => cx.zoom_window(),
Self::Maximize => cx.zoom_window(), Self::Maximize => cx.zoom_window(),
@@ -225,20 +231,16 @@ impl RenderOnce for TitleBar {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let is_linux = cfg!(target_os = "linux"); let is_linux = cfg!(target_os = "linux");
const HEIGHT: Pixels = px(34.); div().flex_shrink_0().child(
div()
.flex_shrink_0()
.child(
self.base self.base
.flex() .flex()
.flex_row() .flex_row()
.items_center() .items_center()
.justify_between() .justify_between()
.h(HEIGHT) .h(HEIGHT)
.bg(cx.theme().title_bar)
.border_b_1() .border_b_1()
.border_color(cx.theme().title_bar_border.opacity(0.7)) .border_color(cx.theme().title_bar_border.opacity(0.7))
.bg(cx.theme().title_bar)
.when(cx.is_fullscreen(), |this| this.pl(px(12.))) .when(cx.is_fullscreen(), |this| this.pl(px(12.)))
.on_double_click(|_, cx| cx.zoom_window()) .on_double_click(|_, cx| cx.zoom_window())
.child( .child(
@@ -247,12 +249,6 @@ impl RenderOnce for TitleBar {
.justify_between() .justify_between()
.flex_shrink_0() .flex_shrink_0()
.flex_1() .flex_1()
.children(self.children),
)
.child(WindowControls {
on_close_window: self.on_close_window,
}),
)
.when(is_linux, |this| { .when(is_linux, |this| {
this.child( this.child(
div() div()
@@ -264,6 +260,12 @@ impl RenderOnce for TitleBar {
.child(TitleBarElement {}), .child(TitleBarElement {}),
) )
}) })
.children(self.children),
)
.child(WindowControls {
on_close_window: self.on_close_window,
}),
)
} }
} }

View File

@@ -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<AnyElement>,
}
/// Get the window paddings.
pub fn window_paddings(cx: &WindowContext) -> Edges<Pixels> {
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<Item = AnyElement>) {
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<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)
}