Add slide down animation t Modal. (#141)

- Export cx.active_modal and cx.active_drawer
- Add support custom modal style, and add show_close option.
This commit is contained in:
Jason Lee 2024-08-14 17:23:39 +08:00 committed by GitHub
parent 1da82c5453
commit 3550b1ee13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 204 additions and 114 deletions

View file

@ -11,10 +11,12 @@ use workspace::{TitleBar, Workspace};
use std::sync::Arc;
use ui::{
button::{Button, ButtonStyle},
drawer::Drawer,
modal::Modal,
popover::Popover,
popup_menu::PopupMenu,
theme::{ActiveTheme, Theme},
IconName, Root, Sizable,
ContextModal as _, IconName, Root, Sizable,
};
use crate::app_state::AppState;
@ -271,6 +273,10 @@ pub fn open_new(
impl Render for StoryWorkspace {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_modal = cx.active_modal();
let active_drawer = cx.active_drawer();
let has_active_modal = active_modal.is_some();
div()
.relative()
.size_full()
@ -331,6 +337,16 @@ impl Render for StoryWorkspace {
),
)
.child(self.workspace.clone())
.when(!has_active_modal, |this| {
this.when_some(active_drawer, |this, builder| {
let drawer = Drawer::new(cx);
this.child(builder(drawer, cx))
})
})
.when_some(active_modal, |this, builder| {
let modal = Modal::new(cx);
this.child(builder(modal, cx))
})
}
}

View file

