From 4540f04dbe4e8d708622d01152b5b9548253ed57 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 09:55:22 -0700 Subject: [PATCH 01/10] Add more detail to panel switcher interaction --- crates/ui2/src/components/icon_button.rs | 6 ++++- crates/workspace2/src/dock.rs | 33 ++++++++++++++++++------ crates/workspace2/src/workspace2.rs | 24 ++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 4408c51f62128cbe27b76dcd28f976b225abedda..1f7b86baddf1e005c20be844e4672a764d2b9f87 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 { @@ -69,6 +69,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, diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 8e7f08252c80d8d69a3349dbe21daf2be18eb780..eec0ce309a4c3dcbc41017a1bb81669b0155e609 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,8 +1,8 @@ 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, + FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, Subscription, View, + ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -416,6 +416,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() { @@ -664,13 +672,22 @@ 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)); + 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(button) }); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f28675661de250b03f9e3d174da21d9835a2d2fd..7282e6839ab8816016936ab22e0d207b0d827f7f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3213,8 +3213,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); @@ -3263,15 +3263,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(); @@ -3616,7 +3616,7 @@ impl Render for Workspace { context.add("Workspace"); let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - self.add_workspace_actions_listeners(div()) + self.actions(div()) .key_context(context) .relative() .size_full() From d782426491a2eda1ec0e1c969b8735810284c88f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 10:26:09 -0700 Subject: [PATCH 02/10] Dismiss tooltips on click --- crates/gpui2/src/elements/div.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 31a8827109d922546f312b507844d3996388e009..49a91a9821537d55bf5ca553ef93e1c76a1fc7f4 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. From 267e07472d4ee0c3070f884d4be9fc9966a7a6a3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 13:32:19 -0700 Subject: [PATCH 03/10] Checkpoint, MenuHandle can open one --- crates/gpui2/src/elements/overlay.rs | 10 +- crates/gpui2/src/window.rs | 1 + crates/workspace2/src/dock.rs | 133 +++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index a190337f04dbfe5a4acd908aaf4a646c33a49240..c45ea2a588e841e82c5b43c75dd06f832b2cd356 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,8 @@ use smallvec::SmallVec; 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 { @@ -48,6 +48,12 @@ impl ParentComponent for Overlay { } } +impl Component for Overlay { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + impl Element for Overlay { type ElementState = OverlayState; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b0d9d07df2db7b3b84db7d9950d3f6364b3b8c9d..17bd4743b1b840cb2d0230f3e041f41c8c34cd75 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -574,6 +574,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. diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index eec0ce309a4c3dcbc41017a1bb81669b0155e609..f2dd2c15cf2c1e3416ebfa9162f5c901c3306ce7 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,13 +1,16 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, Subscription, View, - ViewContext, WeakView, WindowContext, + div, overlay, point, px, Action, AnyElement, AnyView, AppContext, Component, DispatchPhase, + Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, + InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, Pixels, Point, + Render, SharedString, Style, Styled, Subscription, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use ui::{h_stack, IconButton, InteractionState, Tooltip}; +use smallvec::SmallVec; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use ui::{h_stack, IconButton, InteractionState, Label, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -656,6 +659,118 @@ impl PanelButtons { // } // } +pub struct MenuHandle { + id: ElementId, + children: SmallVec<[AnyElement; 2]>, + builder: Rc) -> AnyView + 'static>, +} + +impl ParentComponent for MenuHandle { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl MenuHandle { + fn new( + id: impl Into, + builder: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, + ) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + builder: Rc::new(builder), + } + } +} + +pub struct MenuState { + open: Rc>, + menu: Option>, +} +// Here be dragons +impl Element for MenuHandle { + type ElementState = MenuState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let mut child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + + let open = if let Some(element_state) = element_state { + element_state.open + } else { + Rc::new(RefCell::new(false)) + }; + + let mut menu = None; + if *open.borrow() { + let mut view = (self.builder)(view_state, cx).render(); + child_layout_ids.push(view.layout(view_state, cx)); + menu.replace(view); + } + let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + + (layout_id, MenuState { open, menu }) + } + + fn paint( + &mut self, + bounds: crate::Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + for child in &mut self.children { + child.paint(view_state, cx); + } + + if let Some(mut menu) = element_state.menu.as_mut() { + menu.paint(view_state, cx); + return; + } + + let open = element_state.open.clone(); + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { + dbg!(&event, &phase); + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && bounds.contains_point(&event.position) + { + *open.borrow_mut() = true; + cx.notify(); + } + }); + } +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +struct TestMenu {} +impl Render for TestMenu { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().child("0MG!") + } +} + +// here be kittens impl Render for PanelButtons { type Element = Div; @@ -689,7 +804,13 @@ impl Render for PanelButtons { .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx)) }; - Some(button) + Some( + MenuHandle::new( + SharedString::from(format!("{} tooltip", name)), + move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx), + ) + .child(button), + ) }); h_stack().children(buttons) From 9456f716c2b47ab34e3d10f805e92bf0b0f686e6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 15:30:53 -0700 Subject: [PATCH 04/10] Only send one right click event --- crates/gpui2/src/elements/overlay.rs | 5 +++- crates/gpui2/src/platform/mac/window.rs | 36 +++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index c45ea2a588e841e82c5b43c75dd06f832b2cd356..69ac9c50dc39e4d25bedc76f3043f455522532c6 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -72,7 +72,10 @@ 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 = crate::Position::Absolute; + + let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); (layout_id, OverlayState { child_layout_ids }) } 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); } } From 074a221e0f6a25603ba02ac24c9e5ac347436c5c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 16:59:27 -0700 Subject: [PATCH 05/10] Progress on ContextMenu --- crates/gpui2/src/elements/overlay.rs | 9 +- crates/terminal_view2/src/terminal_view.rs | 14 +- crates/ui2/src/components/context_menu.rs | 238 ++++++++++++++++++--- crates/ui2/src/components/list.rs | 24 ++- crates/workspace2/src/dock.rs | 118 +--------- 5 files changed, 246 insertions(+), 157 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 69ac9c50dc39e4d25bedc76f3043f455522532c6..8580ae3eb02761a9f4e77f2bb0db7e360fa886e7 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::Position; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, - Point, Size, Style, + point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, + ParentComponent, Pixels, Point, Size, Style, }; pub struct OverlayState { @@ -72,8 +73,9 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); + let mut overlay_style = Style::default(); - overlay_style.position = crate::Position::Absolute; + overlay_style.position = Position::Absolute; let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); @@ -106,6 +108,7 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; + dbg!(bounds, desired, limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 14391ca2b2f357f56f084a5688601182bad780cf..4d77d172a641d9c16c97568cf5e93a176f0e3f44 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -87,7 +87,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, @@ -302,10 +302,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 8f32c3ed5664ede770af66580e0abe2f7379f767..8024d334b5539ec992b34dded398410172d0c38a 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,5 +1,13 @@ -use crate::{prelude::*, ListItemVariant}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::{h_stack, prelude::*, ListItemVariant}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; +use gpui::{ + overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle, + Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, +}; +use smallvec::SmallVec; pub enum ContextMenuItem { Header(SharedString), @@ -19,12 +27,12 @@ impl Clone for ContextMenuItem { } } impl ContextMenuItem { - fn to_list_item(self) -> ListItem { + 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) + .action(action) .into(), ContextMenuItem::Separator => ListSeparator::new().into(), } @@ -43,40 +51,196 @@ impl ContextMenuItem { } } -#[derive(Component, Clone)] pub struct ContextMenu { - items: Vec, + items: Vec, + focus_handle: FocusHandle, +} + +pub enum MenuEvent { + Dismissed, +} + +impl EventEmitter for ContextMenu {} +impl FocusableView for ContextMenu { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } } impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { + pub fn new(cx: &mut WindowContext) -> Self { Self { - items: items.into_iter().collect(), + items: Default::default(), + focus_handle: cx.focus_handle(), } } + + 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 confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(MenuEvent::Dismissed); + } + + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(MenuEvent::Dismissed); + } +} + +impl Render for ContextMenu { + type Element = Overlay; // 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))) + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + overlay().child( + 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())), + ), + ) + } +} + +pub struct MenuHandle { + id: ElementId, + children: SmallVec<[AnyElement; 2]>, + builder: Rc) -> View + 'static>, +} + +impl ParentComponent for MenuHandle { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl MenuHandle { + pub fn new( + id: impl Into, + builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + builder: Rc::new(builder), + } + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let mut child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + + let menu = if let Some(element_state) = element_state { + element_state.menu + } else { + Rc::new(RefCell::new(None)) + }; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut view = menu.clone().render(); + child_layout_ids.push(view.layout(view_state, cx)); + view + }); + + let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + + (layout_id, MenuHandleState { menu, menu_element }) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + for child in &mut self.children { + child.paint(view_state, cx); + } + + if let Some(menu) = element_state.menu_element.as_mut() { + menu.paint(view_state, cx); + return; + } + + let menu = element_state.menu.clone(); + let builder = self.builder.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 { + MenuEvent::Dismissed => { + *menu2.borrow_mut() = None; + cx.notify(); + } + }) + .detach(); + *menu.borrow_mut() = Some(new_menu); + cx.notify(); + } + }); + } +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) } } -use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -84,7 +248,7 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{action, Div, Render}; + use gpui::{action, Div, Render, VisualContext}; pub struct ContextMenuStory; @@ -97,17 +261,25 @@ mod stories { 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()); } }) + .child( + MenuHandle::new("test", move |_, cx| { + cx.build_view(|cx| { + ContextMenu::new(cx) + .header("Section header") + .separator() + .entry( + Label::new("Print current time"), + PrintCurrentDate {}.boxed_clone(), + ) + }) + }) + .child(Label::new("RIGHT CLICK ME")), + ) } } } 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/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f2dd2c15cf2c1e3416ebfa9162f5c901c3306ce7..409385cafc30621e8af5f9fd0ce5ee07c3342233 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -10,7 +10,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{cell::RefCell, rc::Rc, sync::Arc}; -use ui::{h_stack, IconButton, InteractionState, Label, Tooltip}; +use ui::{ + h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent, + MenuHandle, Tooltip, +}; pub enum PanelEvent { ChangePosition, @@ -659,117 +662,6 @@ impl PanelButtons { // } // } -pub struct MenuHandle { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - builder: Rc) -> AnyView + 'static>, -} - -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl MenuHandle { - fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, - ) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } - } -} - -pub struct MenuState { - open: Rc>, - menu: Option>, -} -// Here be dragons -impl Element for MenuHandle { - type ElementState = MenuState; - - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut crate::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { - let mut child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - - let open = if let Some(element_state) = element_state { - element_state.open - } else { - Rc::new(RefCell::new(false)) - }; - - let mut menu = None; - if *open.borrow() { - let mut view = (self.builder)(view_state, cx).render(); - child_layout_ids.push(view.layout(view_state, cx)); - menu.replace(view); - } - let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); - - (layout_id, MenuState { open, menu }) - } - - fn paint( - &mut self, - bounds: crate::Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, - ) { - for child in &mut self.children { - child.paint(view_state, cx); - } - - if let Some(mut menu) = element_state.menu.as_mut() { - menu.paint(view_state, cx); - return; - } - - let open = element_state.open.clone(); - cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { - dbg!(&event, &phase); - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Right - && bounds.contains_point(&event.position) - { - *open.borrow_mut() = true; - cx.notify(); - } - }); - } -} - -impl Component for MenuHandle { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -struct TestMenu {} -impl Render for TestMenu { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().child("0MG!") - } -} - // here be kittens impl Render for PanelButtons { type Element = Div; @@ -807,7 +699,7 @@ impl Render for PanelButtons { Some( MenuHandle::new( SharedString::from(format!("{} tooltip", name)), - move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx), + move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")), ) .child(button), ) From 547888942f59db89b4936679546e33796750a828 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 19:39:59 -0700 Subject: [PATCH 06/10] Add storybook3 --- Cargo.lock | 11 +++++ Cargo.toml | 1 + crates/storybook2/src/storybook2.rs | 2 +- crates/storybook3/Cargo.toml | 17 +++++++ crates/storybook3/src/storybook3.rs | 75 +++++++++++++++++++++++++++++ crates/ui2/src/story.rs | 1 - 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 crates/storybook3/Cargo.toml create mode 100644 crates/storybook3/src/storybook3.rs diff --git a/Cargo.lock b/Cargo.lock index bf2e964ea8f533a8a0c0ada0e60077fdd140caa1..a364cbce647c6a2c40d1c4c3118dc502300ce283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8802,6 +8802,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/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c4c1d75eac26b245ede5b79b6042e33cbeec67f7..1481ab9fb4b41c7f0abee86a72e5559879e020be 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..b73bb8c563a700019a341867321647066ee259cc --- /dev/null +++ b/crates/storybook3/src/storybook3.rs @@ -0,0 +1,75 @@ +use anyhow::Result; +use gpui::AssetSource; +use gpui::{ + div, hsla, 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(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() + .p(px(10.)) + .bg(hsla(1., 1., 1., 0.)) + .flex() + .flex_col() + .size_full() + .font("Helvetica") + .child(self.story.clone()) + } +} 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) } From 9547e88d883d1f0187c2268a981aae2bb6c2d1a2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 19:50:31 -0700 Subject: [PATCH 07/10] TEMP --- crates/gpui2/src/elements/overlay.rs | 8 ++++---- crates/ui2/src/components/context_menu.rs | 6 +++--- crates/ui2/src/styled_ext.rs | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 8580ae3eb02761a9f4e77f2bb0db7e360fa886e7..4d3e8fdbf7bdab8b7a2730b39c00d179964071dc 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,9 +1,9 @@ use smallvec::SmallVec; -use taffy::style::Position; +use taffy::style::{Display, Position}; use crate::{ - point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, - ParentComponent, Pixels, Point, Size, Style, + point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, + Point, Size, Style, }; pub struct OverlayState { @@ -76,6 +76,7 @@ impl Element for Overlay { 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()); @@ -108,7 +109,6 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; - dbg!(bounds, desired, limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8024d334b5539ec992b34dded398410172d0c38a..5d4974e63158fb260e6dd2998e319777362cd34c 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,11 +1,11 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{h_stack, prelude::*, ListItemVariant}; +use crate::{prelude::*, ListItemVariant}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, + overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle, + FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, }; use smallvec::SmallVec; 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) From c0ad15756cfef2bdf6f82eccce180af32b6843fd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 21:45:22 -0700 Subject: [PATCH 08/10] More attachment configuration for context menus --- crates/gpui2/src/elements/overlay.rs | 21 +- crates/gpui2/src/window.rs | 8 + crates/terminal_view2/src/terminal_view.rs | 2 +- crates/ui2/src/components/context_menu.rs | 317 +++++++++++++-------- crates/ui2/src/components/icon_button.rs | 17 +- crates/workspace2/src/dock.rs | 34 ++- 6 files changed, 267 insertions(+), 132 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 4d3e8fdbf7bdab8b7a2730b39c00d179964071dc..14a8048d398176bbd8bec49b37bc96b261450ed9 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -15,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -26,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -36,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; @@ -102,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 { @@ -196,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/window.rs b/crates/gpui2/src/window.rs index 17bd4743b1b840cb2d0230f3e041f41c8c34cd75..b8066d48898ea27fee64ad90228f456d9a0e743f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1151,6 +1151,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/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 4d77d172a641d9c16c97568cf5e93a176f0e3f44..80bf65aec43f6633079b9ab7fb392e362451540e 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, }; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 5d4974e63158fb260e6dd2998e319777362cd34c..112e1224f9d55ba27b471b0c06e91a660043e812 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,56 +1,14 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{prelude::*, ListItemVariant}; +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle, - FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, + overlay, px, Action, AnchorCorner, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, + FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View, }; use smallvec::SmallVec; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - Separator, -} - -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 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) - .action(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), - } - } - - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) - } - - pub fn separator() -> Self { - Self::Separator - } - - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) - } -} - pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, @@ -101,67 +59,93 @@ impl ContextMenu { } impl Render for ContextMenu { - type Element = Overlay; + type Element = Div; // todo!() fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - overlay().child( - 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())), - ), + 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())), ) } } pub struct MenuHandle { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - builder: Rc) -> View + 'static>, -} + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } + anchor: Option, + attach: Option, } impl MenuHandle { - pub fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + 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 { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } + 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()) + Some(self.id.clone().expect("menu_handle must have an id()")) } fn layout( @@ -170,27 +154,50 @@ impl Element for MenuHandle { element_state: Option, cx: &mut crate::ViewContext, ) -> (gpui::LayoutId, Self::ElementState) { - let mut child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - - let menu = if let Some(element_state) = element_state { - element_state.menu + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) } else { - Rc::new(RefCell::new(None)) + (Rc::default(), Rc::default()) }; + let mut menu_layout_id = None; + let menu_element = menu.borrow_mut().as_mut().map(|menu| { - let mut view = menu.clone().render(); - child_layout_ids.push(view.layout(view_state, cx)); + 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 layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); - - (layout_id, MenuHandleState { menu, menu_element }) + 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( @@ -200,7 +207,7 @@ impl Element for MenuHandle { element_state: &mut Self::ElementState, cx: &mut crate::ViewContext, ) { - for child in &mut self.children { + if let Some(child) = element_state.child_element.as_mut() { child.paint(view_state, cx); } @@ -209,8 +216,14 @@ impl Element for MenuHandle { return; } + let Some(builder) = self.menu_builder.clone() else { + return; + }; let menu = element_state.menu.clone(); - let builder = self.builder.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 @@ -229,6 +242,14 @@ impl Element for MenuHandle { }) .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(); } }); @@ -250,35 +271,101 @@ mod stories { use crate::story::Story; use gpui::{action, Div, Render, VisualContext}; + #[action] + struct 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; impl Render for ContextMenuStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[action] - struct PrintCurrentDate {} - Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) .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( - MenuHandle::new("test", move |_, cx| { - cx.build_view(|cx| { - ContextMenu::new(cx) - .header("Section header") - .separator() - .entry( - Label::new("Print current time"), - PrintCurrentDate {}.boxed_clone(), - ) - }) - }) - .child(Label::new("RIGHT CLICK ME")), + 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 1f7b86baddf1e005c20be844e4672a764d2b9f87..2772f10a2996b487e8b4b1cf947696ef8aeee845 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -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, @@ -80,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, @@ -93,6 +100,10 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() @@ -113,7 +124,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/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 409385cafc30621e8af5f9fd0ce5ee07c3342233..3a3cc4d49099fda92e16e34b70e719c44b6c78df 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,18 +1,18 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, overlay, point, px, Action, AnyElement, AnyView, AppContext, Component, DispatchPhase, - Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, - InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, Pixels, Point, - Render, SharedString, Style, Styled, Subscription, View, ViewContext, VisualContext, WeakView, - WindowContext, + div, overlay, point, px, Action, AnchorCorner, AnyElement, AnyView, AppContext, Component, + DispatchPhase, Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, + Pixels, Point, Render, SharedString, Style, Styled, Subscription, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{cell::RefCell, rc::Rc, sync::Arc}; use ui::{ - h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent, - MenuHandle, Tooltip, + h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, MenuEvent, MenuHandle, + Tooltip, }; pub enum PanelEvent { @@ -672,6 +672,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() @@ -697,11 +704,14 @@ impl Render for PanelButtons { }; Some( - MenuHandle::new( - SharedString::from(format!("{} tooltip", name)), - move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")), - ) - .child(button), + 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)), ) }); From 2182cb26563f40034676f6fd4d24653ca7138fc5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 22:16:29 -0700 Subject: [PATCH 09/10] Ooh generics --- crates/storybook3/src/storybook3.rs | 2 +- crates/ui2/src/components/context_menu.rs | 26 +++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs index aa27edb1f974dec37c9ad87d30dff5aa8cec8d26..291f8ce2ac451ca7c49e72776fcd4b7411644c6d 100644 --- a/crates/storybook3/src/storybook3.rs +++ b/crates/storybook3/src/storybook3.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::AssetSource; use gpui::{ - div, hsla, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, WindowOptions, }; use settings::{default_settings, Settings, SettingsStore}; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index dadf27d89e9be497616a738947bc24a61ec40da0..789523d52683c3cdcc69cd48ec4e2d27edf1ab2e 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -23,6 +23,7 @@ impl FocusableView for ContextMenu { self.focus_handle.clone() } } +impl Menu for ContextMenu {} impl ContextMenu { pub fn new(cx: &mut WindowContext) -> Self { @@ -81,25 +82,24 @@ impl Render for ContextMenu { } } -pub struct MenuHandle { +pub trait Menu: Render + EventEmitter + FocusableView {} + +pub struct MenuHandle { id: Option, child_builder: Option AnyElement + 'static>>, - menu_builder: Option) -> View + 'static>>, + menu_builder: Option) -> View + 'static>>, anchor: Option, attach: Option, } -impl MenuHandle { +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 { + pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { self.menu_builder = Some(Rc::new(f)); self } @@ -123,7 +123,7 @@ impl MenuHandle { } } -pub fn menu_handle() -> MenuHandle { +pub fn menu_handle() -> MenuHandle { MenuHandle { id: None, child_builder: None, @@ -133,15 +133,15 @@ pub fn menu_handle() -> MenuHandle { } } -pub struct MenuHandleState { - menu: Rc>>>, +pub struct MenuHandleState { + menu: Rc>>>, position: Rc>>, child_layout_id: Option, child_element: Option>, menu_element: Option>, } -impl Element for MenuHandle { - type ElementState = MenuHandleState; +impl Element for MenuHandle { + type ElementState = MenuHandleState; fn element_id(&self) -> Option { Some(self.id.clone().expect("menu_handle must have an id()")) @@ -255,7 +255,7 @@ impl Element for MenuHandle { } } -impl Component for MenuHandle { +impl Component for MenuHandle { fn render(self) -> AnyElement { AnyElement::new(self) } From 2d1d75f4829dd0e71e1219fa9e0f6c85931cfda8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 22:46:44 -0700 Subject: [PATCH 10/10] +ManagedView And some games with rust traits --- .../command_palette2/src/command_palette.rs | 16 +++++----- crates/file_finder2/src/file_finder.rs | 21 ++++++------- crates/go_to_line2/src/go_to_line.rs | 19 ++++++------ crates/gpui2/src/window.rs | 17 +++++++++++ crates/picker2/src/picker2.rs | 10 +++++-- crates/ui2/src/components/context_menu.rs | 30 +++++++------------ crates/workspace2/src/modal_layer.rs | 22 ++++---------- crates/workspace2/src/workspace2.rs | 13 ++++---- 8 files changed, 75 insertions(+), 73 deletions(-) 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/window.rs b/crates/gpui2/src/window.rs index fe6e3a5f6b518d38e35f8c76da4c04d700ca5c2f..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, 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/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 789523d52683c3cdcc69cd48ec4e2d27edf1ab2e..d3214cbff1b31d7fda3c8fe80a108a55490f2119 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -4,8 +4,8 @@ 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, DispatchPhase, Div, EventEmitter, - FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View, + overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, }; pub struct ContextMenu { @@ -13,17 +13,11 @@ pub struct ContextMenu { focus_handle: FocusHandle, } -pub enum MenuEvent { - Dismissed, -} - -impl EventEmitter for ContextMenu {} -impl FocusableView for ContextMenu { +impl ManagedView for ContextMenu { fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { self.focus_handle.clone() } } -impl Menu for ContextMenu {} impl ContextMenu { pub fn new(cx: &mut WindowContext) -> Self { @@ -50,11 +44,11 @@ impl ContextMenu { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { // todo!() - cx.emit(MenuEvent::Dismissed); + cx.emit(Dismiss); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(MenuEvent::Dismissed); + cx.emit(Dismiss); } } @@ -82,9 +76,7 @@ impl Render for ContextMenu { } } -pub trait Menu: Render + EventEmitter + FocusableView {} - -pub struct MenuHandle { +pub struct MenuHandle { id: Option, child_builder: Option AnyElement + 'static>>, menu_builder: Option) -> View + 'static>>, @@ -93,7 +85,7 @@ pub struct MenuHandle { attach: Option, } -impl MenuHandle { +impl MenuHandle { pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self @@ -123,7 +115,7 @@ impl MenuHandle { } } -pub fn menu_handle() -> MenuHandle { +pub fn menu_handle() -> MenuHandle { MenuHandle { id: None, child_builder: None, @@ -140,7 +132,7 @@ pub struct MenuHandleState { child_element: Option>, menu_element: Option>, } -impl Element for MenuHandle { +impl Element for MenuHandle { type ElementState = MenuHandleState; fn element_id(&self) -> Option { @@ -234,7 +226,7 @@ impl Element for MenuHandle { let new_menu = (builder)(view_state, cx); let menu2 = menu.clone(); cx.subscribe(&new_menu, move |this, modal, e, cx| match e { - MenuEvent::Dismissed => { + &Dismiss => { *menu2.borrow_mut() = None; cx.notify(); } @@ -255,7 +247,7 @@ impl Element for MenuHandle { } } -impl Component for MenuHandle { +impl Component for MenuHandle { fn render(self) -> AnyElement { AnyElement::new(self) } 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 96db870d182f8d9bc831e93c7fcd6f4d8b81a419..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; @@ -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, {