From ce87c3a00138d83ddf1317b7f7bf3c5695c28af6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 12 Sep 2024 19:51:33 +0800 Subject: [PATCH] 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`. --- crates/story/src/button_story.rs | 18 +- crates/story/src/calendar_story.rs | 10 +- crates/story/src/dropdown_story.rs | 10 +- crates/story/src/icon_story.rs | 26 ++- crates/story/src/image_story.rs | 14 +- crates/story/src/input_story.rs | 9 +- crates/story/src/lib.rs | 19 +- crates/story/src/list_story.rs | 4 +- crates/story/src/modal_story.rs | 293 +++++++++++++++------------ crates/story/src/popup_story.rs | 4 +- crates/story/src/progress_story.rs | 12 +- crates/story/src/resizable_story.rs | 17 +- crates/story/src/scrollable_story.rs | 16 +- crates/story/src/switch_story.rs | 17 +- crates/story/src/table_story.rs | 10 +- crates/story/src/text_story.rs | 14 +- crates/story/src/tooltip_story.rs | 24 ++- crates/story/src/webview_story.rs | 4 +- crates/ui/src/dock/panel.rs | 23 ++- crates/ui/src/dock/stack_panel.rs | 4 +- crates/ui/src/dock/tab_panel.rs | 27 ++- crates/ui/src/modal.rs | 48 +++-- crates/ui/src/root.rs | 59 +++++- 23 files changed, 436 insertions(+), 246 deletions(-) diff --git a/crates/story/src/button_story.rs b/crates/story/src/button_story.rs index c07f98ce..ec6cc90d 100644 --- a/crates/story/src/button_story.rs +++ b/crates/story/src/button_story.rs @@ -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 { - 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 { + Self::view(cx) + } +} + +impl FocusableView for ButtonStory { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() } } diff --git a/crates/story/src/calendar_story.rs b/crates/story/src/calendar_story.rs index faebc2d5..5281a159 100644 --- a/crates/story/src/calendar_story.rs +++ b/crates/story/src/calendar_story.rs @@ -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 { + 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) -> impl IntoElement { v_flex() diff --git a/crates/story/src/dropdown_story.rs b/crates/story/src/dropdown_story.rs index a62346f9..cd79c67e 100644 --- a/crates/story/src/dropdown_story.rs +++ b/crates/story/src/dropdown_story.rs @@ -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 { + Self::view(cx) + } +} + +impl gpui::FocusableView for DropdownStory { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.fruit_dropdown.focus_handle(cx) } } diff --git a/crates/story/src/icon_story.rs b/crates/story/src/icon_story.rs index 1f5ccd41..11cfad2d 100644 --- a/crates/story/src/icon_story.rs +++ b/crates/story/src/icon_story.rs @@ -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 { + focus_handle: cx.focus_handle(), + } } pub fn view(cx: &mut WindowContext) -> View { - 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 { + Self::view(cx) + } +} + +impl gpui::FocusableView for IconStory { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() } } diff --git a/crates/story/src/image_story.rs b/crates/story/src/image_story.rs index 1be285a1..b0d020bc 100644 --- a/crates/story/src/image_story.rs +++ b/crates/story/src/image_story.rs @@ -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 { + 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) -> impl gpui::IntoElement { v_flex() diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index aea1451e..49661e95 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -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 { + 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) -> impl IntoElement { diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index bb15dec0..4426fca2 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -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::().split("::").last().unwrap() } @@ -127,7 +128,7 @@ pub trait Story { fn title_bg() -> Option { None } - fn new_view(cx: &mut WindowContext) -> AnyView; + fn new_view(cx: &mut WindowContext) -> View; } impl EventEmitter 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 { diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index eec701a9..d4347444 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -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 { + Self::view(cx) } } diff --git a/crates/story/src/modal_story.rs b/crates/story/src/modal_story.rs index e2ed0717..67ef8867 100644 --- a/crates/story/src/modal_story.rs +++ b/crates/story/src/modal_story.rs @@ -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, confirmed_index: Option, @@ -145,6 +147,7 @@ pub struct ModalStory { selected_value: Option, list: View>, input1: View, + input2: View, date_picker: View, 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 { + 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) { + 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) -> 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::("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 { }), ), ) - })), - ), - ), - ) + })), + ), + ), + ) } } diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index 28ee3094..189aca34 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -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 { + Self::view(cx) } } diff --git a/crates/story/src/progress_story.rs b/crates/story/src/progress_story.rs index 1301e82a..a701f31d 100644 --- a/crates/story/src/progress_story.rs +++ b/crates/story/src/progress_story.rs @@ -14,6 +14,7 @@ use ui::{ }; pub struct ProgressStory { + focus_handle: gpui::FocusHandle, value: f32, slider1: View, 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 { + 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) -> impl IntoElement { v_flex() diff --git a/crates/story/src/resizable_story.rs b/crates/story/src/resizable_story.rs index 2a26fdf1..34396f92 100644 --- a/crates/story/src/resizable_story.rs +++ b/crates/story/src/resizable_story.rs @@ -9,6 +9,7 @@ use ui::{ }; pub struct ResizableStory { + focus_handle: gpui::FocusHandle, group1: View, group2: View, } @@ -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 { + 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, + } } } diff --git a/crates/story/src/scrollable_story.rs b/crates/story/src/scrollable_story.rs index f0bb02b2..0c7de7c6 100644 --- a/crates/story/src/scrollable_story.rs +++ b/crates/story/src/scrollable_story.rs @@ -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, scroll_state: Rc>, @@ -21,8 +22,9 @@ pub struct ScrollableStory { } impl ScrollableStory { - fn new() -> Self { + fn new(cx: &mut ViewContext) -> 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 { - cx.new_view(|_| Self::new()) + cx.new_view(Self::new) } pub fn change_test_cases(&mut self, n: usize, cx: &mut ViewContext) { @@ -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 { + Self::view(cx) + } +} + +impl gpui::FocusableView for ScrollableStory { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() } } diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index 2b50441f..34546c6c 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -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 { + Self::view(cx) } } impl SwitchStory { pub fn view(cx: &mut WindowContext) -> View { - 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 { + 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) -> impl IntoElement { let theme = cx.theme(); diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index d7dc0358..a1c5edb2 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -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 { + Self::view(cx) + } +} + +impl gpui::FocusableView for TableStory { + fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle { + self.table.focus_handle(cx) } } diff --git a/crates/story/src/text_story.rs b/crates/story/src/text_story.rs index 0f2ebd11..e5386ebe 100644 --- a/crates/story/src/text_story.rs +++ b/crates/story/src/text_story.rs @@ -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 { + 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) -> impl IntoElement { v_flex() diff --git a/crates/story/src/tooltip_story.rs b/crates/story/src/tooltip_story.rs index b658896b..40e0bbc8 100644 --- a/crates/story/src/tooltip_story.rs +++ b/crates/story/src/tooltip_story.rs @@ -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 { - 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 { + 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 { + 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) -> impl gpui::IntoElement { v_flex() diff --git a/crates/story/src/webview_story.rs b/crates/story/src/webview_story.rs index 203b4406..5ad13fac 100644 --- a/crates/story/src/webview_story.rs +++ b/crates/story/src/webview_story.rs @@ -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 { + Self::view(cx) } } diff --git a/crates/ui/src/dock/panel.rs b/crates/ui/src/dock/panel.rs index bf194e86..c09b7514 100644 --- a/crates/ui/src/dock/panel.rs +++ b/crates/ui/src/dock/panel.rs @@ -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 + 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 + 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; @@ -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 PanelView for View { - fn title(&self, cx: &WindowContext) -> SharedString { + fn title(&self, cx: &WindowContext) -> AnyElement { self.read(cx).title(cx) } @@ -90,6 +92,10 @@ impl PanelView for View { 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"); diff --git a/crates/ui/src/dock/stack_panel.rs b/crates/ui/src/dock/stack_panel.rs index f368001b..de7d7c22 100644 --- a/crates/ui/src/dock/stack_panel.rs +++ b/crates/ui/src/dock/stack_panel.rs @@ -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 { diff --git a/crates/ui/src/dock/tab_panel.rs b/crates/ui/src/dock/tab_panel.rs index 8383b5b6..c8dcd618 100644 --- a/crates/ui/src/dock/tab_panel.rs +++ b/crates/ui/src/dock/tab_panel.rs @@ -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.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) { + 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.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 for TabPanel {} impl EventEmitter for TabPanel {} impl Render for TabPanel { fn render(&mut self, cx: &mut ViewContext) -> 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() diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 8fe4d4b3..7deaf7ef 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -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, 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, + + /// 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) diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index a2e4cf9c..23ec8ba6 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -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, active_drawer: Option Drawer + 'static>>, - active_modals: Vec Modal + 'static>>, + active_modals: Vec, pub notification: View, child: AnyView, } +#[derive(Clone)] +struct ActiveModal { + focus_handle: FocusHandle, + builder: Rc Modal + 'static>, +} + impl Root { pub fn new(child: AnyView, cx: &mut ViewContext) -> 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 {