diff --git a/Cargo.lock b/Cargo.lock index 3b45a329183efcbf686be97aaefb7dbf29990fb0..1747eae2d25fb958fa096a748793a3b0f24ab02b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8823,6 +8823,17 @@ dependencies = [ "util", ] +[[package]] +name = "storybook3" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "settings2", + "theme2", + "ui2", +] + [[package]] name = "stringprep" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index f8d0af77fa85220f348c866edfe5242d0cccbeec..f107dc5390af2ec57b62e2f3b6cf3ac16b9316c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ members = [ "crates/sqlez_macros", "crates/rich_text", "crates/storybook2", + "crates/storybook3", "crates/sum_tree", "crates/terminal", "crates/terminal2", diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 6264606ed9c7db0116d475ac48a57eef872434a5..9463cab68ca1b76984f372573e1271cb6fac76fc 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,9 +1,8 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, - Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke, + ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -16,7 +15,7 @@ use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, }; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; use zed_actions::OpenZedURL; actions!(Toggle); @@ -69,10 +68,9 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} -impl Modal for CommandPalette { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)); +impl ManagedView for CommandPalette { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } @@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index b2850761a97a373f9fcdb6b01ea0c5c9acd80b22..0fee5102e6d0314d134848eb3abf5697d71003d5 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,9 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model, - ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent, + ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, + WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -19,7 +19,7 @@ use text::Point; use theme::ActiveTheme; use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -111,10 +111,9 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} -impl Modal for FileFinder { - fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| picker.focus(cx)) +impl ManagedView for FileFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) } } impl Render for FileFinder { @@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - finder - .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) - .ok()?; + finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?; Some(()) }) @@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index ccd6b7ada2141ef0cf0d02ba3bbae34cc6a926f2..565afb5e939f01225341ae84e1628ead5daf5cbd 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,13 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString, - Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent, + Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{Modal, ModalEvent, Workspace}; +use workspace::Workspace; actions!(Toggle); @@ -23,10 +23,9 @@ pub struct GoToLine { _subscriptions: Vec, } -impl EventEmitter for GoToLine {} -impl Modal for GoToLine { - fn focus(&self, cx: &mut WindowContext) { - self.line_editor.update(cx, |editor, cx| editor.focus(cx)) +impl ManagedView for GoToLine { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.line_editor.focus_handle(cx) } } @@ -88,7 +87,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::Event::Blurred => cx.emit(ModalEvent::Dismissed), + editor::Event::Blurred => cx.emit(Dismiss), editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -123,7 +122,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +139,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ebbc34a48a08d7cab7f2978c8d5bdf730f569e68..f9560f2c53188a17b6c336e9bb284ee3ec58b07f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -960,11 +960,11 @@ where cx.background_executor().timer(TOOLTIP_DELAY).await; view.update(&mut cx, move |view_state, cx| { active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), cursor_offset: cx.mouse_position(), }), + _task: None, }); cx.notify(); }) @@ -972,12 +972,17 @@ where } }); active_tooltip.borrow_mut().replace(ActiveTooltip { - waiting: Some(task), tooltip: None, + _task: Some(task), }); } }); + let active_tooltip = element_state.active_tooltip.clone(); + cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { + active_tooltip.borrow_mut().take(); + }); + if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() { if active_tooltip.tooltip.is_some() { cx.active_tooltip = active_tooltip.tooltip.clone() @@ -1207,9 +1212,8 @@ pub struct InteractiveElementState { } pub struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, tooltip: Option, + _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index a190337f04dbfe5a4acd908aaf4a646c33a49240..14a8048d398176bbd8bec49b37bc96b261450ed9 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,9 @@ use smallvec::SmallVec; +use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point, - Size, Style, + point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, + Point, Size, Style, }; pub struct OverlayState { @@ -14,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -25,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -35,6 +37,13 @@ impl Overlay { self } + /// Sets the position in window co-ordinates + /// (otherwise the location the overlay is rendered is used) + pub fn position(mut self, anchor: Point) -> Self { + self.anchor_position = Some(anchor); + self + } + /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = OverlayFitMode::SnapToWindow; @@ -48,6 +57,12 @@ impl ParentComponent for Overlay { } } +impl Component for Overlay { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + impl Element for Overlay { type ElementState = OverlayState; @@ -66,7 +81,12 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + + let mut overlay_style = Style::default(); + overlay_style.position = Position::Absolute; + overlay_style.display = Display::Flex; + + let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); (layout_id, OverlayState { child_layout_ids }) } @@ -90,7 +110,7 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (child_max - child_min).into(); - let origin = bounds.origin; + let origin = self.anchor_position.unwrap_or(bounds.origin); let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { @@ -184,6 +204,15 @@ impl AnchorCorner { Bounds { origin, size } } + pub fn corner(&self, bounds: Bounds) -> Point { + match self { + Self::TopLeft => bounds.origin, + Self::TopRight => bounds.upper_right(), + Self::BottomLeft => bounds.lower_left(), + Self::BottomRight => bounds.lower_right(), + } + } + fn switch_axis(self, axis: Axis) -> Self { match axis { Axis::Vertical => match self { diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index d07df3d94b352bced85f5dfaa853ff788faf5876..03782d13a84a0cb36e681a6a06470054c61e28e5 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) }; if let Some(mut event) = event { - let synthesized_second_event = match &mut event { + match &mut event { InputEvent::MouseDown( event @ MouseDownEvent { button: MouseButton::Left, @@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { .. }, ) => { + // On mac, a ctrl-left click should be handled as a right click. *event = MouseDownEvent { button: MouseButton::Right, modifiers: Modifiers { @@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { click_count: 1, ..*event }; - - Some(InputEvent::MouseDown(MouseDownEvent { - button: MouseButton::Right, - ..*event - })) } // Because we map a ctrl-left_down to a right_down -> right_up let's ignore // the ctrl-left_up to avoid having a mismatch in button down/up events if the // user is still holding ctrl when releasing the left mouse button - InputEvent::MouseUp(MouseUpEvent { - button: MouseButton::Left, - modifiers: Modifiers { control: true, .. }, - .. - }) => { - lock.synthetic_drag_counter += 1; - return; + InputEvent::MouseUp( + event @ MouseUpEvent { + button: MouseButton::Left, + modifiers: Modifiers { control: true, .. }, + .. + }, + ) => { + *event = MouseUpEvent { + button: MouseButton::Right, + modifiers: Modifiers { + control: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; } - _ => None, + _ => {} }; match &event { @@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { if let Some(mut callback) = lock.event_callback.take() { drop(lock); callback(event); - if let Some(event) = synthesized_second_event { - callback(event); - } window_state.lock().event_callback = Some(callback); } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index ff4c13abce5148768ba89549370173b6b34cb44a..6d07f06d9441b838828f7cf15ab0c2a6da72ff4e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -185,10 +185,27 @@ impl Drop for FocusHandle { } } +/// FocusableView allows users of your view to easily +/// focus it (using cx.focus_view(view)) pub trait FocusableView: Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } +/// ManagedView is a view (like a Modal, Popover, Menu, etc.) +/// where the lifecycle of the view is handled by another view. +pub trait ManagedView: Render { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle; +} + +pub struct Dismiss; +impl EventEmitter for T {} + +impl FocusableView for T { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.focus_handle(cx) + } +} + // Holds the state for a specific window. pub struct Window { pub(crate) handle: AnyWindowHandle, @@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> { result } + #[must_use] /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which /// layout is being requested, along with the layout ids of any children. This method is called during /// calls to the `Element::layout` trait method and enables any element to participate in layout. @@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = mouse_move.position; InputEvent::MouseMove(mouse_move) } + InputEvent::MouseDown(mouse_down) => { + self.window.mouse_position = mouse_down.position; + InputEvent::MouseDown(mouse_down) + } + InputEvent::MouseUp(mouse_up) => { + self.window.mouse_position = mouse_up.position; + InputEvent::MouseUp(mouse_up) + } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. InputEvent::FileDrop(file_drop) => match file_drop { diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 72a2f812e974d7f45d5b1110d4c26ac093e78f29..3491fc3d4a1c7bbbbe1d7e1f0c2cc2304e852bca 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task, - UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, + MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, TextColor}; @@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static { ) -> Self::ListItem; } +impl FocusableView for Picker { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let editor = cx.build_view(|cx| { diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 20adc44c1aa84ec96491ffacd884de5b73efbfb2..a0bc7cd72f10e25fc68674071afce253582cacf9 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -66,7 +66,6 @@ fn main() { story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace)); let theme_registry = cx.global::(); - let mut theme_settings = ThemeSettings::get_global(cx).clone(); theme_settings.active_theme = theme_registry.get(&theme_name).unwrap(); ThemeSettings::override_global(theme_settings, cx); @@ -114,6 +113,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() + .font("Zed Mono") .child(self.story.clone()) } } diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8b04e4d44b306969de63a7bd880d61b724aed32b --- /dev/null +++ b/crates/storybook3/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "storybook3" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "storybook" +path = "src/storybook3.rs" + +[dependencies] +anyhow.workspace = true + +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2", features = ["stories"] } +theme = { package = "theme2", path = "../theme2", features = ["stories"] } +settings = { package = "settings2", path = "../settings2"} diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs new file mode 100644 index 0000000000000000000000000000000000000000..291f8ce2ac451ca7c49e72776fcd4b7411644c6d --- /dev/null +++ b/crates/storybook3/src/storybook3.rs @@ -0,0 +1,73 @@ +use anyhow::Result; +use gpui::AssetSource; +use gpui::{ + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + WindowOptions, +}; +use settings::{default_settings, Settings, SettingsStore}; +use std::borrow::Cow; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::{prelude::*, ContextMenuStory}; + +struct Assets; + +impl AssetSource for Assets { + fn load(&self, _path: &str) -> Result> { + todo!(); + } + + fn list(&self, _path: &str) -> Result> { + Ok(vec![]) + } +} + +fn main() { + let asset_source = Arc::new(Assets); + gpui::App::production(asset_source).run(move |cx| { + let mut store = SettingsStore::default(); + store + .set_default_settings(default_settings().as_ref(), cx) + .unwrap(); + cx.set_global(store); + ui::settings::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + + cx.open_window( + WindowOptions { + bounds: WindowBounds::Fixed(Bounds { + origin: Default::default(), + size: size(px(1500.), px(780.)).into(), + }), + ..Default::default() + }, + move |cx| { + let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; + cx.set_rem_size(ui_font_size); + + cx.build_view(|cx| TestView { + story: cx.build_view(|_| ContextMenuStory).into(), + }) + }, + ); + + cx.activate(true); + }) +} + +struct TestView { + story: AnyView, +} + +impl Render for TestView { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .flex() + .flex_col() + .size_full() + .font("Helvetica") + .child(self.story.clone()) + } +} diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index f815dbe0ea9a05d95880917eae255ef63c797546..b6ab7e86b9191fa6910e5632158ae0c587059c21 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -32,7 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, ContextMenuItem, Label}, + ui::{ContextMenu, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -85,7 +85,7 @@ pub struct TerminalView { has_new_content: bool, //Currently using iTerm bell, show bell emoji in tab until input is received has_bell: bool, - context_menu: Option, + context_menu: Option>, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -300,10 +300,14 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::new(vec![ - ContextMenuItem::entry(Label::new("Clear"), Clear), - ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }), - ])); + self.context_menu = Some(cx.build_view(|cx| { + ContextMenu::new(cx) + .entry(Label::new("Clear"), Box::new(Clear)) + .entry( + Label::new("Close"), + Box::new(CloseActiveItem { save_intent: None }), + ) + })); dbg!(&position); // todo!() // self.context_menu diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 6b3371e33861c5475e42596b9de6b57afd183617..d3214cbff1b31d7fda3c8fe80a108a55490f2119 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,82 +1,258 @@ -use crate::{prelude::*, ListItemVariant}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; +use gpui::{ + overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, +}; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - Separator, +pub struct ContextMenu { + items: Vec, + focus_handle: FocusHandle, } -impl Clone for ContextMenuItem { - fn clone(&self) -> Self { - match self { - ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()), - ContextMenuItem::Entry(label, action) => { - ContextMenuItem::Entry(label.clone(), action.boxed_clone()) - } - ContextMenuItem::Separator => ContextMenuItem::Separator, - } +impl ManagedView for ContextMenu { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() } } -impl ContextMenuItem { - fn to_list_item(self) -> ListItem { - match self { - ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label, action) => ListEntry::new(label) - .variant(ListItemVariant::Inset) - .on_click(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), + +impl ContextMenu { + pub fn new(cx: &mut WindowContext) -> Self { + Self { + items: Default::default(), + focus_handle: cx.focus_handle(), } } - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) + pub fn header(mut self, title: impl Into) -> Self { + self.items.push(ListItem::Header(ListSubHeader::new(title))); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(ListItem::Separator(ListSeparator)); + self + } + + pub fn entry(mut self, label: Label, action: Box) -> Self { + self.items.push(ListEntry::new(label).action(action).into()); + self } - pub fn separator() -> Self { - Self::Separator + pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(Dismiss); } - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(Dismiss); } } -#[derive(Component, Clone)] -pub struct ContextMenu { - items: Vec, +impl Render for ContextMenu { + type Element = Div; + // todo!() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().elevation_2(cx).flex().flex_row().child( + v_stack() + .min_w(px(200.)) + .track_focus(&self.focus_handle) + .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)) + // .on_action(ContextMenu::select_first) + // .on_action(ContextMenu::select_last) + // .on_action(ContextMenu::select_next) + // .on_action(ContextMenu::select_prev) + .on_action(ContextMenu::confirm) + .on_action(ContextMenu::cancel) + .flex_none() + // .bg(cx.theme().colors().elevated_surface_background) + // .border() + // .border_color(cx.theme().colors().border) + .child(List::new(self.items.clone())), + ) + } } -impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { - Self { - items: items.into_iter().collect(), +pub struct MenuHandle { + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, + + anchor: Option, + attach: Option, +} + +impl MenuHandle { + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { + self.child_builder = Some(Box::new(|b| f(b).render())); + self + } + + /// anchor defines which corner of the menu to anchor to the attachment point + /// (by default the cursor position, but see attach) + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor = Some(anchor); + self + } + + /// attach defines which corner of the handle to attach the menu's anchor to + pub fn attach(mut self, attach: AnchorCorner) -> Self { + self.attach = Some(attach); + self + } +} + +pub fn menu_handle() -> MenuHandle { + MenuHandle { + id: None, + child_builder: None, + menu_builder: None, + anchor: None, + attach: None, + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + position: Rc>>, + child_layout_id: Option, + child_element: Option>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone().expect("menu_handle must have an id()")) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) + } else { + (Rc::default(), Rc::default()) + }; + + let mut menu_layout_id = None; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut overlay = overlay::().snap_to_window(); + if let Some(anchor) = self.anchor { + overlay = overlay.anchor(anchor); + } + overlay = overlay.position(*position.borrow()); + + let mut view = overlay.child(menu.clone()).render(); + menu_layout_id = Some(view.layout(view_state, cx)); + view + }); + + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.borrow().is_some())); + + let child_layout_id = child_element + .as_mut() + .map(|child_element| child_element.layout(view_state, cx)); + + let layout_id = cx.request_layout( + &gpui::Style::default(), + menu_layout_id.into_iter().chain(child_layout_id), + ); + + ( + layout_id, + MenuHandleState { + menu, + position, + child_element, + child_layout_id, + menu_element, + }, + ) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + if let Some(child) = element_state.child_element.as_mut() { + child.paint(view_state, cx); } + + if let Some(menu) = element_state.menu_element.as_mut() { + menu.paint(view_state, cx); + return; + } + + let Some(builder) = self.menu_builder.clone() else { + return; + }; + let menu = element_state.menu.clone(); + let position = element_state.position.clone(); + let attach = self.attach.clone(); + let child_layout_id = element_state.child_layout_id.clone(); + + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && bounds.contains_point(&event.position) + { + cx.stop_propagation(); + cx.prevent_default(); + + let new_menu = (builder)(view_state, cx); + let menu2 = menu.clone(); + cx.subscribe(&new_menu, move |this, modal, e, cx| match e { + &Dismiss => { + *menu2.borrow_mut() = None; + cx.notify(); + } + }) + .detach(); + *menu.borrow_mut() = Some(new_menu); + + *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() { + attach + .unwrap() + .corner(cx.layout_bounds(child_layout_id.unwrap())) + } else { + cx.mouse_position() + }; + cx.notify(); + } + }); } - // todo!() - // cx.add_action(ContextMenu::select_first); - // cx.add_action(ContextMenu::select_last); - // cx.add_action(ContextMenu::select_next); - // cx.add_action(ContextMenu::select_prev); - // cx.add_action(ContextMenu::confirm); - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - v_stack() - .flex() - .bg(cx.theme().colors().elevated_surface_background) - .border() - .border_color(cx.theme().colors().border) - .child(List::new( - self.items - .into_iter() - .map(ContextMenuItem::to_list_item::) - .collect(), - )) - .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) } } -use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -84,8 +260,18 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{Div, Render}; - use serde::Deserialize; + use gpui::{actions, Div, Render, VisualContext}; + + actions!(PrintCurrentDate); + + fn build_menu(cx: &mut WindowContext, header: impl Into) -> View { + cx.build_view(|cx| { + ContextMenu::new(cx).header(header).separator().entry( + Label::new("Print current time"), + PrintCurrentDate.boxed_clone(), + ) + }) + } pub struct ContextMenuStory; @@ -93,22 +279,84 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[derive(PartialEq, Clone, Deserialize, gpui::Action)] - struct PrintCurrentDate {} - Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) - .child(Story::label(cx, "Default")) - .child(ContextMenu::new([ - ContextMenuItem::header("Section header"), - ContextMenuItem::Separator, - ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}), - ])) .on_action(|_, _: &PrintCurrentDate, _| { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { println!("Current Unix time is {:?}", unix_time.as_secs()); } }) + .flex() + .flex_row() + .justify_between() + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test2") + .child(|is_open| { + Label::new(if is_open { + "TOP LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .menu(move |_, cx| build_menu(cx, "top left")), + ) + .child( + menu_handle() + .id("test1") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM LEFT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomLeft) + .attach(AnchorCorner::TopLeft) + .menu(move |_, cx| build_menu(cx, "bottom left")), + ), + ) + .child( + div() + .flex() + .flex_col() + .justify_between() + .child( + menu_handle() + .id("test3") + .child(|is_open| { + Label::new(if is_open { + "TOP RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "top right")), + ) + .child( + menu_handle() + .id("test4") + .child(|is_open| { + Label::new(if is_open { + "BOTTOM RIGHT" + } else { + "RIGHT CLICK ME" + }) + .render() + }) + .anchor(AnchorCorner::BottomRight) + .attach(AnchorCorner::TopRight) + .menu(move |_, cx| build_menu(cx, "bottom right")), + ), + ) } } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 5512da2b34638e25e6450fd92f59b4a8d60c23f6..9b8548e3f9c6cf092f08f41bbda3bfc25293bc98 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement}; -use gpui::{prelude::*, AnyView, MouseButton}; +use gpui::{prelude::*, Action, AnyView, MouseButton}; use std::sync::Arc; struct IconButtonHandlers { @@ -19,6 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, + selected: bool, tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -31,6 +32,7 @@ impl IconButton { color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + selected: false, tooltip: None, handlers: IconButtonHandlers::default(), } @@ -56,6 +58,11 @@ impl IconButton { self } + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + pub fn tooltip( mut self, tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, @@ -69,6 +76,10 @@ impl IconButton { self } + pub fn action(self, action: Box) -> Self { + self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + } + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, @@ -76,7 +87,7 @@ impl IconButton { _ => self.color, }; - let (bg_color, bg_hover_color, bg_active_color) = match self.variant { + let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { ButtonVariant::Filled => ( cx.theme().colors().element_background, cx.theme().colors().element_hover, @@ -89,6 +100,10 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() @@ -108,7 +123,9 @@ impl IconButton { } if let Some(tooltip) = self.tooltip.take() { - button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + if !self.selected { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + } } button diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 4b355dd5b6c159c906117e64b3008f8d100129c7..b9508c54136aa424789f943ef40cf47d58122fae 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -117,7 +117,7 @@ impl ListHeader { } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -172,7 +172,7 @@ pub enum ListEntrySize { Medium, } -#[derive(Component)] +#[derive(Component, Clone)] pub enum ListItem { Entry(ListEntry), Separator(ListSeparator), @@ -234,6 +234,24 @@ pub struct ListEntry { on_click: Option>, } +impl Clone for ListEntry { + fn clone(&self) -> Self { + Self { + disabled: self.disabled, + // TODO: Reintroduce this + // disclosure_control_style: DisclosureControlVisibility, + indent_level: self.indent_level, + label: self.label.clone(), + left_slot: self.left_slot.clone(), + overflow: self.overflow, + size: self.size, + toggle: self.toggle, + variant: self.variant, + on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()), + } + } +} + impl ListEntry { pub fn new(label: Label) -> Self { Self { @@ -249,7 +267,7 @@ impl ListEntry { } } - pub fn on_click(mut self, action: impl Into>) -> Self { + pub fn action(mut self, action: impl Into>) -> Self { self.on_click = Some(action.into()); self } diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index 94e38267f4c5b160365317c4e597a76748c05d6f..c98cfa012f261c8a3e77251370e2265e057158ec 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -12,7 +12,6 @@ impl Story { .flex_col() .pt_2() .px_4() - .font("Zed Mono") .bg(cx.theme().colors().background) } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index d9911e683358dfd37a9110f71cf3899debe52904..9037682807c0685ab7cf844fbed9c6737e30e83f 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize}; fn elevated(this: E, cx: &mut ViewContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) + .z_index(index.z_index()) .rounded_lg() .border() .border_color(cx.theme().colors().border_variant) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index ee45ca862c96a4ca034645748ca4ac730c40223e..f62633e439f0883975e90d220ac74fb04e80039e 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,14 +1,14 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext, - WeakView, WindowContext, + div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, + EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, + Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; use theme2::ActiveTheme; -use ui::{h_stack, IconButton, InteractionState, Tooltip}; +use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -417,6 +417,14 @@ impl Dock { } } + pub fn toggle_action(&self) -> Box { + match self.position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + } + } + // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { // todo!() // if let Some(active_entry) = self.visible_entry() { @@ -655,6 +663,7 @@ impl PanelButtons { // } // } +// here be kittens impl Render for PanelButtons { type Element = Div; @@ -664,6 +673,13 @@ impl Render for PanelButtons { let active_index = dock.active_panel_index; let is_open = dock.is_open; + let (menu_anchor, menu_attach) = match dock.position { + DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft), + DockPosition::Bottom | DockPosition::Right => { + (AnchorCorner::BottomRight, AnchorCorner::TopRight) + } + }; + let buttons = dock .panel_entries .iter() @@ -671,15 +687,33 @@ impl Render for PanelButtons { .filter_map(|(i, panel)| { let icon = panel.panel.icon(cx)?; let name = panel.panel.persistent_name(); - let action = panel.panel.toggle_action(cx); - let action2 = action.boxed_clone(); - - let mut button = IconButton::new(panel.panel.persistent_name(), icon) - .when(i == active_index, |el| el.state(InteractionState::Active)) - .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) - .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx)); - Some(button) + let mut button: IconButton = if i == active_index && is_open { + let action = dock.toggle_action(); + let tooltip: SharedString = + format!("Close {} dock", dock.position.to_label()).into(); + IconButton::new(name, icon) + .state(InteractionState::Active) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) + } else { + let action = panel.panel.toggle_action(cx); + + IconButton::new(name, icon) + .action(action.boxed_clone()) + .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx)) + }; + + Some( + menu_handle() + .id(name) + .menu(move |_, cx| { + cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) + }) + .anchor(menu_anchor) + .attach(menu_attach) + .child(|is_open| button.selected(is_open)), + ) }); h_stack().gap_0p5().children(buttons) diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index cd5995d65e438c98264d7d1bd44747d1ce72caf6..8afd8317f94ed5452e49106c50b5e69f056a6e6e 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View, - ViewContext, WindowContext, + div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View, + ViewContext, }; use ui::{h_stack, v_stack}; @@ -15,14 +15,6 @@ pub struct ModalLayer { active_modal: Option, } -pub trait Modal: Render + EventEmitter { - fn focus(&self, cx: &mut WindowContext); -} - -pub enum ModalEvent { - Dismissed, -} - impl ModalLayer { pub fn new() -> Self { Self { active_modal: None } @@ -30,7 +22,7 @@ impl ModalLayer { pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: Modal, + V: ManagedView, B: FnOnce(&mut ViewContext) -> V, { if let Some(active_modal) = &self.active_modal { @@ -46,17 +38,15 @@ impl ModalLayer { pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) where - V: Modal, + V: ManagedView, { self.active_modal = Some(ActiveModal { modal: new_modal.clone().into(), - subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.hide_modal(cx), - }), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)), previous_focus_handle: cx.focused(), focus_handle: cx.focus_handle(), }); - new_modal.update(cx, |modal, cx| modal.focus(cx)); + cx.focus_view(&new_modal); cx.notify(); } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a01883b9a0c018f9e2b9d3c4016caa2173673510..dc69280c1ebcf165a7d8d9168e9bcc744194b79e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -31,9 +31,9 @@ use futures::{ use gpui::{ actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model, ModelContext, - ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model, + ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3212,8 +3212,8 @@ impl Workspace { }) } - fn actions(div: Div) -> Div { - div + fn actions(&self, div: Div) -> Div { + self.add_workspace_actions_listeners(div) // cx.add_async_action(Workspace::open); // cx.add_async_action(Workspace::follow_next_collaborator); // cx.add_async_action(Workspace::close); @@ -3262,15 +3262,15 @@ impl Workspace { .on_action(|this, e: &ToggleLeftDock, cx| { this.toggle_dock(DockPosition::Left, cx); }) - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); + .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }) + .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + workspace.toggle_dock(DockPosition::Bottom, cx); + }) + .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + workspace.close_all_docks(cx); + }) // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); @@ -3380,11 +3380,14 @@ impl Workspace { div } - pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + pub fn active_modal( + &mut self, + cx: &ViewContext, + ) -> Option> { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { @@ -3624,7 +3627,7 @@ impl Render for Workspace { cx.set_rem_size(ui_font_size); - self.add_workspace_actions_listeners(div()) + self.actions(div()) .key_context(context) .relative() .size_full()