diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 334f1214..10e2cf01 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -3,7 +3,7 @@ use prelude::FluentBuilder as _; use private::serde::Deserialize; use story::{ ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory, - PickerStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, + ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory, TableStory, TextStory, TooltipStory, }; use workspace::{TitleBar, Workspace}; @@ -14,7 +14,7 @@ use ui::{ popover::Popover, popup_menu::PopupMenu, theme::{ActiveTheme, Theme}, - IconName, Sizable, + IconName, Root, Sizable, }; use crate::app_state::AppState; @@ -96,9 +96,9 @@ impl StoryWorkspace { .detach(); StoryContainer::add_pane( - "Picker", - "Picker is a component that allows the user to select an item from a list of options.", - PickerStory::view(cx).into(), + "Modal", + "Modal & Drawer use examples", + ModalStory::view(cx).into(), workspace.clone(), cx, ) @@ -212,7 +212,7 @@ impl StoryWorkspace { pub fn new_local( app_state: Arc, cx: &mut AppContext, - ) -> Task>> { + ) -> Task>> { let window_bounds = Bounds::centered(None, size(px(1600.0), px(1200.0)), cx); cx.spawn(|mut cx| async move { @@ -233,7 +233,8 @@ impl StoryWorkspace { let window = cx.open_window(options, |cx| { let workspace = cx.new_view(|cx| Workspace::new(None, cx)); - cx.new_view(|cx| Self::new(app_state.clone(), workspace, cx)) + let story_view = cx.new_view(|cx| Self::new(app_state.clone(), workspace, cx)); + cx.new_view(|cx| Root::new(story_view.into(), cx)) })?; window @@ -256,14 +257,13 @@ impl StoryWorkspace { pub fn open_new( app_state: Arc, cx: &mut AppContext, - init: impl FnOnce(&mut StoryWorkspace, &mut ViewContext) + 'static + Send, + init: impl FnOnce(&mut Root, &mut ViewContext) + 'static + Send, ) -> Task<()> { - let task: Task, anyhow::Error>> = + let task: Task, anyhow::Error>> = StoryWorkspace::new_local(app_state, cx); cx.spawn(|mut cx| async move { - if let Some(workspace) = task.await.ok() { - workspace - .update(&mut cx, |workspace, cx| init(workspace, cx)) + if let Some(root) = task.await.ok() { + root.update(&mut cx, |workspace, cx| init(workspace, cx)) .expect("failed to init workspace"); } }) diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 0c010aa3..d1359fbf 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -5,7 +5,7 @@ mod icon_story; mod image_story; mod input_story; mod list_story; -mod picker_story; +mod modal_story; mod popup_story; mod progress_story; mod resizable_story; @@ -23,7 +23,7 @@ pub use icon_story::IconStory; pub use image_story::ImageStory; pub use input_story::InputStory; pub use list_story::ListStory; -pub use picker_story::PickerStory; +pub use modal_story::ModalStory; pub use popup_story::PopupStory; pub use progress_story::ProgressStory; pub use resizable_story::ResizableStory; diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index 4ddfd6dc..22d83b82 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -246,7 +246,6 @@ fn random_company() -> Company { Company { name: fake::faker::company::en::CompanyName().fake(), industry: fake::faker::company::en::Industry().fake(), - // description: fake::faker::lorem::en::Paragraph(3..5).fake(), last_done, prev_close, } @@ -260,15 +259,14 @@ impl FocusableView for ListStory { impl Render for ListStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - h_flex() + div() .track_focus(&self.focus_handle) .on_action(cx.listener(Self::selected_company)) .size_full() .gap_4() - .mb_4() .border_1() .border_color(cx.theme().border) .rounded_md() - .child(v_flex().h_full().w_full().child(self.company_list.clone())) + .child(self.company_list.clone()) } } diff --git a/crates/story/src/picker_story.rs b/crates/story/src/modal_story.rs similarity index 58% rename from crates/story/src/picker_story.rs rename to crates/story/src/modal_story.rs index 24e98a4c..72c9b318 100644 --- a/crates/story/src/picker_story.rs +++ b/crates/story/src/modal_story.rs @@ -2,21 +2,22 @@ use std::{sync::Arc, time::Duration}; use fake::Fake; use gpui::{ - deferred, div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, FocusHandle, - FocusableView, InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Task, - Timer, View, ViewContext, VisualContext as _, WeakView, WindowContext, + div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, IntoElement, ParentElement, + Render, Styled, Task, Timer, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use ui::{ button::{Button, ButtonStyle}, + date_picker::DatePicker, h_flex, + input::TextInput, list::{List, ListDelegate, ListItem}, theme::ActiveTheme as _, - v_flex, Icon, IconName, StyledExt, + v_flex, ContextModal as _, Icon, IconName, Placement, }; pub struct ListItemDeletegate { - story: WeakView, + story: WeakView, confirmed_index: Option, selected_index: Option, items: Vec>, @@ -108,10 +109,7 @@ impl ListDelegate for ListItemDeletegate { fn cancel(&mut self, cx: &mut ViewContext>) { if let Some(story) = self.story.upgrade() { - cx.update_view(&story, |story, cx| { - story.open = false; - cx.notify(); - }); + cx.update_view(&story, |story, cx| story.close_drawer(cx)); } } @@ -124,8 +122,7 @@ impl ListDelegate for ListItemDeletegate { story.selected_value = Some(item.clone()); } } - story.open = false; - cx.notify(); + cx.close_drawer(); }); } } @@ -139,13 +136,16 @@ impl ListDelegate for ListItemDeletegate { } } -pub struct PickerStory { - list: View>, - open: bool, +pub struct ModalStory { + focus_handle: FocusHandle, + drawer_placement: Option, selected_value: Option>, + list: View>, + input1: View, + date_picker: View, } -impl PickerStory { +impl ModalStory { pub fn view(cx: &mut WindowContext) -> View { cx.new_view(Self::new) } @@ -220,79 +220,126 @@ impl PickerStory { list }); + let input1 = cx.new_view(|cx| TextInput::new(cx).placeholder("Your Name")); + let date_picker = + cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth")); + Self { - list, - open: false, + focus_handle: cx.focus_handle(), + drawer_placement: None, selected_value: None, + list, + input1, + date_picker, } } -} -impl FocusableView for PickerStory { - fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { - self.list.focus_handle(cx) - } -} + fn open_drawer_at(&mut self, placement: Placement, cx: &mut ViewContext) { + let input = self.input1.clone(); + let date_picker = self.date_picker.clone(); + let list = self.list.clone(); -impl Render for PickerStory { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_flex() - .gap_6() - .child( - v_flex().items_start().child( - Button::new("show-picker", cx) - .label("Show Picker...") - .icon(IconName::Search) - .on_click(cx.listener(|this, _, cx| { - this.open = !this.open; - this.list.focus_handle(cx).focus(cx); - cx.notify(); - })), - ), - ) - .when_some(self.selected_value.clone(), |this, selected_value| { - this.child( - h_flex().gap_1().child("You have selected:").child( - div() - .child(selected_value.to_string()) - .text_color(gpui::red()), - ), - ) - }) - .when(self.open, |this| { - this.child(deferred( + let list_h = match placement { + Placement::Left | Placement::Right => px(400.), + Placement::Top | Placement::Bottom => px(160.), + }; + + cx.open_drawer(move |this, cx| { + this.margin_top(px(33.)) + .placement(placement) + .size(px(400.)) + .title("Drawer Title") + .gap_4() + .child(input.clone()) + .child(date_picker.clone()) + .child( div() - .absolute() + .border_1() + .border_color(cx.theme().border) + .rounded_md() .size_full() - .top_0() - .left_0() + .flex_1() + .h(list_h) + .child(list.clone()), + ) + .footer( + h_flex() + .gap_6() + .items_center() .child( - v_flex().flex().flex_col().items_center().child( - v_flex() - .occlude() - .w(px(450.)) - .h(px(350.)) - .bg(cx.theme().popover) - .border_1() - .border_color(cx.theme().border) - .shadow_lg() - .rounded_lg() - .child(self.list.clone()) - .on_mouse_down_out(cx.listener(|this, _, cx| { - this.open = false; - cx.notify(); - })), - ), + Button::new("confirm", cx) + .primary() + .label("Confirm") + .on_click(|_, cx| { + cx.close_drawer(); + }), ) - .with_animation( - "slide-down", - Animation::new(Duration::from_secs_f64(0.15)), - move |this, delta| { - let y = px(-10.) + delta * px(10.); - this.top(y).opacity((1.0 * delta + 0.3).min(1.0)) - }, - ), - )) - }) + .child(Button::new("cancel", cx).label("Cancel").on_click(|_, cx| { + cx.close_drawer(); + })), + ) + }); + } + + fn close_drawer(&mut self, cx: &mut ViewContext) { + self.drawer_placement = None; + cx.notify(); + } +} + +impl FocusableView for ModalStory { + fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for ModalStory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div().child( + v_flex() + .gap_6() + .child( + h_flex() + .items_start() + .gap_3() + .child( + Button::new("show-drawer-left", cx) + .label("Left Drawer...") + .on_click(cx.listener(|this, _, cx| { + this.open_drawer_at(Placement::Left, cx) + })), + ) + .child( + Button::new("show-drawer-top", cx) + .label("Top Drawer...") + .on_click(cx.listener(|this, _, cx| { + this.open_drawer_at(Placement::Top, cx) + })), + ) + .child( + Button::new("show-drawer", cx) + .label("Right Drawer...") + .on_click(cx.listener(|this, _, cx| { + this.open_drawer_at(Placement::Right, cx) + })), + ) + .child( + Button::new("show-drawer", cx) + .label("Bottom Drawer...") + .on_click(cx.listener(|this, _, cx| { + this.open_drawer_at(Placement::Bottom, cx) + })), + ), + ) + .when_some(self.selected_value.clone(), |this, selected_value| { + this.child( + h_flex().gap_1().child("You have selected:").child( + div() + .child(selected_value.to_string()) + .text_color(gpui::red()), + ), + ) + }), + ) } } diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index 08bf5a0f..81b5e49e 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -2,7 +2,7 @@ use std::cell::Cell; use std::rc::Rc; use gpui::{ - canvas, div, px, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle, + canvas, div, px, Entity, InteractiveElement, ParentElement, Pixels, Render, ScrollHandle, StatefulInteractiveElement as _, Styled, View, ViewContext, VisualContext, WindowContext, }; use ui::button::Button; @@ -65,6 +65,7 @@ impl Render for ScrollableStory { let view = cx.view().clone(); v_flex() + .size_full() .gap_4() .child( h_flex() @@ -170,7 +171,7 @@ impl Render for ScrollableStory { .bottom_0() .child( Scrollbar::both( - view, + view.entity_id(), self.scroll_state.clone(), self.scroll_handle.clone(), self.scroll_size, @@ -189,11 +190,12 @@ impl Render for ScrollableStory { .border_1() .border_color(cx.theme().border) .w_full() - .h(px(200.)) + .flex_1() + .overflow_hidden() .child( v_flex() .id("test-1") - .scrollable(cx.view().clone(), ScrollbarAxis::Vertical) + .scrollable(cx.view().entity_id(), ScrollbarAxis::Vertical) .focusable() .p_3() .w(test_width) diff --git a/crates/ui/src/drawer.rs b/crates/ui/src/drawer.rs new file mode 100644 index 00000000..f81349a1 --- /dev/null +++ b/crates/ui/src/drawer.rs @@ -0,0 +1,220 @@ +use std::{rc::Rc, time::Duration}; + +use gpui::{ + anchored, div, hsla, point, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, + AnyElement, ClickEvent, DefiniteLength, DismissEvent, Div, EventEmitter, FocusHandle, + InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Styled, + WindowContext, +}; + +use crate::{ + button::Button, h_flex, root::ContextModal as _, scroll::ScrollbarAxis, theme::ActiveTheme, + v_flex, IconName, Placement, Sizable, StyledExt as _, +}; + +#[derive(IntoElement)] +pub struct Drawer { + focus_handle: FocusHandle, + placement: Placement, + size: DefiniteLength, + resizable: bool, + on_close: Rc, + title: Option, + footer: Option, + content: Div, + margin_top: Pixels, +} + +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(), + margin_top: px(0.), + 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 + } + + /// 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 focus_handle = self.focus_handle.clone(); + let placement = self.placement; + let titlebar_height = self.margin_top; + let size = cx.viewport_size(); + let on_close = self.on_close.clone(); + + let overlay_color = if cx.theme().mode.is_dark() { + hsla(0., 1., 1., 0.06) + } else { + hsla(0., 0., 0., 0.06) + }; + + anchored() + .position(point(px(0.), titlebar_height)) + .snap_to_window() + .child( + div() + .occlude() + .w(size.width) + .h(size.height - titlebar_height) + .bg(overlay_color) + .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("") + .track_focus(&focus_handle) + .absolute() + .occlude() + .bg(cx.theme().background) + .border_color(cx.theme().border) + .shadow_xl() + .map(|this| { + // Set the size of the drawer. + if placement.is_vertical() { + 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_3() + .w_full() + .child(self.title.unwrap_or(div().into_any_element())) + .child( + Button::new("close", cx) + .small() + .ghost() + .icon(IconName::Close) + .on_click(move |_, cx| { + on_close(&ClickEvent::default(), cx); + cx.close_drawer(); + }), + ), + ) + .child( + div().flex_1().overflow_hidden().child( + v_flex() + .p_4() + .pt_0() + .scrollable( + cx.parent_view_id().unwrap_or_default(), + ScrollbarAxis::Vertical, + ) + .child(self.content), + ), + ) + .when_some(self.footer, |this, 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 523aca29..fbb7f065 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -3,6 +3,7 @@ mod event; mod focusable; mod icon; +mod root; mod styled; mod svg_img; mod time; @@ -12,6 +13,7 @@ pub mod checkbox; pub mod clipboard; pub mod context_menu; pub mod divider; +pub mod drawer; pub mod dropdown; pub mod indicator; pub mod input; @@ -34,14 +36,13 @@ pub mod theme; pub mod tooltip; pub mod webview; -use std::ops::Deref; - // re-export pub use wry; pub use crate::Disableable; pub use event::InteractiveElementExt; pub use focusable::FocusableCycle; +pub use root::{ContextModal, Root}; pub use styled::*; pub use time::*; @@ -49,8 +50,6 @@ pub use colors::*; pub use icon::*; pub use svg_img::*; -rust_i18n::i18n!("locales", fallback = "en"); - /// Initialize the UI module. pub fn init(cx: &mut gpui::AppContext) { input::init(cx); @@ -64,6 +63,8 @@ pub fn init(cx: &mut gpui::AppContext) { webview::init(cx) } +rust_i18n::i18n!("locales", fallback = "en"); +use std::ops::Deref; pub fn locale() -> impl Deref { rust_i18n::locale() } diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 1e1141e3..bff5e605 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -11,7 +11,7 @@ use gpui::{ InteractiveElement, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton, ParentElement, Render, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, }; -use gpui::{SharedString, WindowContext}; +use gpui::{Entity, SharedString, WindowContext}; use smol::Timer; actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]); @@ -167,7 +167,7 @@ where } Some(Scrollbar::uniform_scroll( - cx.view().clone(), + cx.view().entity_id(), self.scrollbar_state.clone(), self.vertical_scroll_handle.clone(), self.delegate.items_count(), diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs new file mode 100644 index 00000000..d2348c06 --- /dev/null +++ b/crates/ui/src/root.rs @@ -0,0 +1,89 @@ +use gpui::{ + div, prelude::FluentBuilder as _, AnyView, ParentElement as _, Render, Styled, ViewContext, + WindowContext, +}; +use std::{ops::DerefMut, rc::Rc}; + +use crate::{drawer::Drawer, theme::ActiveTheme}; + +/// 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; + + /// Closes the active drawer. + fn close_drawer(&mut self); +} + +impl<'a> ContextModal for WindowContext<'a> { + fn open_drawer(&mut self, build: F) + where + F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static, + { + Root::update_root(self, move |root, cx| { + root.active_drawer = Some(Rc::new(build)); + cx.notify(); + }) + } + + fn close_drawer(&mut self) { + Root::update_root(self, |root, cx| { + root.active_drawer = None; + cx.notify(); + }) + } +} +impl<'a, V> ContextModal for ViewContext<'a, V> { + fn open_drawer(&mut self, build: F) + where + F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static, + { + self.deref_mut().open_drawer(build) + } + + fn close_drawer(&mut self) { + self.deref_mut().close_drawer() + } +} + +pub struct Root { + active_drawer: Option Drawer + 'static>>, + root_view: AnyView, +} + +impl Root { + pub fn new(root_view: AnyView, _cx: &mut ViewContext) -> Self { + Self { + active_drawer: None, + root_view, + } + } + + fn update_root(cx: &mut WindowContext, f: F) + where + F: FnOnce(&mut Self, &mut ViewContext) + 'static, + { + 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`."); + + root.update(cx, |root, cx| f(root, cx)) + } +} + +impl Render for Root { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + div() + .size_full() + .text_color(cx.theme().foreground) + .child(self.root_view.clone()) + .when_some(self.active_drawer.clone(), |this, build| { + let drawer = Drawer::new(cx); + this.child(build(drawer, cx)) + }) + } +} diff --git a/crates/ui/src/scroll/scrollable.rs b/crates/ui/src/scroll/scrollable.rs index 2d83ab36..d3915f40 100644 --- a/crates/ui/src/scroll/scrollable.rs +++ b/crates/ui/src/scroll/scrollable.rs @@ -2,7 +2,7 @@ use std::{cell::Cell, rc::Rc}; use super::{Scrollbar, ScrollbarAxis, ScrollbarState}; use gpui::{ - canvas, div, relative, AnyElement, AnyView, Div, Element, ElementId, GlobalElementId, + canvas, div, relative, AnyElement, Div, Element, ElementId, EntityId, GlobalElementId, InteractiveElement, IntoElement, ParentElement, Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement, Style, StyleRefinement, Styled, WindowContext, }; @@ -11,7 +11,7 @@ use gpui::{ pub struct Scrollable { id: ElementId, element: Option, - view: AnyView, + view_id: EntityId, axis: ScrollbarAxis, /// This is a fake element to handle Styled, InteractiveElement, not used. _element: Stateful
, @@ -21,11 +21,10 @@ impl Scrollable where E: Element, { - pub(crate) fn new(element: E, view: impl Into, axis: ScrollbarAxis) -> Self { - let view: AnyView = view.into(); + pub(crate) fn new(view_id: EntityId, element: E, axis: ScrollbarAxis) -> Self { let id = ElementId::Name(SharedString::from(format!( "ScrollView:{}-{:?}", - view.entity_id(), + view_id, element.id(), ))); @@ -33,7 +32,7 @@ where element: Some(element), _element: div().id("fake"), id, - view, + view_id, axis, } } @@ -158,7 +157,7 @@ where style.size.height = relative(1.0).into(); let axis = self.axis; - let view = self.view.clone(); + let view_id = self.view_id; let scroll_id = self.id.clone(); let content = self.element.take().map(|c| c.into_any_element()); @@ -171,6 +170,7 @@ where let mut element = div() .relative() .size_full() + .overflow_hidden() .child( div() .id(scroll_id) @@ -193,7 +193,7 @@ where .right_0() .bottom_0() .child( - Scrollbar::both(view, state, handle.clone(), scroll_size.get()) + Scrollbar::both(view_id, state, handle.clone(), scroll_size.get()) .axis(axis), ), ) diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 1320d41d..f7905a96 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -2,7 +2,7 @@ use std::{cell::Cell, rc::Rc}; use crate::theme::ActiveTheme; use gpui::{ - fill, point, px, relative, size, AnyView, Bounds, ContentMask, Edges, Element, Hitbox, + fill, point, px, relative, size, Bounds, ContentMask, Edges, Element, EntityId, Hitbox, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, Style, UniformListScrollHandle, }; @@ -140,7 +140,7 @@ impl ScrollbarAxis { /// Scrollbar control for scroll-area or a uniform-list. pub struct Scrollbar { - view: AnyView, + view_id: EntityId, axis: ScrollbarAxis, /// When is vertical, this is the height of the scrollbar. width: Pixels, @@ -151,14 +151,14 @@ pub struct Scrollbar { impl Scrollbar { fn new( - view: AnyView, + view_id: EntityId, state: Rc>, axis: ScrollbarAxis, scroll_handle: impl ScrollHandleOffsetable + 'static, scroll_size: gpui::Size, ) -> Self { Self { - view, + view_id, state, axis, scroll_size, @@ -169,13 +169,13 @@ impl Scrollbar { /// Create with vertical and horizontal scrollbar. pub fn both( - view: impl Into, + view_id: EntityId, state: Rc>, scroll_handle: impl ScrollHandleOffsetable + 'static, scroll_size: gpui::Size, ) -> Self { Self::new( - view.into(), + view_id, state, ScrollbarAxis::Both, scroll_handle, @@ -185,13 +185,13 @@ impl Scrollbar { /// Create with horizontal scrollbar. pub fn horizontal( - view: impl Into, + view_id: EntityId, state: Rc>, scroll_handle: impl ScrollHandleOffsetable + 'static, scroll_size: gpui::Size, ) -> Self { Self::new( - view.into(), + view_id, state, ScrollbarAxis::Horizontal, scroll_handle, @@ -201,13 +201,13 @@ impl Scrollbar { /// Create with vertical scrollbar. pub fn vertical( - view: impl Into, + view_id: EntityId, state: Rc>, scroll_handle: impl ScrollHandleOffsetable + 'static, scroll_size: gpui::Size, ) -> Self { Self::new( - view.into(), + view_id, state, ScrollbarAxis::Vertical, scroll_handle, @@ -217,7 +217,7 @@ impl Scrollbar { /// Create vertical scrollbar for uniform list. pub fn uniform_scroll( - view: impl Into, + view_id: EntityId, state: Rc>, scroll_handle: UniformListScrollHandle, items_count: usize, @@ -227,7 +227,7 @@ impl Scrollbar { let scroll_size = size(px(0.), max_height); Self::new( - view.into(), + view_id, state, ScrollbarAxis::Vertical, scroll_handle, @@ -436,7 +436,7 @@ impl Element for Scrollbar { cx.on_mouse_event({ let state = self.state.clone(); - let view_id = self.view.entity_id(); + let view_id = self.view_id; let scroll_handle = self.scroll_handle.clone(); move |event: &MouseDownEvent, phase, cx| { @@ -448,6 +448,7 @@ impl Element for Scrollbar { let pos = event.position - thumb_bounds.origin; state.set(state.get().with_drag_pos(axis, pos)); + cx.notify(view_id); } else { // click on the scrollbar, jump to the position @@ -481,18 +482,20 @@ impl Element for Scrollbar { cx.on_mouse_event({ let scroll_handle = self.scroll_handle.clone(); let state = self.state.clone(); - let view_id = self.view.entity_id(); + let view_id = self.view_id; move |event: &MouseMoveEvent, _, cx| { if bounds.contains(&event.position) { if state.get().hovered_axis != Some(axis) { state.set(state.get().with_hovered(Some(axis))); + cx.notify(view_id); } } else { if state.get().hovered_axis == Some(axis) { if state.get().hovered_axis.is_some() { state.set(state.get().with_hovered(None)); + cx.notify(view_id); } } @@ -541,12 +544,13 @@ impl Element for Scrollbar { }); cx.on_mouse_event({ - let view_id = self.view.entity_id(); + let view_id = self.view_id; let state = self.state.clone(); move |_event: &MouseUpEvent, phase, cx| { if phase.bubble() { state.set(state.get().with_unset_drag_pos()); + cx.notify(view_id); } } diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index f140d2e5..b3ae86f9 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -3,7 +3,7 @@ use crate::{ theme::{ActiveTheme, Colorize}, }; use gpui::{ - div, px, rems, AnyView, Axis, Div, Element, Fill, FocusHandle, Pixels, Styled, WindowContext, + div, px, rems, Axis, Div, Element, EntityId, Fill, FocusHandle, Pixels, Styled, WindowContext, }; /// Returns a `Div` as horizontal flex layout. @@ -103,11 +103,11 @@ pub trait StyledExt: Styled + Sized { /// Wraps the element in a ScrollView. /// /// Current this is only have a vertical scrollbar. - fn scrollable(self, view: impl Into, axis: ScrollbarAxis) -> Scrollable + fn scrollable(self, view_id: EntityId, axis: ScrollbarAxis) -> Scrollable where Self: Element, { - Scrollable::new(self, view, axis) + Scrollable::new(view_id, self, axis) } font_weight!(font_thin, THIN); @@ -313,3 +313,27 @@ impl AxisExt for Axis { self == Axis::Vertical } } + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Placement { + Top, + Bottom, + Left, + Right, +} + +impl Placement { + pub fn is_horizontal(&self) -> bool { + match self { + Placement::Top | Placement::Bottom => true, + _ => false, + } + } + + pub fn is_vertical(&self) -> bool { + match self { + Placement::Left | Placement::Right => true, + _ => false, + } + } +} diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index 52332d3a..7cd7faf5 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -8,7 +8,7 @@ use crate::{ }; use gpui::{ actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, - DragMoveEvent, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext, VisualContext as _, WindowContext, @@ -339,7 +339,6 @@ where } fn render_scrollbar(&self, cx: &mut ViewContext) -> Option { - let view = cx.view().clone(); let state = self.scrollbar_state.clone(); Some( @@ -350,7 +349,7 @@ where .right_0() .bottom_0() .child(Scrollbar::uniform_scroll( - view, + cx.view().entity_id(), state, self.vertical_scroll_handle.clone(), self.delegate.rows_count(),