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::{
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()
}
}

View file

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

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."
}
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)
}
}

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::{
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()
}
}

View file

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

View file

@ -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 {

View file

@ -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> {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

@ -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 {