From eb43c78cea87feefba49b02edfc937ed71fea4a2 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 11 Jul 2024 16:20:26 +0800 Subject: [PATCH] Add to support open Popover as a child window. (#18) ## Goal Change to let Popup elements open into a window, to make sure the Popup windows, panels, menus render top than the WebView. Then we can embed wry or other webview into GPUI. https://github.com/huacnlee/gpui-component/assets/5518/ce005456-088f-4298-bf5d-2642cf96faff ---- This is also like other software, they are also used in child window to render the menus, popups. - https://github.com/huacnlee/gpui-component/pull/2 - https://github.com/zed-industries/zed/pull/13730 ## Todos - [x] Multiple display window position. - [x] Windows transparent background. (No transparent, just render a correct window border) - [x] Disable window resize on Windows. - [ ] Handle Menu action. - [x] Provided `popover` or `window` two mode to uses. --- Cargo.lock | 76 +----- Cargo.toml | 1 + crates/app/src/main.rs | 4 - crates/story/src/popover_story.rs | 81 +++--- crates/ui/Cargo.toml | 2 +- crates/ui/src/popover.rs | 392 ++++++++++++++++++++++++++---- crates/ui/src/popup_menu.rs | 34 ++- 7 files changed, 433 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3e278e7..f51927a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,8 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +version = "0.9.0" +source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2" dependencies = [ "async-fs 2.1.2", "async-net 2.0.0", @@ -917,7 +916,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "rustc-hash", ] @@ -1230,7 +1229,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "proc-macro2", "quote", @@ -2181,7 +2180,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2222,11 +2221,9 @@ dependencies = [ "log", "media", "metal", - "mio", "num_cpus", "objc", "oo7", - "open", "parking", "parking_lot", "pathfinder_geometry", @@ -2269,7 +2266,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "proc-macro2", "quote", @@ -2454,7 +2451,7 @@ dependencies = [ [[package]] name = "http" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "anyhow", "futures", @@ -2730,25 +2727,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - [[package]] name = "isahc" version = "1.7.2" @@ -3128,7 +3106,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "anyhow", "bindgen", @@ -3214,19 +3192,6 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" -[[package]] -name = "mio" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - [[package]] name = "naga" version = "0.14.2" @@ -3531,17 +3496,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "open" -version = "5.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - [[package]] name = "openssl-probe" version = "0.1.5" @@ -3636,12 +3590,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -4258,7 +4206,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "derive_refineable", ] @@ -4533,7 +4481,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "anyhow", "serde", @@ -4946,7 +4894,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "arrayvec", "log", @@ -5605,7 +5553,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#ed09bb949c2be7972234fe9cbb06def686182860" +source = "git+https://github.com/zed-industries/zed.git#5bc3846d592dc7e9acb81f709cc7257ae594bc77" dependencies = [ "anyhow", "async-fs 1.6.0", diff --git a/Cargo.toml b/Cargo.toml index 380ba5db..03e377b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ default-members = ["crates/app"] resolver = "2" [workspace.dependencies] +# gpui = { path = "/Users/jason/github/zed/crates/gpui" } gpui = { git = "https://github.com/zed-industries/zed.git" } ui = { path = "crates/ui" } story = { path = "crates/story" } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 1873211a..ef7a8d05 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -12,10 +12,6 @@ mod story_workspace; actions!(main_menu, [Quit, Copy]); fn init(app_state: Arc, cx: &mut AppContext) -> Result<()> { - if std::env::var("MTL_HUD_ENABLED").is_err() { - std::env::set_var("MTL_HUD_ENABLED", "1"); - } - story_workspace::init(app_state.clone(), cx); cx.bind_keys([ diff --git a/crates/story/src/popover_story.rs b/crates/story/src/popover_story.rs index e585d418..39e34561 100644 --- a/crates/story/src/popover_story.rs +++ b/crates/story/src/popover_story.rs @@ -10,6 +10,8 @@ use ui::{ input::TextInput, popover::{Popover, PopoverContent}, popup_menu::PopupMenu, + prelude::FluentBuilder, + switch::Switch, v_flex, Clickable, IconName, }; @@ -53,6 +55,8 @@ impl Render for Form { pub struct PopoverStory { focus_handle: FocusHandle, form: View
, + message: String, + window_mode: bool, } impl PopoverStory { @@ -65,20 +69,22 @@ impl PopoverStory { Self { form, focus_handle: cx.focus_handle(), + message: "".to_string(), + window_mode: false, } } fn on_copy(&mut self, _: &Copy, _: &mut ViewContext) { - println!("You have clicked copy"); + self.message = "You have clicked copy".to_string(); } fn on_cut(&mut self, _: &Cut, _: &mut ViewContext) { - println!("You have clicked cut"); + self.message = "You have clicked cut".to_string(); } fn on_paste(&mut self, _: &Paste, _: &mut ViewContext) { - println!("You have clicked paste"); + self.message = "You have clicked paste".to_string(); } fn on_search_all(&mut self, _: &SearchAll, _: &mut ViewContext) { - println!("You have clicked SearchAll"); + self.message = "You have clicked search all".to_string(); } } @@ -101,19 +107,21 @@ impl Render for PopoverStory { .on_action(cx.listener(Self::on_paste)) .on_action(cx.listener(Self::on_search_all)) .p_4() + .mb_5() .size_full() - .min_h(px(600.)) + .min_h(px(400.)) .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| { cx.focus(&this.focus_handle); })) - .child( - Button::new("test1", cx) - .label("Hello") - .on_click(move |_, cx| { - cx.dispatch_action(Box::new(Copy)); - }), - ) .gap_6() + .child( + Switch::new("switch-window-mode") + .checked(self.window_mode) + .label("Use Window Popover") + .on_click(cx.listener(|this, _, _| { + this.window_mode = !this.window_mode; + })), + ) .child( h_flex() .items_center() @@ -121,6 +129,7 @@ impl Render for PopoverStory { .child( v_flex().gap_4().child( Popover::new("info-top-left") + .when(self.window_mode, |this| this.window_mode()) .trigger(Button::new("info-top-left", cx).label("Top Left")) .content(|cx| { PopoverContent::new(cx, |cx| { @@ -141,12 +150,14 @@ impl Render for PopoverStory { ) .child( Popover::new("info-top-right") + .when(self.window_mode, |this| this.window_mode()) .anchor(AnchorCorner::TopRight) .trigger(Button::new("info-top-right", cx).label("Top Right")) .content(|cx| { PopoverContent::new(cx, |cx| { v_flex() .gap_4() + .w_96() .child("Hello, this is a Popover on the Top Right.") .child(Divider::horizontal()) .child( @@ -161,27 +172,31 @@ impl Render for PopoverStory { ), ) .child( - h_flex().child( - Popover::new("popup-menu") - .trigger(Button::new("popup-menu-1", cx).icon(IconName::Info)) - .content(move |cx| { - let focus_handle = focus_handle.clone(); - PopupMenu::build(cx, |mut this, _| { - this.content(focus_handle) - .menu("Copy", Box::new(Copy)) - .menu("Cut", Box::new(Cut)) - .menu("Paste", Box::new(Paste)) - .separator() - .menu_with_icon( - IconName::Search, - "Search", - Box::new(SearchAll), - ); + h_flex() + .gap_3() + .child( + Popover::new("popup-menu") + .when(self.window_mode, |this| this.window_mode()) + .trigger(Button::new("popup-menu-1", cx).icon(IconName::Info)) + .content(move |cx| { + let focus_handle = focus_handle.clone(); + PopupMenu::build(cx, |mut this, cx| { + this.content(focus_handle) + .menu("Copy", Box::new(Copy)) + .menu("Cut", Box::new(Cut)) + .menu("Paste", Box::new(Paste)) + .separator() + .menu_with_icon( + IconName::Search, + "Search", + Box::new(SearchAll), + ); - this - }) - }), - ), + this + }) + }), + ) + .child(self.message.clone()), ) .child( div().absolute().bottom_4().left_0().w_full().h_10().child( @@ -190,6 +205,7 @@ impl Render for PopoverStory { .justify_between() .child( Popover::new("info-bottom-left") + .when(self.window_mode, |this| this.window_mode()) .anchor(AnchorCorner::BottomLeft) .trigger( Button::new("pop", cx) @@ -200,6 +216,7 @@ impl Render for PopoverStory { ) .child( Popover::new("info-bottom-right") + .when(self.window_mode, |this| this.window_mode()) .anchor(AnchorCorner::BottomRight) .mouse_button(MouseButton::Right) .trigger( diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index b29da22d..0dc93ea8 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -9,7 +9,7 @@ publish = false doctest = false [dependencies] -gpui = { git = "https://github.com/zed-industries/zed.git" } +gpui.workspace = true anyhow = "1" log = "0.4" serde = "1.0.203" diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index e81ea4eb..588fc353 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -1,15 +1,21 @@ +use anyhow::Result; +use gpui::{ + actions, anchored, deferred, div, point, prelude::FluentBuilder as _, px, size, AnchorCorner, + AnyElement, AppContext, Bounds, Context, DismissEvent, DispatchPhase, Element, ElementId, + EventEmitter, FocusHandle, FocusableView, Global, GlobalElementId, Hitbox, + InteractiveElement as _, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, + ParentElement, Pixels, Point, Render, Style, Styled, Subscription, View, ViewContext, + VisualContext, WindowBackgroundAppearance, WindowContext, WindowId, WindowOptions, +}; use std::{cell::RefCell, rc::Rc}; use crate::{theme::ActiveTheme, Selectable, StyledExt as _}; -use gpui::{ - anchored, deferred, div, prelude::FluentBuilder, AnchorCorner, AnyElement, AppContext, Bounds, - DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle, FocusableView, - GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, ManagedView, MouseButton, - MouseDownEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, View, - ViewContext, VisualContext, WindowContext, -}; -pub fn init(_cx: &AppContext) {} +actions!(popover, [Open, Dismiss]); + +pub fn init(cx: &mut AppContext) { + cx.set_global(PopoverWindowState { window_id: None }); +} pub struct PopoverContent { focus_handle: FocusHandle, @@ -45,18 +51,26 @@ impl Render for PopoverContent { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PopupMode { + View, + Window, +} + pub struct Popover { id: ElementId, anchor: AnchorCorner, trigger: Option AnyElement + 'static>>, content: Option View + 'static>>, mouse_button: MouseButton, + mode: PopupMode, } impl Popover where M: ManagedView, { + /// Create a new Popover with `view` mode. pub fn new(id: impl Into) -> Self { Self { id: id.into(), @@ -64,9 +78,16 @@ where trigger: None, content: None, mouse_button: MouseButton::Left, + mode: PopupMode::View, } } + /// Set Popover to use Window mode + pub fn window_mode(mut self) -> Self { + self.mode = PopupMode::Window; + self + } + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { self.anchor = anchor; self @@ -149,6 +170,7 @@ where pub struct PopoverElementState { trigger_layout_id: Option, + popover_layout_id: Option, popover_element: Option, trigger_element: Option, content_view: Rc>>>, @@ -160,6 +182,7 @@ impl Default for PopoverElementState { fn default() -> Self { Self { trigger_layout_id: None, + popover_layout_id: None, popover_element: None, trigger_element: None, content_view: Rc::new(RefCell::new(None)), @@ -172,11 +195,11 @@ pub struct PrepaintState { hitbox: Hitbox, /// Trigger bounds for limit a rect to handle mouse click. trigger_bounds: Option>, + popover_bounds: Option>, } impl Element for Popover { type RequestLayoutState = PopoverElementState; - type PrepaintState = PrepaintState; fn id(&self) -> Option { @@ -188,44 +211,60 @@ impl Element for Popover { id: Option<&gpui::GlobalElementId>, cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { - self.with_element_state(id.unwrap(), cx, |this, element_state, cx| { - let mut trigger_element = this.render_trigger(cx).into_any_element(); + let mode = self.mode; + + self.with_element_state(id.unwrap(), cx, |view, element_state, cx| { + let mut trigger_element = view.render_trigger(cx).into_any_element(); let trigger_layout_id = trigger_element.request_layout(cx); let mut popover_layout_id = None; let mut popover_element = None; if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() { - let content_view_mut = element_state.content_view.clone(); - - let mut anchored = anchored().snap_to_window().anchor(this.anchor); + let mut anchored = anchored().snap_to_window().anchor(view.anchor); if let Some(trigger_bounds) = element_state.trigger_bounds { - anchored = anchored.position(this.resolved_corner(trigger_bounds)); + anchored = anchored.position(view.resolved_corner(trigger_bounds)); } - let mut element = deferred( - anchored.child( - div() - .occlude() - .elevation_2(cx) - .bg(cx.theme().popover) - .border_1() - .border_color(cx.theme().border) - .map(|d| match this.anchor { - AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_2(), - AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_2(), - }) - .on_mouse_down_out(move |_, cx| { - // Update the element_state.content_view to `None`, - // so that the `paint`` method will not paint it. - *content_view_mut.borrow_mut() = None; - cx.refresh(); - }) - .child(content_view.clone()), - ), - ) - .with_priority(1) - .into_any(); + let mut element = if mode == PopupMode::Window { + // layout the content view, to let the popover know the size of the content for window size. + anchored + .child( + div() + .border_1() + .border_color(cx.theme().border) + .child(content_view.clone()), + ) + .into_any() + } else { + let content_view_mut = element_state.content_view.clone(); + deferred( + anchored.child( + div() + .size_full() + .occlude() + .elevation_2(cx) + .bg(cx.theme().popover) + .border_1() + .border_color(cx.theme().border) + .map(|this| match view.anchor { + AnchorCorner::TopLeft | AnchorCorner::TopRight => this.top_2(), + AnchorCorner::BottomLeft | AnchorCorner::BottomRight => { + this.bottom_2() + } + }) + .child(content_view.clone()) + .on_mouse_down_out(move |_, cx| { + // Update the element_state.content_view to `None`, + // so that the `paint`` method will not paint it. + *content_view_mut.borrow_mut() = None; + cx.refresh(); + }), + ), + ) + .with_priority(1) + .into_any() + }; popover_layout_id = Some(element.request_layout(cx)); popover_element = Some(element); @@ -240,7 +279,8 @@ impl Element for Popover { layout_id, PopoverElementState { trigger_layout_id: Some(trigger_layout_id), - popover_element: popover_element, + popover_layout_id, + popover_element, trigger_element: Some(trigger_element), ..Default::default() }, @@ -265,10 +305,17 @@ impl Element for Popover { let trigger_bounds = request_layout .trigger_layout_id .map(|id| cx.layout_bounds(id)); + + // Prepare the popover, for get the bounds of it for open window size. + let popover_bounds = request_layout + .popover_layout_id + .map(|id| cx.layout_bounds(id)); + let hitbox = cx.insert_hitbox(trigger_bounds.unwrap_or_default(), false); PrepaintState { trigger_bounds, + popover_bounds, hitbox, } } @@ -281,6 +328,8 @@ impl Element for Popover { prepaint: &mut Self::PrepaintState, cx: &mut WindowContext, ) { + let anchor = self.anchor; + let mode = self.mode; self.with_element_state(id.unwrap(), cx, |this, element_state, cx| { element_state.trigger_bounds = prepaint.trigger_bounds; @@ -288,24 +337,44 @@ impl Element for Popover { element.paint(cx); } - if let Some(mut element) = request_layout.popover_element.take() { - element.paint(cx); - return; + if mode == PopupMode::Window { + if let Some(content_view) = element_state.content_view.take() { + let popover_bounds = prepaint.popover_bounds.unwrap(); + let trigger_bounds = prepaint.trigger_bounds.unwrap(); + + PopoverWindow::open_popover( + content_view, + trigger_bounds, + popover_bounds, + anchor, + cx, + ) + .expect("BUG: failed to open popover window."); + + return; + } + } else { + if let Some(mut element) = request_layout.popover_element.take() { + element.paint(cx); + return; + } } + // When mouse click down in the trigger bounds, open the popover. let Some(content_build) = this.content.take() else { return; }; - let old_content_view = element_state.content_view.clone(); let hitbox_id = prepaint.hitbox.id; let mouse_button = this.mouse_button; - // When mouse click down in the trigger bounds, open the popover. cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && event.button == mouse_button && hitbox_id.is_hovered(cx) { + cx.stop_propagation(); + cx.prevent_default(); + let new_content_view = (content_build)(cx); let old_content_view1 = old_content_view.clone(); @@ -317,6 +386,8 @@ impl Element for Popover { } } *old_content_view1.borrow_mut() = None; + close_popover(cx); + cx.refresh(); }) .detach(); @@ -326,6 +397,239 @@ impl Element for Popover { cx.refresh(); } }); + + // Click parent window to dimiss popover + if mode == PopupMode::Window { + let content_view = element_state.content_view.clone(); + cx.on_mouse_event(move |_: &MouseDownEvent, _, cx| { + *content_view.borrow_mut() = None; + close_popover(cx); + }); + } }); } } + +struct PopoverWindowState { + window_id: Option, +} + +impl Global for PopoverWindowState {} + +impl PopoverWindowState { + fn window_id(cx: &AppContext) -> Option { + cx.try_global::().and_then(|state| state.window_id) + } + + fn set_window_id(window_id: WindowId, cx: &mut WindowContext) { + cx.set_global(PopoverWindowState { + window_id: Some(window_id), + }); + } + + fn close_window(cx: &mut AppContext) { + if let Some(window) = cx + .windows() + .into_iter() + .find(|window| Some(window.window_id()) == PopoverWindowState::window_id(cx)) + { + cx.update_window(window, |_, cx| { + cx.remove_window(); + }) + .ok(); + } + } +} + +pub struct PopoverWindow { + view: View, + anchor: AnchorCorner, + close_when_deactivate: bool, + _subscriptions: Vec, +} + +pub fn close_popover(cx: &mut AppContext) { + PopoverWindowState::close_window(cx); +} + +impl PopoverWindow +where + M: ManagedView, +{ + pub fn open_popover( + view: View, + trigger_bounds: Bounds, + bounds: Bounds, + anchor: AnchorCorner, + cx: &mut WindowContext, + ) -> Result<()> { + // Every open_popover will close the existing one + PopoverWindowState::close_window(cx); + + let display = cx.display(); + let window_bounds = cx.bounds(); + + // TODO: avoid out of the screen bounds + + let border_bounds = if cfg!(target_os = "windows") { + Bounds { + origin: point(px(-8.0), px(5.0)), + size: size(px(16.0), px(8.0)), + } + } else { + Bounds { + origin: point(px(-8.0), px(0.0)), + size: size(px(20.0), px(20.0)), + } + }; + + let trigger_screen_bounds = Bounds { + origin: window_bounds.origin + trigger_bounds.origin + border_bounds.origin, + size: trigger_bounds.size, + }; + + let popover_offset = px(2.); + let popover_origin = match anchor { + AnchorCorner::TopLeft => { + trigger_screen_bounds.lower_left() + point(px(0.), popover_offset) + } + AnchorCorner::TopRight => { + trigger_screen_bounds.lower_right() + point(-bounds.size.width, popover_offset) + } + AnchorCorner::BottomLeft => { + trigger_screen_bounds.origin + - point( + px(0.0), + bounds.size.height + border_bounds.size.height + popover_offset, + ) + } + AnchorCorner::BottomRight => { + trigger_screen_bounds.upper_right() + - point( + bounds.size.width, + bounds.size.height + border_bounds.size.height + popover_offset, + ) + } + }; + + let bounds = Bounds { + origin: popover_origin, + size: size( + bounds.size.width + border_bounds.size.width, + bounds.size.height + border_bounds.size.height, + ), + }; + + let view = view.clone(); + cx.spawn(|mut cx| async move { + let window = cx + .open_window( + WindowOptions { + titlebar: None, + window_bounds: Some(gpui::WindowBounds::Windowed(bounds)), + window_background: WindowBackgroundAppearance::Transparent, + // NOTE: on Windows in currently must use PopUp kind, otherwise the window will be sizeable. + // And the PopUp kind can fast open. + kind: gpui::WindowKind::PopUp, + is_movable: false, + focus: true, + show: true, + display_id: display.map(|d| d.id()), + ..Default::default() + }, + |cx| { + let mut _subscriptions = Vec::new(); + + let view = cx.new_view(|cx| { + // Listen to window diactivation to close window + _subscriptions.push( + cx.observe_window_activation(Self::window_activation_changed), + ); + + PopoverWindow { + view, + anchor, + close_when_deactivate: true, + _subscriptions, + } + }); + + view + }, + ) + .expect("BUG: faild to create a new window."); + + cx.update(|cx| { + PopoverWindowState::set_window_id(window.window_id(), cx); + }) + .expect("BUG: failed to set window id.") + }) + .detach(); + + Ok(()) + } +} + +impl PopoverWindow +where + M: ManagedView, +{ + fn window_activation_changed(&mut self, cx: &mut ViewContext) { + if self.close_when_deactivate { + if !cx.is_window_active() { + self.dismiss(cx); + } + } + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.remove_window(); + } +} + +impl FocusableView for PopoverWindow +where + M: ManagedView, +{ + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.view.focus_handle(cx) + } +} + +impl Render for PopoverWindow +where + M: ManagedView, +{ + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { + let is_windows = cfg!(target_os = "windows"); + + div() + .id("PopoverWindow") + .size_full() + .when(!is_windows, |this| this.p_2()) + .when(is_windows, |this| this.bg(cx.theme().popover)) + .text_color(cx.theme().popover_foreground) + // Leave margin for show window shadow + .map(|d| match self.anchor { + AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_8(), + AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_8(), + }) + .child( + div() + .when(!is_windows, |this| { + this.bg(cx.theme().popover) + .border_1() + .border_color(cx.theme().border) + .elevation_2(cx) + }) + .bg(cx.theme().popover) + .child(self.view.clone()) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|_, _, cx| { + PopoverWindowState::close_window(cx); + }), + ), + ) + } +} diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 649a5449..c9a72832 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -1,12 +1,13 @@ use std::rc::Rc; use gpui::{ - actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, KeyBinding, ParentElement, Render, - SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext, + actions, div, prelude::FluentBuilder, px, Action, AnyWindowHandle, AppContext, Context, + DismissEvent, EventEmitter, FocusHandle, FocusableView, InteractiveElement, KeyBinding, + ParentElement, Render, SharedString, Styled as _, View, ViewContext, VisualContext as _, + WindowContext, }; -use crate::{h_flex, list::ListItem, theme::ActiveTheme, v_flex, Icon}; +use crate::{h_flex, list::ListItem, theme::ActiveTheme, v_flex, Icon, StyledExt}; actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]); @@ -37,6 +38,8 @@ impl PopupMenuItem { pub struct PopupMenu { focus_handle: FocusHandle, + /// The parent window handle + window_handle: AnyWindowHandle, action_context: Option, menu_items: Vec, selected_index: Option, @@ -57,6 +60,7 @@ impl PopupMenu { let menu = Self { focus_handle, action_context: None, + window_handle: cx.window_handle(), menu_items: Vec::new(), selected_index: None, _subscriptions: [_on_blur_subscription], @@ -68,8 +72,8 @@ impl PopupMenu { /// You must set content (FocusHandle) with the parent view, if the menu action is listening on the parent view. /// When the Menu Item confirmed, the parent view will be focused again to ensure to receive the action. - pub fn content(&mut self, content: FocusHandle) -> &mut Self { - self.action_context = Some(content); + pub fn content(&mut self, focus_handle: FocusHandle) -> &mut Self { + self.action_context = Some(focus_handle); self } @@ -94,14 +98,19 @@ impl PopupMenu { label: impl Into, action: Box, ) -> &mut Self { + let window_handle = self.window_handle; self.menu_items.push(PopupMenuItem::Item { icon, label: label.into(), - handler: Rc::new(move |content, cx| { - if let Some(content) = &content { - cx.focus(content); + handler: Rc::new(move |handle, cx| { + if let Some(handle) = handle { + cx.update_window(window_handle, |_, cx| { + cx.activate_window(); + cx.focus(handle); + cx.dispatch_action(action.boxed_clone()); + }) + .unwrap(); } - cx.dispatch_action(action.boxed_clone()); }), }); self @@ -129,13 +138,14 @@ impl PopupMenu { } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - let content = self.action_context.as_ref(); + let handle = self.action_context.as_ref(); match self.selected_index { Some(index) => { let item = self.menu_items.get(index); match item { Some(PopupMenuItem::Item { handler, .. }) => { - handler(content, cx); + handler(handle, cx); + self.dismiss(&Dismiss, cx) } _ => {} }