@ -206,7 +206,7 @@ impl StoryContainer {
}
impl Render for StoryContainer {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.id("story-container")
.size_full()

View file

@ -146,6 +146,8 @@ pub struct ModalStory {
input1: View<TextInput>,
date_picker: View<DatePicker>,
modal_overlay: bool,
model_show_close: bool,
model_padding: bool,
}
impl ModalStory {
@ -235,6 +237,8 @@ impl ModalStory {
input1,
date_picker,
modal_overlay: true,
model_show_close: true,
model_padding: true,
}
}
@ -291,6 +295,64 @@ impl ModalStory {
self.drawer_placement = None;
cx.notify();
}
fn show_modal(&mut self, cx: &mut ViewContext<Self>) {
let overlay = self.modal_overlay;
let modal_show_close = self.model_show_close;
let modal_padding = self.model_padding;
let input1 = self.input1.clone();
let date_picker = self.date_picker.clone();
let view = cx.view().clone();
cx.open_modal(move |modal, cx| {
modal
.title("Form Modal")
.overlay(overlay)
.show_close(modal_show_close)
.when(!modal_padding, |this| this.p(px(0.)))
.child(
v_flex()
.gap_3()
.child("This is a modal dialog.")
.child("You can put anything here.")
.child(input1.clone())
.child(date_picker.clone()),
)
.footer(
h_flex()
.gap_6()
.items_center()
.child(
Button::new("confirm", cx)
.primary()
.label("Confirm")
.on_click({
let view = view.clone();
let input1 = input1.clone();
let date_picker = date_picker.clone();
move |_, cx| {
cx.close_modal();
view.update(cx, |view, cx| {
view.selected_value = Some(
format!(
"Hello, {}, date: {}",
input1.read(cx).text(),
date_picker.read(cx).date()
)
.into(),
)
});
}
}),
)
.child(Button::new("cancel", cx).label("Cancel").on_click(|_, cx| {
cx.close_modal();
})),
)
})
}
}
impl FocusableView for ModalStory {
@ -301,17 +363,40 @@ impl FocusableView for ModalStory {
impl Render for ModalStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child(
div().size_full().child(
v_flex()
.gap_6()
.child(
Checkbox::new("modal-overlay")
.label("Modal Overlay")
.checked(self.modal_overlay)
.on_click(cx.listener(|view, _, cx| {
view.modal_overlay = !view.modal_overlay;
cx.notify();
})),
h_flex()
.items_center()
.gap_3()
.child(
Checkbox::new("modal-overlay")
.label("Modal Overlay")
.checked(self.modal_overlay)
.on_click(cx.listener(|view, _, cx| {
view.modal_overlay = !view.modal_overlay;
cx.notify();
})),
)
.child(
Checkbox::new("modal-show-close")
.label("Model Close Button")
.checked(self.model_show_close)
.on_click(cx.listener(|view, _, cx| {
view.model_show_close = !view.model_show_close;
cx.notify();
})),
)
.child(
Checkbox::new("modal-padding")
.label("Model Padding")
.checked(self.model_padding)
.on_click(cx.listener(|view, _, cx| {
view.model_padding = !view.model_padding;
cx.notify();
})),
),
)
.child(
h_flex()
@ -358,63 +443,7 @@ impl Render for ModalStory {
.child(
Button::new("show-modal", cx)
.label("Open Modal...")
.on_click(cx.listener(|this, _, cx| {
let overlay = this.modal_overlay;
let input1 = this.input1.clone();
let date_picker = this.date_picker.clone();
let view = cx.view().clone();
cx.open_modal(move |modal, cx| {
modal
.title("Form Modal")
.overlay(overlay)
.child(
v_flex()
.gap_3()
.child("This is a modal dialog.")
.child("You can put anything here.")
.child(input1.clone())
.child(date_picker.clone()),
)
.footer(
h_flex()
.gap_6()
.items_center()
.child(
Button::new("confirm", cx)
.primary()
.label("Confirm")
.on_click({
let view = view.clone();
let input1 = input1.clone();
let date_picker = date_picker.clone();
move |_, cx| {
cx.close_modal();
view.update(cx, |view, cx| {
view.selected_value = Some(
format!(
"Hello, {}, date: {}",
input1.read(cx).text(),
date_picker.read(cx).date()
)
.into(),
)
});
}
}),
)
.child(
Button::new("cancel", cx).label("Cancel").on_click(
|_, cx| {
cx.close_modal();
},
),
),
)
})
})),
.on_click(cx.listener(|this, _, cx| this.show_modal(cx))),
),
)
}

View file

@ -1,9 +1,9 @@
use std::rc::Rc;
use std::{rc::Rc, time::Duration};
use gpui::{
anchored, div, hsla, prelude::FluentBuilder, px, AnyElement, Bounds, ClickEvent, Div,
FocusHandle, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
Point, RenderOnce, Styled, WindowContext,
anchored, div, hsla, prelude::FluentBuilder, px, Animation, AnimationExt as _, AnyElement,
Bounds, ClickEvent, Div, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement,
Pixels, Point, RenderOnce, Styled, WindowContext,
};
use crate::{
@ -12,7 +12,7 @@ use crate::{
#[derive(IntoElement)]
pub struct Modal {
focus_handle: FocusHandle,
base: Div,
title: Option<AnyElement>,
footer: Option<AnyElement>,
content: Div,
@ -20,6 +20,7 @@ pub struct Modal {
max_width: Option<Pixels>,
margin_top: Option<Pixels>,
on_close: Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
show_close: bool,
overlay: bool,
}
@ -37,8 +38,18 @@ pub(crate) fn overlay_color(overlay: bool, cx: &WindowContext) -> Hsla {
impl Modal {
pub fn new(cx: &mut WindowContext) -> Self {
let base = v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.rounded_lg()
.shadow_xl()
.min_h_48()
.p_4()
.gap_4();
Self {
focus_handle: cx.focus_handle(),
base,
title: None,
footer: None,
content: v_flex(),
@ -47,6 +58,7 @@ impl Modal {
max_width: None,
overlay: true,
on_close: Rc::new(|_, _| {}),
show_close: true,
}
}
@ -71,6 +83,12 @@ impl Modal {
self
}
/// Sets the false to hide close icon, default: true
pub fn show_close(mut self, show_close: bool) -> Self {
self.show_close = show_close;
self
}
/// Set the top offset of the modal, defaults to None, will use the 1/10 of the viewport height.
pub fn margin_top(mut self, margin_top: Pixels) -> Self {
self.margin_top = Some(margin_top);
@ -102,6 +120,12 @@ impl ParentElement for Modal {
}
}
impl Styled for Modal {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}
impl RenderOnce for Modal {
fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement {
let on_close = self.on_close.clone();
@ -129,40 +153,41 @@ impl RenderOnce for Modal {
})
})
.child(
v_flex()
self.base
.id("modal")
.absolute()
.track_focus(&self.focus_handle)
.occlude()
.relative()
.left(x)
.top(y)
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.rounded_lg()
.shadow_xl()
.min_h_48()
.w(self.width)
.when_some(self.max_width, |this, w| this.max_w(w))
.p_4()
.gap_4()
.children(self.title)
.child(
Button::new("close", cx)
.absolute()
.top_2()
.right_2()
.small()
.ghost()
.icon(IconName::Close)
.on_click(move |_, cx| {
on_close(&ClickEvent::default(), cx);
cx.close_modal();
}),
)
.when(self.show_close, |this| {
this.child(
Button::new("close", cx)
.absolute()
.top_2()
.right_2()
.small()
.ghost()
.icon(IconName::Close)
.on_click(move |_, cx| {
on_close(&ClickEvent::default(), cx);
cx.close_modal();
}),
)
})
.child(self.content)
.children(self.footer),
.children(self.footer)
.with_animation(
"slide-down",
Animation::new(Duration::from_secs_f64(0.1)),
move |this, delta| {
let y_offset = px(-30.) + delta * px(30.);
this.top(y + y_offset)
},
),
),
)
}

View file

@ -1,8 +1,8 @@
use gpui::{
div, prelude::FluentBuilder as _, AnyView, ParentElement as _, Render, Styled, ViewContext,
WindowContext,
use gpui::{div, AnyView, ParentElement as _, Render, Styled, ViewContext, WindowContext};
use std::{
ops::{Deref, DerefMut},
rc::Rc,
};
use std::{ops::DerefMut, rc::Rc};
use crate::{drawer::Drawer, modal::Modal, theme::ActiveTheme};
@ -13,6 +13,9 @@ pub trait ContextModal: Sized {
where
F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static;
/// Return the active Drawer builder, you must add the Drawer to the view.
fn active_drawer(&self) -> Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>;
/// Closes the active Drawer.
fn close_drawer(&mut self);
@ -21,6 +24,9 @@ pub trait ContextModal: Sized {
where
F: Fn(Modal, &mut WindowContext) -> Modal + 'static;
/// Return the active Modal builder, you must add the Modal to the view.
fn active_modal(&self) -> Option<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>;
/// Closes the active Modal.
fn close_modal(&mut self);
}
@ -36,6 +42,10 @@ impl<'a> ContextModal for WindowContext<'a> {
})
}
fn active_drawer(&self) -> Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>> {
Root::read_root(&self).active_drawer.clone()
}
fn close_drawer(&mut self) {
Root::update_root(self, |root, cx| {
root.active_drawer = None;
@ -53,6 +63,10 @@ impl<'a> ContextModal for WindowContext<'a> {
})
}
fn active_modal(&self) -> Option<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>> {
Root::read_root(&self).active_modal.clone()
}
fn close_modal(&mut self) {
Root::update_root(self, |root, cx| {
root.active_modal = None;
@ -68,6 +82,10 @@ impl<'a, V> ContextModal for ViewContext<'a, V> {
self.deref_mut().open_drawer(build)
}
fn active_modal(&self) -> Option<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>> {
self.deref().active_modal()
}
fn close_drawer(&mut self) {
self.deref_mut().close_drawer()
}
@ -79,6 +97,10 @@ impl<'a, V> ContextModal for ViewContext<'a, V> {
self.deref_mut().open_modal(build)
}
fn active_drawer(&self) -> Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>> {
self.deref().active_drawer()
}
fn close_modal(&mut self) {
self.deref_mut().close_modal()
}
@ -91,7 +113,7 @@ pub struct Root {
}
impl Root {
pub fn new(root_view: AnyView, _cx: &mut ViewContext<Self>) -> Self {
pub fn new(root_view: AnyView, _: &mut ViewContext<Self>) -> Self {
Self {
active_drawer: None,
active_modal: None,
@ -111,25 +133,23 @@ impl Root {
root.update(cx, |root, cx| f(root, cx))
}
fn read_root<'a>(cx: &'a WindowContext) -> &'a Self {
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.read(cx)
}
}
impl Render for Root {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let has_modal = self.active_modal.is_some();
div()
.size_full()
.text_color(cx.theme().foreground)
.child(self.root_view.clone())
.when(!has_modal, |this| {
this.when_some(self.active_drawer.clone(), |this, build| {
let drawer = Drawer::new(cx);
this.child(build(drawer, cx))
})
})
.when_some(self.active_modal.clone(), |this, build| {
let modal = Modal::new(cx);
this.child(build(modal, cx))
})
}
}