diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 10e2cf01..e5c5a972 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -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) -> 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)) + }) } } diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index d1359fbf..4b8dedbf 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -206,7 +206,7 @@ impl StoryContainer { } impl Render for StoryContainer { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { v_flex() .id("story-container") .size_full() diff --git a/crates/story/src/modal_story.rs b/crates/story/src/modal_story.rs index 897668ec..b7fe1fc8 100644 --- a/crates/story/src/modal_story.rs +++ b/crates/story/src/modal_story.rs @@ -146,6 +146,8 @@ pub struct ModalStory { input1: View, date_picker: View, 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) { + 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) -> 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))), ), ) } diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 661a331e..4002fa59 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -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, footer: Option, content: Div, @@ -20,6 +20,7 @@ pub struct Modal { max_width: Option, margin_top: Option, on_close: Rc, + 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) + }, + ), ), ) } diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index cce1ba12..81a09987 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -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 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 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 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 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 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 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 { + pub fn new(root_view: AnyView, _: &mut ViewContext) -> 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::() + .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) -> 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)) - }) } }