From a0d3d35be404e9218ed29f36bac787600f2169a5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 10 Sep 2024 16:58:22 +0800 Subject: [PATCH] modal: Add to support open multiple Modals. (#232) - Add `Esc` to close modal. --- crates/app/src/story_workspace.rs | 23 ++------ crates/story/src/modal_story.rs | 25 ++++++-- crates/ui/src/dropdown.rs | 1 - crates/ui/src/lib.rs | 3 +- crates/ui/src/modal.rs | 40 +++++++++++-- crates/ui/src/root.rs | 98 +++++++++++++++++++++++++++---- 6 files changed, 151 insertions(+), 39 deletions(-) diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index bfb395e3..4062c8bb 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -12,9 +12,7 @@ use ui::{ button::Button, color_picker::{ColorPicker, ColorPickerEvent}, dock::{DockArea, DockEvent, DockItem, DockItemState}, - drawer::Drawer, h_flex, - modal::Modal, popup_menu::PopupMenuExt, theme::{ActiveTheme, Colorize as _, Theme}, ContextModal, IconName, Root, Sizable, @@ -281,10 +279,9 @@ pub fn open_new( impl Render for StoryWorkspace { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let active_modal = Root::read(cx).active_modal.clone(); - let active_drawer = Root::read(cx).active_drawer.clone(); - let has_active_modal = active_modal.is_some(); - let notification_view = Root::read(cx).notification.clone(); + let drawer_layer = Root::render_drawer_layer(cx); + let modal_layer = Root::render_modal_layer(cx); + let notification_layer = Root::render_notification_layer(cx); let notifications_count = cx.notifications().len(); div() @@ -374,17 +371,9 @@ impl Render for StoryWorkspace { ), ) .child(self.dock_area.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)) - }) - .child(div().absolute().top_8().child(notification_view)) + .children(drawer_layer) + .children(modal_layer) + .child(div().absolute().top_8().children(notification_layer)) } } diff --git a/crates/story/src/modal_story.rs b/crates/story/src/modal_story.rs index c8a3c381..e2ed0717 100644 --- a/crates/story/src/modal_story.rs +++ b/crates/story/src/modal_story.rs @@ -321,9 +321,9 @@ impl ModalStory { let view = cx.view().clone(); input1.focus_handle(cx).focus(cx); + cx.open_modal(move |modal, cx| { modal - .margin_top(px(33.)) .title("Form Modal") .overlay(overlay) .show_close(modal_show_close) @@ -348,7 +348,6 @@ impl ModalStory { let view = view.clone(); let input1 = input1.clone(); let date_picker = date_picker.clone(); - move |_, cx| { cx.close_modal(); @@ -365,9 +364,25 @@ impl ModalStory { } }), ) - .child(Button::new("cancel", cx).label("Cancel").on_click(|_, cx| { - cx.close_modal(); - })), + .child( + Button::new("new-modal", cx) + .label("Open Other Modal") + .on_click(move |_, cx| { + cx.open_modal(move |modal, _| { + modal + .title("Other Modal") + .child("This is another modal.") + .min_h(px(300.)) + }); + }), + ) + .child( + Button::new("cancel", cx) + .label("Cancel") + .on_click(move |_, cx| { + cx.close_modal(); + }), + ), ) }); } diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 2fc379e9..266754b6 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -18,7 +18,6 @@ use crate::{ actions!(dropdown, [Up, Down, Enter, Escape]); const CONTEXT: &str = "Dropdown"; - pub fn init(cx: &mut AppContext) { cx.bind_keys([ KeyBinding::new("up", Up, Some(CONTEXT)), diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 597d3c13..afaddde9 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -64,10 +64,11 @@ pub fn init(cx: &mut gpui::AppContext) { dropdown::init(cx); input::init(cx); list::init(cx); + modal::init(cx); popover::init(cx); popup_menu::init(cx); table::init(cx); - webview::init(cx) + webview::init(cx); } rust_i18n::i18n!("locales", fallback = "en"); diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 8943a7f0..0fe165b8 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -1,9 +1,9 @@ use std::{rc::Rc, time::Duration}; use gpui::{ - anchored, div, hsla, prelude::FluentBuilder, px, Animation, AnimationExt as _, AnyElement, - Bounds, ClickEvent, Div, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, - Pixels, Point, RenderOnce, Styled, WindowContext, + actions, anchored, div, hsla, prelude::FluentBuilder, px, Animation, AnimationExt as _, + AnyElement, AppContext, Bounds, ClickEvent, Div, Hsla, InteractiveElement, IntoElement, + KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, Styled, WindowContext, }; use crate::{ @@ -11,6 +11,13 @@ use crate::{ IconName, Sizable as _, }; +actions!(modal, [Escape]); + +const CONTEXT: &str = "Modal"; +pub fn init(cx: &mut AppContext) { + cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))]) +} + #[derive(IntoElement)] pub struct Modal { base: Div, @@ -20,9 +27,12 @@ pub struct Modal { width: Pixels, max_width: Option, margin_top: Option, + /// Used to offset the modal from the top when modal is a sub-modal. + pub(crate) offset_top: Pixels, on_close: Rc, show_close: bool, overlay: bool, + pub(crate) overlay_visible: bool, } pub(crate) fn overlay_color(overlay: bool, cx: &WindowContext) -> Hsla { @@ -55,9 +65,11 @@ impl Modal { footer: None, content: v_flex(), margin_top: None, + offset_top: px(0.), width: px(480.), max_width: None, overlay: true, + overlay_visible: true, on_close: Rc::new(|_, _| {}), show_close: true, } @@ -113,6 +125,10 @@ impl Modal { self.overlay = overlay; self } + + pub(crate) fn has_overlay(&self) -> bool { + self.overlay + } } impl ParentElement for Modal { @@ -135,7 +151,7 @@ impl RenderOnce for Modal { origin: Point::default(), size: view_size, }; - let y = self.margin_top.unwrap_or(view_size.height / 10.); + let y = self.margin_top.unwrap_or(view_size.height / 10.) + self.offset_top; let x = bounds.center().x - self.width / 2.; anchored().snap_to_window().child( @@ -143,7 +159,9 @@ impl RenderOnce for Modal { .occlude() .w(view_size.width) .h(view_size.height) - .bg(overlay_color(self.overlay, cx)) + .when(self.overlay_visible, |this| { + this.bg(overlay_color(self.overlay, cx)) + }) .when(self.overlay, |this| { this.on_mouse_down(MouseButton::Left, { let on_close = self.on_close.clone(); @@ -156,6 +174,18 @@ impl RenderOnce for Modal { .child( self.base .id("modal") + .key_context(CONTEXT) + .on_action({ + let on_close = self.on_close.clone(); + move |_: &Escape, cx| { + // FIXME: + // + // Here some Modal have no focus_handle, so it will not work will Escape key. + // But by now, we `cx.close_modal()` going to close the last active model, so the Escape is unexpected to work. + on_close(&ClickEvent::default(), cx); + cx.close_modal(); + } + }) .absolute() .occlude() .relative() diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 9c6203e2..a2e4cf9c 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -1,6 +1,6 @@ use gpui::{ - div, AnyView, FocusHandle, InteractiveElement, ParentElement as _, Render, Styled, View, - ViewContext, VisualContext as _, WindowContext, + div, px, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, + Styled, View, ViewContext, VisualContext as _, WindowContext, }; use std::{ ops::{Deref, DerefMut}, @@ -35,9 +35,12 @@ pub trait ContextModal: Sized { /// Return true, if there is an active Modal. fn has_active_modal(&self) -> bool; - /// Closes the active Modal. + /// Closes the last active Modal. fn close_modal(&mut self); + /// Closes all active Modals. + fn close_all_modals(&mut self); + /// Pushes a notification to the notification list. fn push_notification(&mut self, note: impl Into); fn clear_notifications(&mut self); @@ -75,18 +78,26 @@ impl<'a> ContextModal for WindowContext<'a> { { Root::update(self, move |root, cx| { root.previous_focus_handle = cx.focused(); - root.active_modal = Some(Rc::new(build)); + root.active_modals.push(Rc::new(build)); cx.notify(); }) } fn has_active_modal(&self) -> bool { - Root::read(&self).active_modal.is_some() + Root::read(&self).active_modals.len() > 0 } fn close_modal(&mut self) { + Root::update(self, move |root, cx| { + root.active_modals.pop(); + root.focus_back(cx); + cx.notify(); + }) + } + + fn close_all_modals(&mut self) { Root::update(self, |root, cx| { - root.active_modal = None; + root.active_modals.clear(); root.focus_back(cx); cx.notify(); }) @@ -138,10 +149,16 @@ impl<'a, V> ContextModal for ViewContext<'a, V> { self.deref().has_active_drawer() } + /// Close the last active modal. fn close_modal(&mut self) { self.deref_mut().close_modal() } + /// Close all modals. + fn close_all_modals(&mut self) { + self.deref_mut().close_all_modals() + } + fn push_notification(&mut self, note: impl Into) { self.deref_mut().push_notification(note) } @@ -162,8 +179,8 @@ pub struct Root { /// Used to store the focus handle of the previus revious view. /// When the Modal, Drawer closes, we will focus back to the previous view. previous_focus_handle: Option, - pub active_drawer: Option Drawer + 'static>>, - pub active_modal: Option Modal + 'static>>, + active_drawer: Option Drawer + 'static>>, + active_modals: Vec Modal + 'static>>, pub notification: View, child: AnyView, } @@ -173,7 +190,7 @@ impl Root { Self { previous_focus_handle: None, active_drawer: None, - active_modal: None, + active_modals: Vec::new(), notification: cx.new_view(NotificationList::new), child, } @@ -207,10 +224,71 @@ impl Root { cx.focus(&handle); } } + + // Render Notification layer. + pub fn render_notification_layer(cx: &mut WindowContext) -> Option { + 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`."); + + Some(div().child(root.read(cx).notification.clone())) + } + + /// Render the Drawer layer. + pub fn render_drawer_layer(cx: &mut WindowContext) -> Option { + 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`."); + + if let Some(builder) = root.read(cx).active_drawer.clone() { + let drawer = Drawer::new(cx); + return Some(builder(drawer, cx)); + } + + None + } + + /// Render the Modal layer. + pub fn render_modal_layer(cx: &mut WindowContext) -> Option { + 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`."); + + let active_modals = root.read(cx).active_modals.clone(); + let mut has_overlay = false; + + if active_modals.is_empty() { + return None; + } + + Some( + div().children(active_modals.iter().enumerate().map(|(i, builder)| { + let mut modal = Modal::new(cx); + modal = builder(modal, cx); + modal.offset_top = px(i as f32 * 16.); + + // Keep only have one overlay, we only render the first modal with overlay. + if has_overlay { + modal.overlay_visible = false; + } + if modal.has_overlay() { + has_overlay = true; + } + + modal + })), + ) + } } impl Render for Root { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl gpui::IntoElement { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { div() .id("root") .size_full()