Add Drawer (#137)

- Update scrollable to accept EntityId.

https://github.com/user-attachments/assets/a6c02890-3a6e-4b82-b5c5-3f0a48f35368
This commit is contained in:
Jason Lee 2024-08-13 16:01:52 +08:00 committed by GitHub
parent 7059165fa6
commit 63f333d8ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 520 additions and 136 deletions

View file

@ -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<AppState>,
cx: &mut AppContext,
) -> Task<anyhow::Result<WindowHandle<Self>>> {
) -> Task<anyhow::Result<WindowHandle<Root>>> {
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<AppState>,
cx: &mut AppContext,
init: impl FnOnce(&mut StoryWorkspace, &mut ViewContext<StoryWorkspace>) + 'static + Send,
init: impl FnOnce(&mut Root, &mut ViewContext<Root>) + 'static + Send,
) -> Task<()> {
let task: Task<std::result::Result<WindowHandle<StoryWorkspace>, anyhow::Error>> =
let task: Task<std::result::Result<WindowHandle<Root>, 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");
}
})

View file

@ -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;

View file

@ -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<Self>) -> 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())
}
}

View file

@ -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<PickerStory>,
story: WeakView<ModalStory>,
confirmed_index: Option<usize>,
selected_index: Option<usize>,
items: Vec<Arc<String>>,
@ -108,10 +109,7 @@ impl ListDelegate for ListItemDeletegate {
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {
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<List<ListItemDeletegate>>,
open: bool,
pub struct ModalStory {
focus_handle: FocusHandle,
drawer_placement: Option<Placement>,
selected_value: Option<Arc<String>>,
list: View<List<ListItemDeletegate>>,
input1: View<TextInput>,
date_picker: View<DatePicker>,
}
impl PickerStory {
impl ModalStory {
pub fn view(cx: &mut WindowContext) -> View<Self> {
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<Self>) {
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<Self>) -> 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>) {
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<Self>) -> 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()),
),
)
}),
)
}
}

View file

@ -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)

220
crates/ui/src/drawer.rs Normal file
View file

@ -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<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
title: Option<AnyElement>,
footer: Option<AnyElement>,
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<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
}
/// 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 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),
})
},
),
),
)
}
}

View file

@ -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<Target = str> {
rust_i18n::locale()
}

View file

@ -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(),

89
crates/ui/src/root.rs Normal file
View file

@ -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<F>(&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<F>(&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<F>(&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<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>,
root_view: AnyView,
}
impl Root {
pub fn new(root_view: AnyView, _cx: &mut ViewContext<Self>) -> Self {
Self {
active_drawer: None,
root_view,
}
}
fn update_root<F>(cx: &mut WindowContext, f: F)
where
F: FnOnce(&mut Self, &mut ViewContext<Self>) + 'static,
{
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`.");
root.update(cx, |root, cx| f(root, cx))
}
}
impl Render for Root {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> 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))
})
}
}

View file

@ -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<E> {
id: ElementId,
element: Option<E>,
view: AnyView,
view_id: EntityId,
axis: ScrollbarAxis,
/// This is a fake element to handle Styled, InteractiveElement, not used.
_element: Stateful<Div>,
@ -21,11 +21,10 @@ impl<E> Scrollable<E>
where
E: Element,
{
pub(crate) fn new(element: E, view: impl Into<AnyView>, 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),
),
)

View file

@ -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<Cell<ScrollbarState>>,
axis: ScrollbarAxis,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> 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<AnyView>,
view_id: EntityId,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> 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<AnyView>,
view_id: EntityId,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> 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<AnyView>,
view_id: EntityId,
state: Rc<Cell<ScrollbarState>>,
scroll_handle: impl ScrollHandleOffsetable + 'static,
scroll_size: gpui::Size<Pixels>,
) -> 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<AnyView>,
view_id: EntityId,
state: Rc<Cell<ScrollbarState>>,
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);
}
}

View file

@ -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<AnyView>, axis: ScrollbarAxis) -> Scrollable<Self>
fn scrollable(self, view_id: EntityId, axis: ScrollbarAxis) -> Scrollable<Self>
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,
}
}
}

View file

@ -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<Self>) -> Option<impl IntoElement> {
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(),