Add Drawer (#137)
- Update scrollable to accept EntityId. https://github.com/user-attachments/assets/a6c02890-3a6e-4b82-b5c5-3f0a48f35368
This commit is contained in:
parent
7059165fa6
commit
63f333d8ad
13 changed files with 520 additions and 136 deletions
|
|
@ -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");
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
220
crates/ui/src/drawer.rs
Normal 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),
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
89
crates/ui/src/root.rs
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue