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,
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<Self>) -> 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))
}
}

View file

@ -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();
}),
),
)
});
}

View file

@ -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)),

View file

@ -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");

View file

@ -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<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>,
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()

View file

@ -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<Notification>);
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<Notification>) {
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<FocusHandle>,
pub active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>,
pub active_modal: Option<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>,
active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>,
active_modals: Vec<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>,
pub notification: View<NotificationList>,
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<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 {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div()
.id("root")
.size_full()