modal: Add to support open multiple Modals. (#232)

- Add `Esc` to close modal.
This commit is contained in:
Jason Lee 2024-09-10 16:58:22 +08:00 committed by GitHub
parent 303dd51047
commit a0d3d35be4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 151 additions and 39 deletions

View file

@ -12,9 +12,7 @@ use ui::{
button::Button, button::Button,
color_picker::{ColorPicker, ColorPickerEvent}, color_picker::{ColorPicker, ColorPickerEvent},
dock::{DockArea, DockEvent, DockItem, DockItemState}, dock::{DockArea, DockEvent, DockItem, DockItemState},
drawer::Drawer,
h_flex, h_flex,
modal::Modal,
popup_menu::PopupMenuExt, popup_menu::PopupMenuExt,
theme::{ActiveTheme, Colorize as _, Theme}, theme::{ActiveTheme, Colorize as _, Theme},
ContextModal, IconName, Root, Sizable, ContextModal, IconName, Root, Sizable,
@ -281,10 +279,9 @@ pub fn open_new(
impl Render for StoryWorkspace { impl Render for StoryWorkspace {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_modal = Root::read(cx).active_modal.clone(); let drawer_layer = Root::render_drawer_layer(cx);
let active_drawer = Root::read(cx).active_drawer.clone(); let modal_layer = Root::render_modal_layer(cx);
let has_active_modal = active_modal.is_some(); let notification_layer = Root::render_notification_layer(cx);
let notification_view = Root::read(cx).notification.clone();
let notifications_count = cx.notifications().len(); let notifications_count = cx.notifications().len();
div() div()
@ -374,17 +371,9 @@ impl Render for StoryWorkspace {
), ),
) )
.child(self.dock_area.clone()) .child(self.dock_area.clone())
.when(!has_active_modal, |this| { .children(drawer_layer)
this.when_some(active_drawer, |this, builder| { .children(modal_layer)
let drawer = Drawer::new(cx); .child(div().absolute().top_8().children(notification_layer))
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))
} }
} }

View file

@ -321,9 +321,9 @@ impl ModalStory {
let view = cx.view().clone(); let view = cx.view().clone();
input1.focus_handle(cx).focus(cx); input1.focus_handle(cx).focus(cx);
cx.open_modal(move |modal, cx| { cx.open_modal(move |modal, cx| {
modal modal
.margin_top(px(33.))
.title("Form Modal") .title("Form Modal")
.overlay(overlay) .overlay(overlay)
.show_close(modal_show_close) .show_close(modal_show_close)
@ -348,7 +348,6 @@ impl ModalStory {
let view = view.clone(); let view = view.clone();
let input1 = input1.clone(); let input1 = input1.clone();
let date_picker = date_picker.clone(); let date_picker = date_picker.clone();
move |_, cx| { move |_, cx| {
cx.close_modal(); cx.close_modal();
@ -365,9 +364,25 @@ impl ModalStory {
} }
}), }),
) )
.child(Button::new("cancel", cx).label("Cancel").on_click(|_, cx| { .child(
cx.close_modal(); 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();
}),
),
) )
}); });
} }

View file

@ -18,7 +18,6 @@ use crate::{
actions!(dropdown, [Up, Down, Enter, Escape]); actions!(dropdown, [Up, Down, Enter, Escape]);
const CONTEXT: &str = "Dropdown"; const CONTEXT: &str = "Dropdown";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.bind_keys([ cx.bind_keys([
KeyBinding::new("up", Up, Some(CONTEXT)), KeyBinding::new("up", Up, Some(CONTEXT)),

View file

@ -64,10 +64,11 @@ pub fn init(cx: &mut gpui::AppContext) {
dropdown::init(cx); dropdown::init(cx);
input::init(cx); input::init(cx);
list::init(cx); list::init(cx);
modal::init(cx);
popover::init(cx); popover::init(cx);
popup_menu::init(cx); popup_menu::init(cx);
table::init(cx); table::init(cx);
webview::init(cx) webview::init(cx);
} }
rust_i18n::i18n!("locales", fallback = "en"); rust_i18n::i18n!("locales", fallback = "en");

View file

@ -1,9 +1,9 @@
use std::{rc::Rc, time::Duration}; use std::{rc::Rc, time::Duration};
use gpui::{ use gpui::{
anchored, div, hsla, prelude::FluentBuilder, px, Animation, AnimationExt as _, AnyElement, actions, anchored, div, hsla, prelude::FluentBuilder, px, Animation, AnimationExt as _,
Bounds, ClickEvent, Div, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, AnyElement, AppContext, Bounds, ClickEvent, Div, Hsla, InteractiveElement, IntoElement,
Pixels, Point, RenderOnce, Styled, WindowContext, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, Styled, WindowContext,
}; };
use crate::{ use crate::{
@ -11,6 +11,13 @@ use crate::{
IconName, Sizable as _, 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)] #[derive(IntoElement)]
pub struct Modal { pub struct Modal {
base: Div, base: Div,
@ -20,9 +27,12 @@ pub struct Modal {
width: Pixels, width: Pixels,
max_width: Option<Pixels>, max_width: Option<Pixels>,
margin_top: Option<Pixels>, margin_top: Option<Pixels>,
/// Used to offset the modal from the top when modal is a sub-modal.
pub(crate) offset_top: Pixels,
on_close: Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>, on_close: Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
show_close: bool, show_close: bool,
overlay: bool, overlay: bool,
pub(crate) overlay_visible: bool,
} }
pub(crate) fn overlay_color(overlay: bool, cx: &WindowContext) -> Hsla { pub(crate) fn overlay_color(overlay: bool, cx: &WindowContext) -> Hsla {
@ -55,9 +65,11 @@ impl Modal {
footer: None, footer: None,
content: v_flex(), content: v_flex(),
margin_top: None, margin_top: None,
offset_top: px(0.),
width: px(480.), width: px(480.),
max_width: None, max_width: None,
overlay: true, overlay: true,
overlay_visible: true,
on_close: Rc::new(|_, _| {}), on_close: Rc::new(|_, _| {}),
show_close: true, show_close: true,
} }
@ -113,6 +125,10 @@ impl Modal {
self.overlay = overlay; self.overlay = overlay;
self self
} }
pub(crate) fn has_overlay(&self) -> bool {
self.overlay
}
} }
impl ParentElement for Modal { impl ParentElement for Modal {
@ -135,7 +151,7 @@ impl RenderOnce for Modal {
origin: Point::default(), origin: Point::default(),
size: view_size, 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.; let x = bounds.center().x - self.width / 2.;
anchored().snap_to_window().child( anchored().snap_to_window().child(
@ -143,7 +159,9 @@ impl RenderOnce for Modal {
.occlude() .occlude()
.w(view_size.width) .w(view_size.width)
.h(view_size.height) .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| { .when(self.overlay, |this| {
this.on_mouse_down(MouseButton::Left, { this.on_mouse_down(MouseButton::Left, {
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
@ -156,6 +174,18 @@ impl RenderOnce for Modal {
.child( .child(
self.base self.base
.id("modal") .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() .absolute()
.occlude() .occlude()
.relative() .relative()

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, AnyView, FocusHandle, InteractiveElement, ParentElement as _, Render, Styled, View, div, px, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render,
ViewContext, VisualContext as _, WindowContext, Styled, View, ViewContext, VisualContext as _, WindowContext,
}; };
use std::{ use std::{
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
@ -35,9 +35,12 @@ pub trait ContextModal: Sized {
/// Return true, if there is an active Modal. /// Return true, if there is an active Modal.
fn has_active_modal(&self) -> bool; fn has_active_modal(&self) -> bool;
/// Closes the active Modal. /// Closes the last active Modal.
fn close_modal(&mut self); fn close_modal(&mut self);
/// Closes all active Modals.
fn close_all_modals(&mut self);
/// Pushes a notification to the notification list. /// Pushes a notification to the notification list.
fn push_notification(&mut self, note: impl Into<Notification>); fn push_notification(&mut self, note: impl Into<Notification>);
fn clear_notifications(&mut self); fn clear_notifications(&mut self);
@ -75,18 +78,26 @@ impl<'a> ContextModal for WindowContext<'a> {
{ {
Root::update(self, move |root, cx| { Root::update(self, move |root, cx| {
root.previous_focus_handle = cx.focused(); root.previous_focus_handle = cx.focused();
root.active_modal = Some(Rc::new(build)); root.active_modals.push(Rc::new(build));
cx.notify(); cx.notify();
}) })
} }
fn has_active_modal(&self) -> bool { 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) { 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::update(self, |root, cx| {
root.active_modal = None; root.active_modals.clear();
root.focus_back(cx); root.focus_back(cx);
cx.notify(); cx.notify();
}) })
@ -138,10 +149,16 @@ impl<'a, V> ContextModal for ViewContext<'a, V> {
self.deref().has_active_drawer() self.deref().has_active_drawer()
} }
/// Close the last active modal.
fn close_modal(&mut self) { fn close_modal(&mut self) {
self.deref_mut().close_modal() 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<Notification>) { fn push_notification(&mut self, note: impl Into<Notification>) {
self.deref_mut().push_notification(note) self.deref_mut().push_notification(note)
} }
@ -162,8 +179,8 @@ pub struct Root {
/// Used to store the focus handle of the previus revious view. /// Used to store the focus handle of the previus revious view.
/// When the Modal, Drawer closes, we will focus back to the previous view. /// When the Modal, Drawer closes, we will focus back to the previous view.
previous_focus_handle: Option<FocusHandle>, previous_focus_handle: Option<FocusHandle>,
pub active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>, active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>,
pub active_modal: Option<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>, active_modals: Vec<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>,
pub notification: View<NotificationList>, pub notification: View<NotificationList>,
child: AnyView, child: AnyView,
} }
@ -173,7 +190,7 @@ impl Root {
Self { Self {
previous_focus_handle: None, previous_focus_handle: None,
active_drawer: None, active_drawer: None,
active_modal: None, active_modals: Vec::new(),
notification: cx.new_view(NotificationList::new), notification: cx.new_view(NotificationList::new),
child, child,
} }
@ -207,10 +224,71 @@ impl Root {
cx.focus(&handle); cx.focus(&handle);
} }
} }
// Render Notification layer.
pub fn render_notification_layer(cx: &mut WindowContext) -> Option<impl IntoElement> {
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`.");
Some(div().child(root.read(cx).notification.clone()))
}
/// Render the Drawer layer.
pub fn render_drawer_layer(cx: &mut WindowContext) -> Option<impl IntoElement> {
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`.");
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<impl IntoElement> {
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`.");
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 { impl Render for Root {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div() div()
.id("root") .id("root")
.size_full() .size_full()