modal: Improve Modal and Panel focus handle. (#238)
- modal: Improve modal focus take, when modal close, make sure focus back. - modal: Fix hover close button will cross to back modal on multiple modal layer. - panel: Add focus handle to Panel and TabPanel. - panel: Update title method to return `AnyElement`.
This commit is contained in:
parent
9388b88f59
commit
ce87c3a001
23 changed files with 436 additions and 246 deletions
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{
|
||||
px, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
px, ClickEvent, FocusableView, IntoElement, ParentElement as _, Render, Styled as _, View,
|
||||
ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
|
|
@ -16,6 +16,7 @@ use ui::{
|
|||
use crate::section;
|
||||
|
||||
pub struct ButtonStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
disabled: bool,
|
||||
loading: bool,
|
||||
selected: bool,
|
||||
|
|
@ -24,7 +25,8 @@ pub struct ButtonStory {
|
|||
|
||||
impl ButtonStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|_| Self {
|
||||
cx.new_view(|cx| Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
disabled: false,
|
||||
loading: false,
|
||||
selected: false,
|
||||
|
|
@ -50,8 +52,14 @@ impl super::Story for ButtonStory {
|
|||
false
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ButtonStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ impl super::Story for CalendarStory {
|
|||
"A date picker and calendar component."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +105,12 @@ impl CalendarStory {
|
|||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for CalendarStory {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.date_picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CalendarStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
|
|
|
|||
|
|
@ -64,8 +64,14 @@ impl super::Story for DropdownStory {
|
|||
"Displays a list of options for the user to pick from—triggered by a button."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for DropdownStory {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.fruit_dropdown.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use gpui::{px, rems, ParentElement, Render, Styled, View, VisualContext as _, WindowContext};
|
||||
use gpui::{
|
||||
px, rems, ParentElement, Render, Styled, View, ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
use ui::{
|
||||
button::{Button, ButtonStyle},
|
||||
h_flex,
|
||||
|
|
@ -6,15 +8,19 @@ use ui::{
|
|||
v_flex, Icon, IconName,
|
||||
};
|
||||
|
||||
pub struct IconStory {}
|
||||
pub struct IconStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
}
|
||||
|
||||
impl IconStory {
|
||||
pub fn new(_: &WindowContext) -> Self {
|
||||
Self {}
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|cx| Self::new(cx))
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,8 +33,14 @@ impl super::Story for IconStory {
|
|||
"Icon use examples"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for IconStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg");
|
|||
const PIE_JSON: &str = include_str!("./fixtures/pie.json");
|
||||
|
||||
pub struct ImageStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
google_logo: SvgImg,
|
||||
pie_chart: SvgImg,
|
||||
inbox_img: SvgImg,
|
||||
|
|
@ -15,16 +16,17 @@ impl super::Story for ImageStory {
|
|||
"Image"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageStory {
|
||||
pub fn new(_: &WindowContext) -> Self {
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
let chart = charts_rs::PieChart::from_json(PIE_JSON).unwrap();
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
google_logo: svg_img().source(GOOGLE_LOGO.as_bytes(), px(300.), px(300.)),
|
||||
pie_chart: svg_img().source(chart.svg().unwrap().as_bytes(), px(400.), px(400.)),
|
||||
inbox_img: svg_img().source("icons/inbox.svg", px(300.), px(300.)),
|
||||
|
|
@ -36,6 +38,12 @@ impl ImageStory {
|
|||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for ImageStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ImageStory {
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
v_flex()
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ impl super::Story for InputStory {
|
|||
false
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +220,11 @@ impl FocusableCycle for InputStory {
|
|||
.to_vec()
|
||||
}
|
||||
}
|
||||
impl gpui::FocusableView for InputStory {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.input1.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
|
|
|||
|
|
@ -36,9 +36,10 @@ pub use tooltip_story::TooltipStory;
|
|||
pub use webview_story::WebViewStory;
|
||||
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder as _, px, AnyView, AppContext, Div, EventEmitter,
|
||||
FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext, WindowContext,
|
||||
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div,
|
||||
EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
|
|
@ -112,7 +113,7 @@ pub enum ContainerEvent {
|
|||
Close,
|
||||
}
|
||||
|
||||
pub trait Story {
|
||||
pub trait Story: FocusableView {
|
||||
fn klass() -> &'static str {
|
||||
std::any::type_name::<Self>().split("::").last().unwrap()
|
||||
}
|
||||
|
|
@ -127,7 +128,7 @@ pub trait Story {
|
|||
fn title_bg() -> Option<Hsla> {
|
||||
None
|
||||
}
|
||||
fn new_view(cx: &mut WindowContext) -> AnyView;
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl FocusableView>;
|
||||
}
|
||||
|
||||
impl EventEmitter<ContainerEvent> for StoryContainer {}
|
||||
|
|
@ -155,9 +156,11 @@ impl StoryContainer {
|
|||
let description = S::description();
|
||||
let story = S::new_view(cx);
|
||||
let story_klass = S::klass();
|
||||
let focus_handle = story.focus_handle(cx);
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let mut story = Self::new(cx).story(story, story_klass);
|
||||
let mut story = Self::new(cx).story(story.into(), story_klass);
|
||||
story.focus_handle = focus_handle;
|
||||
story.closeable = S::closeable();
|
||||
story.name = name.into();
|
||||
story.description = description.into();
|
||||
|
|
@ -249,8 +252,8 @@ impl Panel for StoryContainer {
|
|||
"StoryContainer"
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
self.name.clone()
|
||||
fn title(&self, _cx: &WindowContext) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> {
|
||||
|
|
|
|||
|
|
@ -205,8 +205,8 @@ impl super::Story for ListStory {
|
|||
"A list displays a series of items."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::{sync::Arc, time::Duration};
|
|||
|
||||
use fake::Fake;
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, IntoElement, ParentElement,
|
||||
Render, SharedString, Styled, Task, Timer, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
actions, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Render, SharedString, Styled, Task, Timer,
|
||||
View, ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
|
|
@ -19,6 +19,8 @@ use ui::{
|
|||
v_flex, ContextModal as _, Icon, IconName, Placement,
|
||||
};
|
||||
|
||||
actions!(modal_story, [TestAction]);
|
||||
|
||||
pub struct ListItemDeletegate {
|
||||
story: WeakView<ModalStory>,
|
||||
confirmed_index: Option<usize>,
|
||||
|
|
@ -145,6 +147,7 @@ pub struct ModalStory {
|
|||
selected_value: Option<SharedString>,
|
||||
list: View<List<ListItemDeletegate>>,
|
||||
input1: View<TextInput>,
|
||||
input2: View<TextInput>,
|
||||
date_picker: View<DatePicker>,
|
||||
modal_overlay: bool,
|
||||
model_show_close: bool,
|
||||
|
|
@ -160,8 +163,8 @@ impl super::Story for ModalStory {
|
|||
"Modal & Drawer use examples"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +244,7 @@ impl ModalStory {
|
|||
});
|
||||
|
||||
let input1 = cx.new_view(|cx| TextInput::new(cx).placeholder("Your Name"));
|
||||
let input2 = cx.new_view(|cx| TextInput::new(cx).placeholder("Input on the Window"));
|
||||
let date_picker =
|
||||
cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth"));
|
||||
|
||||
|
|
@ -250,6 +254,7 @@ impl ModalStory {
|
|||
selected_value: None,
|
||||
list,
|
||||
input1,
|
||||
input2,
|
||||
date_picker,
|
||||
modal_overlay: true,
|
||||
model_show_close: true,
|
||||
|
|
@ -268,7 +273,6 @@ impl ModalStory {
|
|||
};
|
||||
|
||||
let overlay = self.modal_overlay;
|
||||
input.focus_handle(cx).focus(cx);
|
||||
cx.open_drawer(move |this, cx| {
|
||||
this.margin_top(px(33.))
|
||||
.placement(placement)
|
||||
|
|
@ -320,9 +324,8 @@ impl ModalStory {
|
|||
let date_picker = self.date_picker.clone();
|
||||
let view = cx.view().clone();
|
||||
|
||||
input1.focus_handle(cx).focus(cx);
|
||||
|
||||
cx.open_modal(move |modal, cx| {
|
||||
input1.focus_handle(cx).focus(cx);
|
||||
modal
|
||||
.title("Form Modal")
|
||||
.overlay(overlay)
|
||||
|
|
@ -386,6 +389,10 @@ impl ModalStory {
|
|||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn on_action_test_action(&mut self, _: &TestAction, cx: &mut ViewContext<Self>) {
|
||||
cx.push_notification("You have clicked the TestAction.");
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ModalStory {
|
||||
|
|
@ -396,135 +403,155 @@ impl FocusableView for ModalStory {
|
|||
|
||||
impl Render for ModalStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().size_full().child(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
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()
|
||||
.items_start()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("show-drawer-left", cx)
|
||||
.label("Left Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Left, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer-top", cx)
|
||||
.label("Top Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Top, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer", cx)
|
||||
.label("Right Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Right, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer", cx)
|
||||
.label("Bottom Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Bottom, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(self.selected_value.clone(), |this, selected_value| {
|
||||
this.child(
|
||||
h_flex().gap_1().child("You have selected:").child(
|
||||
div()
|
||||
.child(selected_value.to_string())
|
||||
.text_color(gpui::red()),
|
||||
),
|
||||
div()
|
||||
.id("modal-story")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_test_action))
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
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(
|
||||
Button::new("show-modal", cx)
|
||||
.label("Open Modal...")
|
||||
.on_click(cx.listener(|this, _, cx| this.show_modal(cx))),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("show-notify-info", cx)
|
||||
.label("Info Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification("You have been saved file successfully.")
|
||||
})),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child("Test Focus Back")
|
||||
.child(self.input2.clone())
|
||||
.child(
|
||||
Button::new("test-action", cx)
|
||||
.label("Test Dispatch Action")
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(TestAction));
|
||||
}).tooltip("This button for test dispatch action, to make sure when Modal close,\nthis still can handle the action."),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("show-drawer-left", cx)
|
||||
.label("Left Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Left, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer-top", cx)
|
||||
.label("Top Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Top, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer", cx)
|
||||
.label("Right Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Right, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-drawer", cx)
|
||||
.label("Bottom Drawer...")
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open_drawer_at(Placement::Bottom, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when_some(self.selected_value.clone(), |this, selected_value| {
|
||||
this.child(
|
||||
h_flex().gap_1().child("You have selected:").child(
|
||||
div()
|
||||
.child(selected_value.to_string())
|
||||
.text_color(gpui::red()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-error", cx)
|
||||
.label("Error Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification((
|
||||
})
|
||||
.child(
|
||||
Button::new("show-modal", cx)
|
||||
.label("Open Modal...")
|
||||
.on_click(cx.listener(|this, _, cx| this.show_modal(cx))),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("show-notify-info", cx)
|
||||
.label("Info Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification(
|
||||
"You have been saved file successfully.",
|
||||
)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-error", cx)
|
||||
.label("Error Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification((
|
||||
NotificationType::Error,
|
||||
"There have some error occurred. Please try again later.",
|
||||
))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-success", cx)
|
||||
.label("Success Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification((
|
||||
NotificationType::Success,
|
||||
"We have received your payment successfully.",
|
||||
))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-warning", cx)
|
||||
.label("Warning Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
struct WarningNotification;
|
||||
cx.push_notification(Notification::warning(
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-success", cx)
|
||||
.label("Success Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.push_notification((
|
||||
NotificationType::Success,
|
||||
"We have received your payment successfully.",
|
||||
))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-warning", cx)
|
||||
.label("Warning Notify...")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
struct WarningNotification;
|
||||
cx.push_notification(Notification::warning(
|
||||
"The network is not stable, please check your connection.",
|
||||
).id1::<WarningNotification>("test"))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-warning", cx)
|
||||
.label("Notification with Title")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
struct TestNotification;
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("show-notify-warning", cx)
|
||||
.label("Notification with Title")
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
struct TestNotification;
|
||||
|
||||
cx.push_notification(
|
||||
cx.push_notification(
|
||||
Notification::new(
|
||||
"你已经成功保存了文件,但是有一些警告信息需要你注意。",
|
||||
)
|
||||
|
|
@ -540,9 +567,9 @@ impl Render for ModalStory {
|
|||
}),
|
||||
),
|
||||
)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ impl super::Story for PopupStory {
|
|||
"A popup displays content on top of the main page."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use ui::{
|
|||
};
|
||||
|
||||
pub struct ProgressStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
value: f32,
|
||||
slider1: View<Slider>,
|
||||
slider1_value: f32,
|
||||
|
|
@ -26,8 +27,8 @@ impl super::Story for ProgressStory {
|
|||
"Progress"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ impl ProgressStory {
|
|||
.detach();
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
value: 50.,
|
||||
slider1_value: 15.,
|
||||
slider2_value: 1.,
|
||||
|
|
@ -75,6 +77,12 @@ impl ProgressStory {
|
|||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for ProgressStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProgressStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use ui::{
|
|||
};
|
||||
|
||||
pub struct ResizableStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
group1: View<ResizablePanelGroup>,
|
||||
group2: View<ResizablePanelGroup>,
|
||||
}
|
||||
|
|
@ -22,8 +23,14 @@ impl super::Story for ResizableStory {
|
|||
"The resizable panels."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for ResizableStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +104,11 @@ impl ResizableStory {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
Self { group1, group2 }
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
group1,
|
||||
group2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use ui::theme::ActiveTheme;
|
|||
use ui::{h_flex, v_flex, StyledExt as _};
|
||||
|
||||
pub struct ScrollableStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
scroll_handle: ScrollHandle,
|
||||
scroll_size: gpui::Size<Pixels>,
|
||||
scroll_state: Rc<Cell<ScrollbarState>>,
|
||||
|
|
@ -21,8 +22,9 @@ pub struct ScrollableStory {
|
|||
}
|
||||
|
||||
impl ScrollableStory {
|
||||
fn new() -> Self {
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
|
||||
scroll_size: gpui::Size::default(),
|
||||
|
|
@ -33,7 +35,7 @@ impl ScrollableStory {
|
|||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|_| Self::new())
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext<Self>) {
|
||||
|
|
@ -69,8 +71,14 @@ impl super::Story for ScrollableStory {
|
|||
"Add vertical or horizontal, or both scrollbars to a container."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for ScrollableStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ use ui::{
|
|||
v_flex, Disableable as _, Sizable, StyledExt,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SwitchStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
switch1: bool,
|
||||
switch2: bool,
|
||||
switch3: bool,
|
||||
|
|
@ -27,18 +27,19 @@ impl super::Story for SwitchStory {
|
|||
"A control that allows the user to toggle between two states."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl SwitchStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|cx| Self::new(cx))
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
pub fn new(_: &mut WindowContext) -> Self {
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
switch1: true,
|
||||
switch2: false,
|
||||
switch3: true,
|
||||
|
|
@ -46,6 +47,12 @@ impl SwitchStory {
|
|||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for SwitchStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SwitchStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
|
|
|
|||
|
|
@ -371,8 +371,14 @@ impl super::Story for TableStory {
|
|||
"A complex data table with selection, sorting, column moving, and loading more."
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for TableStory {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.table.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use ui::{
|
|||
use crate::section;
|
||||
|
||||
pub struct TextStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
check1: bool,
|
||||
check2: bool,
|
||||
check3: bool,
|
||||
|
|
@ -34,14 +35,15 @@ impl super::Story for TextStory {
|
|||
"The text render testing and examples"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl TextStory {
|
||||
pub(crate) fn new(_cx: &mut WindowContext) -> Self {
|
||||
pub(crate) fn new(cx: &mut WindowContext) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
check1: false,
|
||||
check2: false,
|
||||
check3: true,
|
||||
|
|
@ -60,7 +62,11 @@ impl TextStory {
|
|||
println!("Check value changed: {}", checked);
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for TextStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl Render for TextStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{
|
||||
div, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement,
|
||||
Styled, View, VisualContext as _, WindowContext,
|
||||
Styled, View, ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
|
|
@ -12,15 +12,19 @@ use ui::{
|
|||
v_flex,
|
||||
};
|
||||
|
||||
pub struct TooltipStory;
|
||||
pub struct TooltipStory {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
}
|
||||
|
||||
impl TooltipStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|cx| Self::new(cx))
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
fn new(_: &mut WindowContext) -> Self {
|
||||
Self {}
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,11 +33,15 @@ impl super::Story for TooltipStory {
|
|||
"Tooltip"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
impl gpui::FocusableView for TooltipStory {
|
||||
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TooltipStory {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
v_flex()
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ impl super::Story for WebViewStory {
|
|||
"WebView"
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> gpui::AnyView {
|
||||
Self::view(cx).into()
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use std::{collections::HashMap, sync::Arc};
|
|||
|
||||
use crate::popup_menu::PopupMenu;
|
||||
use gpui::{
|
||||
AnyView, AppContext, Axis, EventEmitter, FocusableView, Global, Hsla, Pixels, SharedString,
|
||||
View, VisualContext, WeakView, WindowContext,
|
||||
AnyElement, AnyView, AppContext, Axis, EventEmitter, FocusHandle, FocusableView, Global, Hsla,
|
||||
IntoElement, Pixels, SharedString, View, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rust_i18n::t;
|
||||
|
|
@ -29,9 +29,9 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
|||
/// Once you have defined a panel name, this must not be changed.
|
||||
fn panel_name(&self) -> &'static str;
|
||||
|
||||
/// The title of the panel, default is `None`.
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
t!("Dock.Unnamed").into()
|
||||
/// The title of the panel
|
||||
fn title(&self, _cx: &WindowContext) -> AnyElement {
|
||||
SharedString::from(t!("Dock.Unnamed")).into_any_element()
|
||||
}
|
||||
|
||||
/// The theme of the panel title, default is `None`.
|
||||
|
|
@ -56,7 +56,7 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
|||
}
|
||||
|
||||
pub trait PanelView: 'static + Send + Sync {
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString;
|
||||
fn title(&self, _cx: &WindowContext) -> AnyElement;
|
||||
|
||||
fn title_style(&self, _cx: &WindowContext) -> Option<TitleStyle>;
|
||||
|
||||
|
|
@ -66,11 +66,13 @@ pub trait PanelView: 'static + Send + Sync {
|
|||
|
||||
fn view(&self) -> AnyView;
|
||||
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState;
|
||||
}
|
||||
|
||||
impl<T: Panel> PanelView for View<T> {
|
||||
fn title(&self, cx: &WindowContext) -> SharedString {
|
||||
fn title(&self, cx: &WindowContext) -> AnyElement {
|
||||
self.read(cx).title(cx)
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +92,10 @@ impl<T: Panel> PanelView for View<T> {
|
|||
self.clone().into()
|
||||
}
|
||||
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.read(cx).focus_handle(cx)
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
self.read(cx).dump(cx)
|
||||
}
|
||||
|
|
@ -284,10 +290,9 @@ where
|
|||
.insert(panel_name.to_string(), Arc::new(deserialize));
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_deserialize_item_state() {
|
||||
let json = include_str!("../../tests/fixtures/layout.json");
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ impl Panel for StackPanel {
|
|||
"StackPanel"
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString {
|
||||
"StackPanel".into()
|
||||
fn title(&self, _cx: &gpui::WindowContext) -> gpui::AnyElement {
|
||||
"StackPanel".into_any_element()
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ use crate::{
|
|||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
tab::{Tab, TabBar},
|
||||
theme::ActiveTheme,
|
||||
tooltip::Tooltip,
|
||||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
|
||||
};
|
||||
|
||||
|
|
@ -74,10 +73,10 @@ impl Panel for TabPanel {
|
|||
"TabPanel"
|
||||
}
|
||||
|
||||
fn title(&self, cx: &WindowContext) -> gpui::SharedString {
|
||||
fn title(&self, cx: &WindowContext) -> gpui::AnyElement {
|
||||
self.active_panel()
|
||||
.map(|panel| panel.title(cx))
|
||||
.unwrap_or("Empty Tab".into())
|
||||
.unwrap_or("Empty Tab".into_any_element())
|
||||
}
|
||||
|
||||
fn closeable(&self, cx: &WindowContext) -> bool {
|
||||
|
|
@ -134,6 +133,7 @@ impl TabPanel {
|
|||
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.active_ix = ix;
|
||||
self.tab_bar_scroll_handle.scroll_to_item(ix);
|
||||
self.focus_active_panel(cx);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
|
@ -278,7 +278,6 @@ impl TabPanel {
|
|||
|
||||
if self.panels.len() == 1 {
|
||||
let panel = self.panels.get(0).unwrap();
|
||||
let title = panel.title(cx);
|
||||
let title_style = panel.title_style(cx);
|
||||
|
||||
return h_flex()
|
||||
|
|
@ -297,8 +296,7 @@ impl TabPanel {
|
|||
.min_w_16()
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.child(title.clone())
|
||||
.tooltip(move |cx| Tooltip::new(title.clone(), cx))
|
||||
.child(panel.title(cx))
|
||||
.on_drag(
|
||||
DragPanel {
|
||||
panel: panel.clone(),
|
||||
|
|
@ -576,6 +574,12 @@ impl TabPanel {
|
|||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
|
||||
fn focus_active_panel(&self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_panel) = self.active_panel() {
|
||||
active_panel.focus_handle(cx).focus(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||
self.is_zoomed = !self.is_zoomed;
|
||||
if self.is_zoomed {
|
||||
|
|
@ -593,17 +597,22 @@ impl TabPanel {
|
|||
}
|
||||
|
||||
impl FocusableView for TabPanel {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
if let Some(active_panel) = self.active_panel() {
|
||||
active_panel.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||
impl Render for TabPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
v_flex()
|
||||
.id("tab-panel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.track_focus(&focus_handle)
|
||||
.on_action(cx.listener(Self::on_action_toggle_zoom))
|
||||
.on_action(cx.listener(Self::on_action_close_panel))
|
||||
.size_full()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::{rc::Rc, time::Duration};
|
|||
|
||||
use gpui::{
|
||||
actions, anchored, div, hsla, prelude::FluentBuilder, px, relative, Animation,
|
||||
AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, Hsla, InteractiveElement,
|
||||
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla,
|
||||
InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
||||
RenderOnce, SharedString, Styled, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -28,11 +28,14 @@ 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,
|
||||
|
||||
/// This will be change when open the modal, the focus handle is create when open the modal.
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
pub(crate) layer_ix: usize,
|
||||
pub(crate) overlay_visible: bool,
|
||||
}
|
||||
|
||||
|
|
@ -62,14 +65,15 @@ impl Modal {
|
|||
|
||||
Self {
|
||||
base,
|
||||
focus_handle: cx.focus_handle(),
|
||||
title: None,
|
||||
footer: None,
|
||||
content: v_flex(),
|
||||
margin_top: None,
|
||||
offset_top: px(0.),
|
||||
width: px(480.),
|
||||
max_width: None,
|
||||
overlay: true,
|
||||
layer_ix: 0,
|
||||
overlay_visible: true,
|
||||
on_close: Rc::new(|_, _| {}),
|
||||
show_close: true,
|
||||
|
|
@ -146,13 +150,15 @@ impl Styled for Modal {
|
|||
|
||||
impl RenderOnce for Modal {
|
||||
fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement {
|
||||
let layer_ix = self.layer_ix;
|
||||
let on_close = self.on_close.clone();
|
||||
let view_size = cx.viewport_size();
|
||||
let bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: view_size,
|
||||
};
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + self.offset_top;
|
||||
let offset_top = px(layer_ix as f32 * 16.);
|
||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||
let x = bounds.center().x - self.width / 2.;
|
||||
|
||||
anchored().snap_to_window().child(
|
||||
|
|
@ -174,8 +180,9 @@ impl RenderOnce for Modal {
|
|||
})
|
||||
.child(
|
||||
self.base
|
||||
.id("modal")
|
||||
.id(SharedString::from(format!("modal-{layer_ix}")))
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action({
|
||||
let on_close = self.on_close.clone();
|
||||
move |_: &Escape, cx| {
|
||||
|
|
@ -199,17 +206,20 @@ impl RenderOnce for 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();
|
||||
}),
|
||||
Button::new(
|
||||
SharedString::from(format!("modal-close-{layer_ix}")),
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use gpui::{
|
||||
div, px, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render,
|
||||
Styled, View, ViewContext, VisualContext as _, WindowContext,
|
||||
div, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, Styled,
|
||||
View, ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
|
|
@ -54,7 +54,9 @@ impl<'a> ContextModal for WindowContext<'a> {
|
|||
F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static,
|
||||
{
|
||||
Root::update(self, move |root, cx| {
|
||||
root.previous_focus_handle = cx.focused();
|
||||
if root.active_drawer.is_none() {
|
||||
root.previous_focus_handle = cx.focused();
|
||||
}
|
||||
root.active_drawer = Some(Rc::new(build));
|
||||
cx.notify();
|
||||
})
|
||||
|
|
@ -77,8 +79,19 @@ impl<'a> ContextModal for WindowContext<'a> {
|
|||
F: Fn(Modal, &mut WindowContext) -> Modal + 'static,
|
||||
{
|
||||
Root::update(self, move |root, cx| {
|
||||
root.previous_focus_handle = cx.focused();
|
||||
root.active_modals.push(Rc::new(build));
|
||||
// Only save focus handle if there are no active modals.
|
||||
// This is used to restore focus when all modals are closed.
|
||||
if root.active_modals.len() == 0 {
|
||||
root.previous_focus_handle = cx.focused();
|
||||
}
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
focus_handle.focus(cx);
|
||||
|
||||
root.active_modals.push(ActiveModal {
|
||||
focus_handle,
|
||||
builder: Rc::new(build),
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
|
@ -90,7 +103,9 @@ impl<'a> ContextModal for WindowContext<'a> {
|
|||
fn close_modal(&mut self) {
|
||||
Root::update(self, move |root, cx| {
|
||||
root.active_modals.pop();
|
||||
root.focus_back(cx);
|
||||
if root.active_modals.len() == 0 {
|
||||
root.focus_back(cx);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
|
@ -180,11 +195,17 @@ pub struct Root {
|
|||
/// When the Modal, Drawer closes, we will focus back to the previous view.
|
||||
previous_focus_handle: Option<FocusHandle>,
|
||||
active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>,
|
||||
active_modals: Vec<Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>>,
|
||||
active_modals: Vec<ActiveModal>,
|
||||
pub notification: View<NotificationList>,
|
||||
child: AnyView,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActiveModal {
|
||||
focus_handle: FocusHandle,
|
||||
builder: Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>,
|
||||
}
|
||||
|
||||
impl Root {
|
||||
pub fn new(child: AnyView, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
|
|
@ -220,7 +241,7 @@ impl Root {
|
|||
}
|
||||
|
||||
fn focus_back(&mut self, cx: &mut WindowContext) {
|
||||
if let Some(handle) = self.previous_focus_handle.take() {
|
||||
if let Some(handle) = self.previous_focus_handle.clone() {
|
||||
cx.focus(&handle);
|
||||
}
|
||||
}
|
||||
|
|
@ -267,11 +288,27 @@ impl Root {
|
|||
return None;
|
||||
}
|
||||
|
||||
let modals_len = active_modals.len();
|
||||
|
||||
Some(
|
||||
div().children(active_modals.iter().enumerate().map(|(i, builder)| {
|
||||
div().children(active_modals.iter().enumerate().map(|(i, active_modal)| {
|
||||
let mut modal = Modal::new(cx);
|
||||
modal = builder(modal, cx);
|
||||
modal.offset_top = px(i as f32 * 16.);
|
||||
|
||||
modal = (active_modal.builder)(modal, cx);
|
||||
modal.layer_ix = i;
|
||||
// Give the modal the focus handle, because `modal` is a temporary value, is not possible to
|
||||
// keep the focus handle in the modal.
|
||||
//
|
||||
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
|
||||
modal.focus_handle = active_modal.focus_handle.clone();
|
||||
|
||||
// Focus to the top modal.
|
||||
if i == modals_len - 1 {
|
||||
// Check to avoid focus, when the modal is already focused.
|
||||
if !modal.focus_handle.contains_focused(cx) {
|
||||
cx.focus(&modal.focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only have one overlay, we only render the first modal with overlay.
|
||||
if has_overlay {
|
||||
|
|
|
|||
Loading…
Reference in a new issue