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:
Jason Lee 2024-09-12 19:51:33 +08:00 committed by GitHub
parent 9388b88f59
commit ce87c3a001
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 436 additions and 246 deletions

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
px, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext, px, ClickEvent, FocusableView, IntoElement, ParentElement as _, Render, Styled as _, View,
VisualContext as _, WindowContext, ViewContext, VisualContext as _, WindowContext,
}; };
use ui::{ use ui::{
@ -16,6 +16,7 @@ use ui::{
use crate::section; use crate::section;
pub struct ButtonStory { pub struct ButtonStory {
focus_handle: gpui::FocusHandle,
disabled: bool, disabled: bool,
loading: bool, loading: bool,
selected: bool, selected: bool,
@ -24,7 +25,8 @@ pub struct ButtonStory {
impl ButtonStory { impl ButtonStory {
pub fn view(cx: &mut WindowContext) -> View<Self> { pub fn view(cx: &mut WindowContext) -> View<Self> {
cx.new_view(|_| Self { cx.new_view(|cx| Self {
focus_handle: cx.focus_handle(),
disabled: false, disabled: false,
loading: false, loading: false,
selected: false, selected: false,
@ -50,8 +52,14 @@ impl super::Story for ButtonStory {
false false
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl FocusableView for ButtonStory {
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
} }
} }

View file

@ -26,8 +26,8 @@ impl super::Story for CalendarStory {
"A date picker and calendar component." "A date picker and calendar component."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() 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 { impl Render for CalendarStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex() v_flex()

View file

@ -64,8 +64,14 @@ impl super::Story for DropdownStory {
"Displays a list of options for the user to pick from—triggered by a button." "Displays a list of options for the user to pick from—triggered by a button."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl gpui::FocusableView for DropdownStory {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.fruit_dropdown.focus_handle(cx)
} }
} }

View file

@ -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::{ use ui::{
button::{Button, ButtonStyle}, button::{Button, ButtonStyle},
h_flex, h_flex,
@ -6,15 +8,19 @@ use ui::{
v_flex, Icon, IconName, v_flex, Icon, IconName,
}; };
pub struct IconStory {} pub struct IconStory {
focus_handle: gpui::FocusHandle,
}
impl IconStory { impl IconStory {
pub fn new(_: &WindowContext) -> Self { fn new(cx: &mut ViewContext<Self>) -> Self {
Self {} Self {
focus_handle: cx.focus_handle(),
}
} }
pub fn view(cx: &mut WindowContext) -> View<Self> { 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" "Icon use examples"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl gpui::FocusableView for IconStory {
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
} }
} }

View file

@ -5,6 +5,7 @@ const GOOGLE_LOGO: &str = include_str!("./fixtures/google.svg");
const PIE_JSON: &str = include_str!("./fixtures/pie.json"); const PIE_JSON: &str = include_str!("./fixtures/pie.json");
pub struct ImageStory { pub struct ImageStory {
focus_handle: gpui::FocusHandle,
google_logo: SvgImg, google_logo: SvgImg,
pie_chart: SvgImg, pie_chart: SvgImg,
inbox_img: SvgImg, inbox_img: SvgImg,
@ -15,16 +16,17 @@ impl super::Story for ImageStory {
"Image" "Image"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
impl ImageStory { impl ImageStory {
pub fn new(_: &WindowContext) -> Self { pub fn new(cx: &mut WindowContext) -> Self {
let chart = charts_rs::PieChart::from_json(PIE_JSON).unwrap(); let chart = charts_rs::PieChart::from_json(PIE_JSON).unwrap();
Self { Self {
focus_handle: cx.focus_handle(),
google_logo: svg_img().source(GOOGLE_LOGO.as_bytes(), px(300.), px(300.)), 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.)), 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.)), 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 { impl Render for ImageStory {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
v_flex() v_flex()

View file

@ -57,8 +57,8 @@ impl super::Story for InputStory {
false false
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
@ -220,6 +220,11 @@ impl FocusableCycle for InputStory {
.to_vec() .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 { impl Render for InputStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {

View file

@ -36,9 +36,10 @@ pub use tooltip_story::TooltipStory;
pub use webview_story::WebViewStory; pub use webview_story::WebViewStory;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder as _, px, AnyView, AppContext, Div, EventEmitter, actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Div,
FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render, SharedString, EventEmitter, FocusableView, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext, WindowContext, SharedString, StatefulInteractiveElement, Styled as _, View, ViewContext, VisualContext,
WindowContext,
}; };
use ui::{ use ui::{
@ -112,7 +113,7 @@ pub enum ContainerEvent {
Close, Close,
} }
pub trait Story { pub trait Story: FocusableView {
fn klass() -> &'static str { fn klass() -> &'static str {
std::any::type_name::<Self>().split("::").last().unwrap() std::any::type_name::<Self>().split("::").last().unwrap()
} }
@ -127,7 +128,7 @@ pub trait Story {
fn title_bg() -> Option<Hsla> { fn title_bg() -> Option<Hsla> {
None None
} }
fn new_view(cx: &mut WindowContext) -> AnyView; fn new_view(cx: &mut WindowContext) -> View<impl FocusableView>;
} }
impl EventEmitter<ContainerEvent> for StoryContainer {} impl EventEmitter<ContainerEvent> for StoryContainer {}
@ -155,9 +156,11 @@ impl StoryContainer {
let description = S::description(); let description = S::description();
let story = S::new_view(cx); let story = S::new_view(cx);
let story_klass = S::klass(); let story_klass = S::klass();
let focus_handle = story.focus_handle(cx);
let view = cx.new_view(|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.closeable = S::closeable();
story.name = name.into(); story.name = name.into();
story.description = description.into(); story.description = description.into();
@ -249,8 +252,8 @@ impl Panel for StoryContainer {
"StoryContainer" "StoryContainer"
} }
fn title(&self, _cx: &WindowContext) -> SharedString { fn title(&self, _cx: &WindowContext) -> AnyElement {
self.name.clone() self.name.clone().into_any_element()
} }
fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> { fn title_style(&self, cx: &WindowContext) -> Option<TitleStyle> {

View file

@ -205,8 +205,8 @@ impl super::Story for ListStory {
"A list displays a series of items." "A list displays a series of items."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }

View file

@ -2,9 +2,9 @@ use std::{sync::Arc, time::Duration};
use fake::Fake; use fake::Fake;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, IntoElement, ParentElement, actions, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
Render, SharedString, Styled, Task, Timer, View, ViewContext, VisualContext as _, WeakView, InteractiveElement as _, IntoElement, ParentElement, Render, SharedString, Styled, Task, Timer,
WindowContext, View, ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use ui::{ use ui::{
@ -19,6 +19,8 @@ use ui::{
v_flex, ContextModal as _, Icon, IconName, Placement, v_flex, ContextModal as _, Icon, IconName, Placement,
}; };
actions!(modal_story, [TestAction]);
pub struct ListItemDeletegate { pub struct ListItemDeletegate {
story: WeakView<ModalStory>, story: WeakView<ModalStory>,
confirmed_index: Option<usize>, confirmed_index: Option<usize>,
@ -145,6 +147,7 @@ pub struct ModalStory {
selected_value: Option<SharedString>, selected_value: Option<SharedString>,
list: View<List<ListItemDeletegate>>, list: View<List<ListItemDeletegate>>,
input1: View<TextInput>, input1: View<TextInput>,
input2: View<TextInput>,
date_picker: View<DatePicker>, date_picker: View<DatePicker>,
modal_overlay: bool, modal_overlay: bool,
model_show_close: bool, model_show_close: bool,
@ -160,8 +163,8 @@ impl super::Story for ModalStory {
"Modal & Drawer use examples" "Modal & Drawer use examples"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
@ -241,6 +244,7 @@ impl ModalStory {
}); });
let input1 = cx.new_view(|cx| TextInput::new(cx).placeholder("Your Name")); 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 = let date_picker =
cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth")); cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth"));
@ -250,6 +254,7 @@ impl ModalStory {
selected_value: None, selected_value: None,
list, list,
input1, input1,
input2,
date_picker, date_picker,
modal_overlay: true, modal_overlay: true,
model_show_close: true, model_show_close: true,
@ -268,7 +273,6 @@ impl ModalStory {
}; };
let overlay = self.modal_overlay; let overlay = self.modal_overlay;
input.focus_handle(cx).focus(cx);
cx.open_drawer(move |this, cx| { cx.open_drawer(move |this, cx| {
this.margin_top(px(33.)) this.margin_top(px(33.))
.placement(placement) .placement(placement)
@ -320,9 +324,8 @@ impl ModalStory {
let date_picker = self.date_picker.clone(); let date_picker = self.date_picker.clone();
let view = cx.view().clone(); let view = cx.view().clone();
input1.focus_handle(cx).focus(cx);
cx.open_modal(move |modal, cx| { cx.open_modal(move |modal, cx| {
input1.focus_handle(cx).focus(cx);
modal modal
.title("Form Modal") .title("Form Modal")
.overlay(overlay) .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 { impl FocusableView for ModalStory {
@ -396,7 +403,12 @@ impl FocusableView for ModalStory {
impl Render for ModalStory { impl Render for ModalStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child( div()
.id("modal-story")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_action_test_action))
.size_full()
.child(
v_flex() v_flex()
.gap_6() .gap_6()
.child( .child(
@ -431,6 +443,19 @@ impl Render for ModalStory {
})), })),
), ),
) )
.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( .child(
h_flex() h_flex()
.items_start() .items_start()
@ -485,7 +510,9 @@ impl Render for ModalStory {
Button::new("show-notify-info", cx) Button::new("show-notify-info", cx)
.label("Info Notify...") .label("Info Notify...")
.on_click(cx.listener(|_, _, cx| { .on_click(cx.listener(|_, _, cx| {
cx.push_notification("You have been saved file successfully.") cx.push_notification(
"You have been saved file successfully.",
)
})), })),
) )
.child( .child(

View file

@ -88,8 +88,8 @@ impl super::Story for PopupStory {
"A popup displays content on top of the main page." "A popup displays content on top of the main page."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }

View file

@ -14,6 +14,7 @@ use ui::{
}; };
pub struct ProgressStory { pub struct ProgressStory {
focus_handle: gpui::FocusHandle,
value: f32, value: f32,
slider1: View<Slider>, slider1: View<Slider>,
slider1_value: f32, slider1_value: f32,
@ -26,8 +27,8 @@ impl super::Story for ProgressStory {
"Progress" "Progress"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
@ -62,6 +63,7 @@ impl ProgressStory {
.detach(); .detach();
Self { Self {
focus_handle: cx.focus_handle(),
value: 50., value: 50.,
slider1_value: 15., slider1_value: 15.,
slider2_value: 1., 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 { impl Render for ProgressStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex() v_flex()

View file

@ -9,6 +9,7 @@ use ui::{
}; };
pub struct ResizableStory { pub struct ResizableStory {
focus_handle: gpui::FocusHandle,
group1: View<ResizablePanelGroup>, group1: View<ResizablePanelGroup>,
group2: View<ResizablePanelGroup>, group2: View<ResizablePanelGroup>,
} }
@ -22,8 +23,14 @@ impl super::Story for ResizableStory {
"The resizable panels." "The resizable panels."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() 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, cx,
) )
}); });
Self { group1, group2 } Self {
focus_handle: cx.focus_handle(),
group1,
group2,
}
} }
} }

View file

@ -12,6 +12,7 @@ use ui::theme::ActiveTheme;
use ui::{h_flex, v_flex, StyledExt as _}; use ui::{h_flex, v_flex, StyledExt as _};
pub struct ScrollableStory { pub struct ScrollableStory {
focus_handle: gpui::FocusHandle,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
scroll_size: gpui::Size<Pixels>, scroll_size: gpui::Size<Pixels>,
scroll_state: Rc<Cell<ScrollbarState>>, scroll_state: Rc<Cell<ScrollbarState>>,
@ -21,8 +22,9 @@ pub struct ScrollableStory {
} }
impl ScrollableStory { impl ScrollableStory {
fn new() -> Self { fn new(cx: &mut ViewContext<Self>) -> Self {
Self { Self {
focus_handle: cx.focus_handle(),
scroll_handle: ScrollHandle::new(), scroll_handle: ScrollHandle::new(),
scroll_state: Rc::new(Cell::new(ScrollbarState::default())), scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::Size::default(), scroll_size: gpui::Size::default(),
@ -33,7 +35,7 @@ impl ScrollableStory {
} }
pub fn view(cx: &mut WindowContext) -> View<Self> { 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>) { 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." "Add vertical or horizontal, or both scrollbars to a container."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl gpui::FocusableView for ScrollableStory {
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
} }
} }

View file

@ -11,8 +11,8 @@ use ui::{
v_flex, Disableable as _, Sizable, StyledExt, v_flex, Disableable as _, Sizable, StyledExt,
}; };
#[derive(Default)]
pub struct SwitchStory { pub struct SwitchStory {
focus_handle: gpui::FocusHandle,
switch1: bool, switch1: bool,
switch2: bool, switch2: bool,
switch3: bool, switch3: bool,
@ -27,18 +27,19 @@ impl super::Story for SwitchStory {
"A control that allows the user to toggle between two states." "A control that allows the user to toggle between two states."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
impl SwitchStory { impl SwitchStory {
pub fn view(cx: &mut WindowContext) -> View<Self> { 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 { Self {
focus_handle: cx.focus_handle(),
switch1: true, switch1: true,
switch2: false, switch2: false,
switch3: true, 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 { impl Render for SwitchStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.theme(); let theme = cx.theme();

View file

@ -371,8 +371,14 @@ impl super::Story for TableStory {
"A complex data table with selection, sorting, column moving, and loading more." "A complex data table with selection, sorting, column moving, and loading more."
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl gpui::FocusableView for TableStory {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.table.focus_handle(cx)
} }
} }

View file

@ -17,6 +17,7 @@ use ui::{
use crate::section; use crate::section;
pub struct TextStory { pub struct TextStory {
focus_handle: gpui::FocusHandle,
check1: bool, check1: bool,
check2: bool, check2: bool,
check3: bool, check3: bool,
@ -34,14 +35,15 @@ impl super::Story for TextStory {
"The text render testing and examples" "The text render testing and examples"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }
impl TextStory { impl TextStory {
pub(crate) fn new(_cx: &mut WindowContext) -> Self { pub(crate) fn new(cx: &mut WindowContext) -> Self {
Self { Self {
focus_handle: cx.focus_handle(),
check1: false, check1: false,
check2: false, check2: false,
check3: true, check3: true,
@ -60,7 +62,11 @@ impl TextStory {
println!("Check value changed: {}", checked); 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 { impl Render for TextStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex() v_flex()

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement, div, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement,
Styled, View, VisualContext as _, WindowContext, Styled, View, ViewContext, VisualContext as _, WindowContext,
}; };
use ui::{ use ui::{
@ -12,15 +12,19 @@ use ui::{
v_flex, v_flex,
}; };
pub struct TooltipStory; pub struct TooltipStory {
focus_handle: gpui::FocusHandle,
}
impl TooltipStory { impl TooltipStory {
pub fn view(cx: &mut WindowContext) -> View<Self> { 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 { fn new(cx: &mut ViewContext<Self>) -> Self {
Self {} Self {
focus_handle: cx.focus_handle(),
}
} }
} }
@ -29,11 +33,15 @@ impl super::Story for TooltipStory {
"Tooltip" "Tooltip"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
}
}
impl gpui::FocusableView for TooltipStory {
fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
} }
} }
impl Render for TooltipStory { impl Render for TooltipStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
v_flex() v_flex()

View file

@ -23,8 +23,8 @@ impl super::Story for WebViewStory {
"WebView" "WebView"
} }
fn new_view(cx: &mut WindowContext) -> gpui::AnyView { fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
Self::view(cx).into() Self::view(cx)
} }
} }

View file

@ -2,8 +2,8 @@ use std::{collections::HashMap, sync::Arc};
use crate::popup_menu::PopupMenu; use crate::popup_menu::PopupMenu;
use gpui::{ use gpui::{
AnyView, AppContext, Axis, EventEmitter, FocusableView, Global, Hsla, Pixels, SharedString, AnyElement, AnyView, AppContext, Axis, EventEmitter, FocusHandle, FocusableView, Global, Hsla,
View, VisualContext, WeakView, WindowContext, IntoElement, Pixels, SharedString, View, VisualContext, WeakView, WindowContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use rust_i18n::t; 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. /// Once you have defined a panel name, this must not be changed.
fn panel_name(&self) -> &'static str; fn panel_name(&self) -> &'static str;
/// The title of the panel, default is `None`. /// The title of the panel
fn title(&self, _cx: &WindowContext) -> SharedString { fn title(&self, _cx: &WindowContext) -> AnyElement {
t!("Dock.Unnamed").into() SharedString::from(t!("Dock.Unnamed")).into_any_element()
} }
/// The theme of the panel title, default is `None`. /// 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 { 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>; fn title_style(&self, _cx: &WindowContext) -> Option<TitleStyle>;
@ -66,11 +66,13 @@ pub trait PanelView: 'static + Send + Sync {
fn view(&self) -> AnyView; fn view(&self) -> AnyView;
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
fn dump(&self, cx: &AppContext) -> DockItemState; fn dump(&self, cx: &AppContext) -> DockItemState;
} }
impl<T: Panel> PanelView for View<T> { impl<T: Panel> PanelView for View<T> {
fn title(&self, cx: &WindowContext) -> SharedString { fn title(&self, cx: &WindowContext) -> AnyElement {
self.read(cx).title(cx) self.read(cx).title(cx)
} }
@ -90,6 +92,10 @@ impl<T: Panel> PanelView for View<T> {
self.clone().into() self.clone().into()
} }
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.read(cx).focus_handle(cx)
}
fn dump(&self, cx: &AppContext) -> DockItemState { fn dump(&self, cx: &AppContext) -> DockItemState {
self.read(cx).dump(cx) self.read(cx).dump(cx)
} }
@ -284,10 +290,9 @@ where
.insert(panel_name.to_string(), Arc::new(deserialize)); .insert(panel_name.to_string(), Arc::new(deserialize));
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_deserialize_item_state() { fn test_deserialize_item_state() {
let json = include_str!("../../tests/fixtures/layout.json"); let json = include_str!("../../tests/fixtures/layout.json");

View file

@ -32,8 +32,8 @@ impl Panel for StackPanel {
"StackPanel" "StackPanel"
} }
fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString { fn title(&self, _cx: &gpui::WindowContext) -> gpui::AnyElement {
"StackPanel".into() "StackPanel".into_any_element()
} }
fn dump(&self, cx: &AppContext) -> DockItemState { fn dump(&self, cx: &AppContext) -> DockItemState {

View file

@ -15,7 +15,6 @@ use crate::{
popup_menu::{PopupMenu, PopupMenuExt}, popup_menu::{PopupMenu, PopupMenuExt},
tab::{Tab, TabBar}, tab::{Tab, TabBar},
theme::ActiveTheme, theme::ActiveTheme,
tooltip::Tooltip,
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
}; };
@ -74,10 +73,10 @@ impl Panel for TabPanel {
"TabPanel" "TabPanel"
} }
fn title(&self, cx: &WindowContext) -> gpui::SharedString { fn title(&self, cx: &WindowContext) -> gpui::AnyElement {
self.active_panel() self.active_panel()
.map(|panel| panel.title(cx)) .map(|panel| panel.title(cx))
.unwrap_or("Empty Tab".into()) .unwrap_or("Empty Tab".into_any_element())
} }
fn closeable(&self, cx: &WindowContext) -> bool { fn closeable(&self, cx: &WindowContext) -> bool {
@ -134,6 +133,7 @@ impl TabPanel {
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) { fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
self.active_ix = ix; self.active_ix = ix;
self.tab_bar_scroll_handle.scroll_to_item(ix); self.tab_bar_scroll_handle.scroll_to_item(ix);
self.focus_active_panel(cx);
cx.emit(PanelEvent::LayoutChanged); cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
@ -278,7 +278,6 @@ impl TabPanel {
if self.panels.len() == 1 { if self.panels.len() == 1 {
let panel = self.panels.get(0).unwrap(); let panel = self.panels.get(0).unwrap();
let title = panel.title(cx);
let title_style = panel.title_style(cx); let title_style = panel.title_style(cx);
return h_flex() return h_flex()
@ -297,8 +296,7 @@ impl TabPanel {
.min_w_16() .min_w_16()
.overflow_hidden() .overflow_hidden()
.text_ellipsis() .text_ellipsis()
.child(title.clone()) .child(panel.title(cx))
.tooltip(move |cx| Tooltip::new(title.clone(), cx))
.on_drag( .on_drag(
DragPanel { DragPanel {
panel: panel.clone(), panel: panel.clone(),
@ -576,6 +574,12 @@ impl TabPanel {
cx.emit(PanelEvent::LayoutChanged); 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>) { fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
self.is_zoomed = !self.is_zoomed; self.is_zoomed = !self.is_zoomed;
if self.is_zoomed { if self.is_zoomed {
@ -593,17 +597,22 @@ impl TabPanel {
} }
impl FocusableView for TabPanel { impl FocusableView for TabPanel {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { 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() self.focus_handle.clone()
} }
}
} }
impl EventEmitter<DismissEvent> for TabPanel {} impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {} impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel { impl Render for TabPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx);
v_flex() v_flex()
.id("tab-panel") .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_toggle_zoom))
.on_action(cx.listener(Self::on_action_close_panel)) .on_action(cx.listener(Self::on_action_close_panel))
.size_full() .size_full()

View file

@ -2,9 +2,9 @@ use std::{rc::Rc, time::Duration};
use gpui::{ use gpui::{
actions, anchored, div, hsla, prelude::FluentBuilder, px, relative, Animation, actions, anchored, div, hsla, prelude::FluentBuilder, px, relative, Animation,
AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, Hsla, InteractiveElement, AnimationExt as _, AnyElement, AppContext, Bounds, ClickEvent, Div, FocusHandle, Hsla,
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, Styled, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
WindowContext, RenderOnce, SharedString, Styled, WindowContext,
}; };
use crate::{ use crate::{
@ -28,11 +28,14 @@ pub struct Modal {
width: Pixels, width: Pixels,
max_width: Option<Pixels>, max_width: Option<Pixels>,
margin_top: Option<Pixels>, margin_top: Option<Pixels>,
/// Used to offset the modal from the top when modal is a sub-modal.
pub(crate) offset_top: Pixels,
on_close: Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>, on_close: Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
show_close: bool, show_close: bool,
overlay: bool, overlay: bool,
/// 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, pub(crate) overlay_visible: bool,
} }
@ -62,14 +65,15 @@ impl Modal {
Self { Self {
base, base,
focus_handle: cx.focus_handle(),
title: None, title: None,
footer: None, footer: None,
content: v_flex(), content: v_flex(),
margin_top: None, margin_top: None,
offset_top: px(0.),
width: px(480.), width: px(480.),
max_width: None, max_width: None,
overlay: true, overlay: true,
layer_ix: 0,
overlay_visible: true, overlay_visible: true,
on_close: Rc::new(|_, _| {}), on_close: Rc::new(|_, _| {}),
show_close: true, show_close: true,
@ -146,13 +150,15 @@ impl Styled for Modal {
impl RenderOnce for Modal { impl RenderOnce for Modal {
fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement { fn render(self, cx: &mut WindowContext) -> impl gpui::IntoElement {
let layer_ix = self.layer_ix;
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
let view_size = cx.viewport_size(); let view_size = cx.viewport_size();
let bounds = Bounds { let bounds = Bounds {
origin: Point::default(), origin: Point::default(),
size: view_size, 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.; let x = bounds.center().x - self.width / 2.;
anchored().snap_to_window().child( anchored().snap_to_window().child(
@ -174,8 +180,9 @@ impl RenderOnce for Modal {
}) })
.child( .child(
self.base self.base
.id("modal") .id(SharedString::from(format!("modal-{layer_ix}")))
.key_context(CONTEXT) .key_context(CONTEXT)
.track_focus(&self.focus_handle)
.on_action({ .on_action({
let on_close = self.on_close.clone(); let on_close = self.on_close.clone();
move |_: &Escape, cx| { move |_: &Escape, cx| {
@ -199,7 +206,10 @@ impl RenderOnce for Modal {
}) })
.when(self.show_close, |this| { .when(self.show_close, |this| {
this.child( this.child(
Button::new("close", cx) Button::new(
SharedString::from(format!("modal-close-{layer_ix}")),
cx,
)
.absolute() .absolute()
.top_2() .top_2()
.right_2() .right_2()

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, px, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, div, AnyView, FocusHandle, InteractiveElement, IntoElement, ParentElement as _, Render, Styled,
Styled, View, ViewContext, VisualContext as _, WindowContext, View, ViewContext, VisualContext as _, WindowContext,
}; };
use std::{ use std::{
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
@ -54,7 +54,9 @@ impl<'a> ContextModal for WindowContext<'a> {
F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static, F: Fn(Drawer, &mut WindowContext) -> Drawer + 'static,
{ {
Root::update(self, move |root, cx| { Root::update(self, move |root, cx| {
if root.active_drawer.is_none() {
root.previous_focus_handle = cx.focused(); root.previous_focus_handle = cx.focused();
}
root.active_drawer = Some(Rc::new(build)); root.active_drawer = Some(Rc::new(build));
cx.notify(); cx.notify();
}) })
@ -77,8 +79,19 @@ impl<'a> ContextModal for WindowContext<'a> {
F: Fn(Modal, &mut WindowContext) -> Modal + 'static, F: Fn(Modal, &mut WindowContext) -> Modal + 'static,
{ {
Root::update(self, move |root, cx| { Root::update(self, move |root, cx| {
// 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(); root.previous_focus_handle = cx.focused();
root.active_modals.push(Rc::new(build)); }
let focus_handle = cx.focus_handle();
focus_handle.focus(cx);
root.active_modals.push(ActiveModal {
focus_handle,
builder: Rc::new(build),
});
cx.notify(); cx.notify();
}) })
} }
@ -90,7 +103,9 @@ impl<'a> ContextModal for WindowContext<'a> {
fn close_modal(&mut self) { fn close_modal(&mut self) {
Root::update(self, move |root, cx| { Root::update(self, move |root, cx| {
root.active_modals.pop(); root.active_modals.pop();
if root.active_modals.len() == 0 {
root.focus_back(cx); root.focus_back(cx);
}
cx.notify(); cx.notify();
}) })
} }
@ -180,11 +195,17 @@ pub struct Root {
/// When the Modal, Drawer closes, we will focus back to the previous view. /// When the Modal, Drawer closes, we will focus back to the previous view.
previous_focus_handle: Option<FocusHandle>, previous_focus_handle: Option<FocusHandle>,
active_drawer: Option<Rc<dyn Fn(Drawer, &mut WindowContext) -> Drawer + 'static>>, 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>, pub notification: View<NotificationList>,
child: AnyView, child: AnyView,
} }
#[derive(Clone)]
struct ActiveModal {
focus_handle: FocusHandle,
builder: Rc<dyn Fn(Modal, &mut WindowContext) -> Modal + 'static>,
}
impl Root { impl Root {
pub fn new(child: AnyView, cx: &mut ViewContext<Self>) -> Self { pub fn new(child: AnyView, cx: &mut ViewContext<Self>) -> Self {
Self { Self {
@ -220,7 +241,7 @@ impl Root {
} }
fn focus_back(&mut self, cx: &mut WindowContext) { 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); cx.focus(&handle);
} }
} }
@ -267,11 +288,27 @@ impl Root {
return None; return None;
} }
let modals_len = active_modals.len();
Some( 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); 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. // Keep only have one overlay, we only render the first modal with overlay.
if has_overlay { if has_overlay {