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.
This commit is contained in:
Jason Lee 2024-07-11 16:20:26 +08:00 committed by GitHub
parent 4d9077fe85
commit eb43c78cea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 433 additions and 157 deletions

76
Cargo.lock generated
View file

@ -117,9 +117,8 @@ dependencies = [
[[package]] [[package]]
name = "ashpd" name = "ashpd"
version = "0.8.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
dependencies = [ dependencies = [
"async-fs 2.1.2", "async-fs 2.1.2",
"async-net 2.0.0", "async-net 2.0.0",
@ -917,7 +916,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" 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 = [ dependencies = [
"rustc-hash", "rustc-hash",
] ]
@ -1230,7 +1229,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2181,7 +2180,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@ -2222,11 +2221,9 @@ dependencies = [
"log", "log",
"media", "media",
"metal", "metal",
"mio",
"num_cpus", "num_cpus",
"objc", "objc",
"oo7", "oo7",
"open",
"parking", "parking",
"parking_lot", "parking_lot",
"pathfinder_geometry", "pathfinder_geometry",
@ -2269,7 +2266,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2454,7 +2451,7 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"futures", "futures",
@ -2730,25 +2727,6 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "isahc" name = "isahc"
version = "1.7.2" version = "1.7.2"
@ -3128,7 +3106,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen",
@ -3214,19 +3192,6 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" 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]] [[package]]
name = "naga" name = "naga"
version = "0.14.2" version = "0.14.2"
@ -3531,17 +3496,6 @@ dependencies = [
"zvariant", "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]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.5" version = "0.1.5"
@ -3636,12 +3590,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "pathfinder_geometry" name = "pathfinder_geometry"
version = "0.5.1" version = "0.5.1"
@ -4258,7 +4206,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" 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 = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@ -4533,7 +4481,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@ -4946,7 +4894,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" 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 = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@ -5605,7 +5553,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" 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 = [ dependencies = [
"anyhow", "anyhow",
"async-fs 1.6.0", "async-fs 1.6.0",

View file

@ -5,6 +5,7 @@ default-members = ["crates/app"]
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.dependencies]
# gpui = { path = "/Users/jason/github/zed/crates/gpui" }
gpui = { git = "https://github.com/zed-industries/zed.git" } gpui = { git = "https://github.com/zed-industries/zed.git" }
ui = { path = "crates/ui" } ui = { path = "crates/ui" }
story = { path = "crates/story" } story = { path = "crates/story" }

View file

@ -12,10 +12,6 @@ mod story_workspace;
actions!(main_menu, [Quit, Copy]); actions!(main_menu, [Quit, Copy]);
fn init(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> { fn init(app_state: Arc<AppState>, 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); story_workspace::init(app_state.clone(), cx);
cx.bind_keys([ cx.bind_keys([

View file

@ -10,6 +10,8 @@ use ui::{
input::TextInput, input::TextInput,
popover::{Popover, PopoverContent}, popover::{Popover, PopoverContent},
popup_menu::PopupMenu, popup_menu::PopupMenu,
prelude::FluentBuilder,
switch::Switch,
v_flex, Clickable, IconName, v_flex, Clickable, IconName,
}; };
@ -53,6 +55,8 @@ impl Render for Form {
pub struct PopoverStory { pub struct PopoverStory {
focus_handle: FocusHandle, focus_handle: FocusHandle,
form: View<Form>, form: View<Form>,
message: String,
window_mode: bool,
} }
impl PopoverStory { impl PopoverStory {
@ -65,20 +69,22 @@ impl PopoverStory {
Self { Self {
form, form,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
message: "".to_string(),
window_mode: false,
} }
} }
fn on_copy(&mut self, _: &Copy, _: &mut ViewContext<Self>) { fn on_copy(&mut self, _: &Copy, _: &mut ViewContext<Self>) {
println!("You have clicked copy"); self.message = "You have clicked copy".to_string();
} }
fn on_cut(&mut self, _: &Cut, _: &mut ViewContext<Self>) { fn on_cut(&mut self, _: &Cut, _: &mut ViewContext<Self>) {
println!("You have clicked cut"); self.message = "You have clicked cut".to_string();
} }
fn on_paste(&mut self, _: &Paste, _: &mut ViewContext<Self>) { fn on_paste(&mut self, _: &Paste, _: &mut ViewContext<Self>) {
println!("You have clicked paste"); self.message = "You have clicked paste".to_string();
} }
fn on_search_all(&mut self, _: &SearchAll, _: &mut ViewContext<Self>) { fn on_search_all(&mut self, _: &SearchAll, _: &mut ViewContext<Self>) {
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_paste))
.on_action(cx.listener(Self::on_search_all)) .on_action(cx.listener(Self::on_search_all))
.p_4() .p_4()
.mb_5()
.size_full() .size_full()
.min_h(px(600.)) .min_h(px(400.))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| { .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| {
cx.focus(&this.focus_handle); cx.focus(&this.focus_handle);
})) }))
.child(
Button::new("test1", cx)
.label("Hello")
.on_click(move |_, cx| {
cx.dispatch_action(Box::new(Copy));
}),
)
.gap_6() .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( .child(
h_flex() h_flex()
.items_center() .items_center()
@ -121,6 +129,7 @@ impl Render for PopoverStory {
.child( .child(
v_flex().gap_4().child( v_flex().gap_4().child(
Popover::new("info-top-left") Popover::new("info-top-left")
.when(self.window_mode, |this| this.window_mode())
.trigger(Button::new("info-top-left", cx).label("Top Left")) .trigger(Button::new("info-top-left", cx).label("Top Left"))
.content(|cx| { .content(|cx| {
PopoverContent::new(cx, |cx| { PopoverContent::new(cx, |cx| {
@ -141,12 +150,14 @@ impl Render for PopoverStory {
) )
.child( .child(
Popover::new("info-top-right") Popover::new("info-top-right")
.when(self.window_mode, |this| this.window_mode())
.anchor(AnchorCorner::TopRight) .anchor(AnchorCorner::TopRight)
.trigger(Button::new("info-top-right", cx).label("Top Right")) .trigger(Button::new("info-top-right", cx).label("Top Right"))
.content(|cx| { .content(|cx| {
PopoverContent::new(cx, |cx| { PopoverContent::new(cx, |cx| {
v_flex() v_flex()
.gap_4() .gap_4()
.w_96()
.child("Hello, this is a Popover on the Top Right.") .child("Hello, this is a Popover on the Top Right.")
.child(Divider::horizontal()) .child(Divider::horizontal())
.child( .child(
@ -161,27 +172,31 @@ impl Render for PopoverStory {
), ),
) )
.child( .child(
h_flex().child( h_flex()
Popover::new("popup-menu") .gap_3()
.trigger(Button::new("popup-menu-1", cx).icon(IconName::Info)) .child(
.content(move |cx| { Popover::new("popup-menu")
let focus_handle = focus_handle.clone(); .when(self.window_mode, |this| this.window_mode())
PopupMenu::build(cx, |mut this, _| { .trigger(Button::new("popup-menu-1", cx).icon(IconName::Info))
this.content(focus_handle) .content(move |cx| {
.menu("Copy", Box::new(Copy)) let focus_handle = focus_handle.clone();
.menu("Cut", Box::new(Cut)) PopupMenu::build(cx, |mut this, cx| {
.menu("Paste", Box::new(Paste)) this.content(focus_handle)
.separator() .menu("Copy", Box::new(Copy))
.menu_with_icon( .menu("Cut", Box::new(Cut))
IconName::Search, .menu("Paste", Box::new(Paste))
"Search", .separator()
Box::new(SearchAll), .menu_with_icon(
); IconName::Search,
"Search",
Box::new(SearchAll),
);
this this
}) })
}), }),
), )
.child(self.message.clone()),
) )
.child( .child(
div().absolute().bottom_4().left_0().w_full().h_10().child( div().absolute().bottom_4().left_0().w_full().h_10().child(
@ -190,6 +205,7 @@ impl Render for PopoverStory {
.justify_between() .justify_between()
.child( .child(
Popover::new("info-bottom-left") Popover::new("info-bottom-left")
.when(self.window_mode, |this| this.window_mode())
.anchor(AnchorCorner::BottomLeft) .anchor(AnchorCorner::BottomLeft)
.trigger( .trigger(
Button::new("pop", cx) Button::new("pop", cx)
@ -200,6 +216,7 @@ impl Render for PopoverStory {
) )
.child( .child(
Popover::new("info-bottom-right") Popover::new("info-bottom-right")
.when(self.window_mode, |this| this.window_mode())
.anchor(AnchorCorner::BottomRight) .anchor(AnchorCorner::BottomRight)
.mouse_button(MouseButton::Right) .mouse_button(MouseButton::Right)
.trigger( .trigger(

View file

@ -9,7 +9,7 @@ publish = false
doctest = false doctest = false
[dependencies] [dependencies]
gpui = { git = "https://github.com/zed-industries/zed.git" } gpui.workspace = true
anyhow = "1" anyhow = "1"
log = "0.4" log = "0.4"
serde = "1.0.203" serde = "1.0.203"

View file

@ -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 std::{cell::RefCell, rc::Rc};
use crate::{theme::ActiveTheme, Selectable, StyledExt as _}; 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 { pub struct PopoverContent {
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -45,18 +51,26 @@ impl Render for PopoverContent {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PopupMode {
View,
Window,
}
pub struct Popover<M: ManagedView> { pub struct Popover<M: ManagedView> {
id: ElementId, id: ElementId,
anchor: AnchorCorner, anchor: AnchorCorner,
trigger: Option<Box<dyn FnOnce(&WindowContext) -> AnyElement + 'static>>, trigger: Option<Box<dyn FnOnce(&WindowContext) -> AnyElement + 'static>>,
content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>, content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
mouse_button: MouseButton, mouse_button: MouseButton,
mode: PopupMode,
} }
impl<M> Popover<M> impl<M> Popover<M>
where where
M: ManagedView, M: ManagedView,
{ {
/// Create a new Popover with `view` mode.
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
id: id.into(), id: id.into(),
@ -64,9 +78,16 @@ where
trigger: None, trigger: None,
content: None, content: None,
mouse_button: MouseButton::Left, 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 { pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor = anchor; self.anchor = anchor;
self self
@ -149,6 +170,7 @@ where
pub struct PopoverElementState<M> { pub struct PopoverElementState<M> {
trigger_layout_id: Option<LayoutId>, trigger_layout_id: Option<LayoutId>,
popover_layout_id: Option<LayoutId>,
popover_element: Option<AnyElement>, popover_element: Option<AnyElement>,
trigger_element: Option<AnyElement>, trigger_element: Option<AnyElement>,
content_view: Rc<RefCell<Option<View<M>>>>, content_view: Rc<RefCell<Option<View<M>>>>,
@ -160,6 +182,7 @@ impl<M> Default for PopoverElementState<M> {
fn default() -> Self { fn default() -> Self {
Self { Self {
trigger_layout_id: None, trigger_layout_id: None,
popover_layout_id: None,
popover_element: None, popover_element: None,
trigger_element: None, trigger_element: None,
content_view: Rc::new(RefCell::new(None)), content_view: Rc::new(RefCell::new(None)),
@ -172,11 +195,11 @@ pub struct PrepaintState {
hitbox: Hitbox, hitbox: Hitbox,
/// Trigger bounds for limit a rect to handle mouse click. /// Trigger bounds for limit a rect to handle mouse click.
trigger_bounds: Option<Bounds<Pixels>>, trigger_bounds: Option<Bounds<Pixels>>,
popover_bounds: Option<Bounds<Pixels>>,
} }
impl<M: ManagedView> Element for Popover<M> { impl<M: ManagedView> Element for Popover<M> {
type RequestLayoutState = PopoverElementState<M>; type RequestLayoutState = PopoverElementState<M>;
type PrepaintState = PrepaintState; type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> { fn id(&self) -> Option<ElementId> {
@ -188,44 +211,60 @@ impl<M: ManagedView> Element for Popover<M> {
id: Option<&gpui::GlobalElementId>, id: Option<&gpui::GlobalElementId>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) { ) -> (gpui::LayoutId, Self::RequestLayoutState) {
self.with_element_state(id.unwrap(), cx, |this, element_state, cx| { let mode = self.mode;
let mut trigger_element = this.render_trigger(cx).into_any_element();
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 trigger_layout_id = trigger_element.request_layout(cx);
let mut popover_layout_id = None; let mut popover_layout_id = None;
let mut popover_element = None; let mut popover_element = None;
if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() { 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(view.anchor);
let mut anchored = anchored().snap_to_window().anchor(this.anchor);
if let Some(trigger_bounds) = element_state.trigger_bounds { 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( let mut element = if mode == PopupMode::Window {
anchored.child( // layout the content view, to let the popover know the size of the content for window size.
div() anchored
.occlude() .child(
.elevation_2(cx) div()
.bg(cx.theme().popover) .border_1()
.border_1() .border_color(cx.theme().border)
.border_color(cx.theme().border) .child(content_view.clone()),
.map(|d| match this.anchor { )
AnchorCorner::TopLeft | AnchorCorner::TopRight => d.mt_2(), .into_any()
AnchorCorner::BottomLeft | AnchorCorner::BottomRight => d.mb_2(), } else {
}) let content_view_mut = element_state.content_view.clone();
.on_mouse_down_out(move |_, cx| { deferred(
// Update the element_state.content_view to `None`, anchored.child(
// so that the `paint`` method will not paint it. div()
*content_view_mut.borrow_mut() = None; .size_full()
cx.refresh(); .occlude()
}) .elevation_2(cx)
.child(content_view.clone()), .bg(cx.theme().popover)
), .border_1()
) .border_color(cx.theme().border)
.with_priority(1) .map(|this| match view.anchor {
.into_any(); 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_layout_id = Some(element.request_layout(cx));
popover_element = Some(element); popover_element = Some(element);
@ -240,7 +279,8 @@ impl<M: ManagedView> Element for Popover<M> {
layout_id, layout_id,
PopoverElementState { PopoverElementState {
trigger_layout_id: Some(trigger_layout_id), trigger_layout_id: Some(trigger_layout_id),
popover_element: popover_element, popover_layout_id,
popover_element,
trigger_element: Some(trigger_element), trigger_element: Some(trigger_element),
..Default::default() ..Default::default()
}, },
@ -265,10 +305,17 @@ impl<M: ManagedView> Element for Popover<M> {
let trigger_bounds = request_layout let trigger_bounds = request_layout
.trigger_layout_id .trigger_layout_id
.map(|id| cx.layout_bounds(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); let hitbox = cx.insert_hitbox(trigger_bounds.unwrap_or_default(), false);
PrepaintState { PrepaintState {
trigger_bounds, trigger_bounds,
popover_bounds,
hitbox, hitbox,
} }
} }
@ -281,6 +328,8 @@ impl<M: ManagedView> Element for Popover<M> {
prepaint: &mut Self::PrepaintState, prepaint: &mut Self::PrepaintState,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let anchor = self.anchor;
let mode = self.mode;
self.with_element_state(id.unwrap(), cx, |this, element_state, cx| { self.with_element_state(id.unwrap(), cx, |this, element_state, cx| {
element_state.trigger_bounds = prepaint.trigger_bounds; element_state.trigger_bounds = prepaint.trigger_bounds;
@ -288,24 +337,44 @@ impl<M: ManagedView> Element for Popover<M> {
element.paint(cx); element.paint(cx);
} }
if let Some(mut element) = request_layout.popover_element.take() { if mode == PopupMode::Window {
element.paint(cx); if let Some(content_view) = element_state.content_view.take() {
return; 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 { let Some(content_build) = this.content.take() else {
return; return;
}; };
let old_content_view = element_state.content_view.clone(); let old_content_view = element_state.content_view.clone();
let hitbox_id = prepaint.hitbox.id; let hitbox_id = prepaint.hitbox.id;
let mouse_button = this.mouse_button; 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| { cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble if phase == DispatchPhase::Bubble
&& event.button == mouse_button && event.button == mouse_button
&& hitbox_id.is_hovered(cx) && hitbox_id.is_hovered(cx)
{ {
cx.stop_propagation();
cx.prevent_default();
let new_content_view = (content_build)(cx); let new_content_view = (content_build)(cx);
let old_content_view1 = old_content_view.clone(); let old_content_view1 = old_content_view.clone();
@ -317,6 +386,8 @@ impl<M: ManagedView> Element for Popover<M> {
} }
} }
*old_content_view1.borrow_mut() = None; *old_content_view1.borrow_mut() = None;
close_popover(cx);
cx.refresh(); cx.refresh();
}) })
.detach(); .detach();
@ -326,6 +397,239 @@ impl<M: ManagedView> Element for Popover<M> {
cx.refresh(); 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<WindowId>,
}
impl Global for PopoverWindowState {}
impl PopoverWindowState {
fn window_id(cx: &AppContext) -> Option<WindowId> {
cx.try_global::<Self>().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<M: ManagedView> {
view: View<M>,
anchor: AnchorCorner,
close_when_deactivate: bool,
_subscriptions: Vec<Subscription>,
}
pub fn close_popover(cx: &mut AppContext) {
PopoverWindowState::close_window(cx);
}
impl<M> PopoverWindow<M>
where
M: ManagedView,
{
pub fn open_popover(
view: View<M>,
trigger_bounds: Bounds<Pixels>,
bounds: Bounds<Pixels>,
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<M> PopoverWindow<M>
where
M: ManagedView,
{
fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
if self.close_when_deactivate {
if !cx.is_window_active() {
self.dismiss(cx);
}
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
}
}
impl<M> FocusableView for PopoverWindow<M>
where
M: ManagedView,
{
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.view.focus_handle(cx)
}
}
impl<M> Render for PopoverWindow<M>
where
M: ManagedView,
{
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> 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);
}),
),
)
}
}

View file

@ -1,12 +1,13 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter, actions, div, prelude::FluentBuilder, px, Action, AnyWindowHandle, AppContext, Context,
FocusHandle, FocusableView, InteractiveElement, KeyBinding, ParentElement, Render, DismissEvent, EventEmitter, FocusHandle, FocusableView, InteractiveElement, KeyBinding,
SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext, 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]); actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
@ -37,6 +38,8 @@ impl PopupMenuItem {
pub struct PopupMenu { pub struct PopupMenu {
focus_handle: FocusHandle, focus_handle: FocusHandle,
/// The parent window handle
window_handle: AnyWindowHandle,
action_context: Option<FocusHandle>, action_context: Option<FocusHandle>,
menu_items: Vec<PopupMenuItem>, menu_items: Vec<PopupMenuItem>,
selected_index: Option<usize>, selected_index: Option<usize>,
@ -57,6 +60,7 @@ impl PopupMenu {
let menu = Self { let menu = Self {
focus_handle, focus_handle,
action_context: None, action_context: None,
window_handle: cx.window_handle(),
menu_items: Vec::new(), menu_items: Vec::new(),
selected_index: None, selected_index: None,
_subscriptions: [_on_blur_subscription], _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. /// 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. /// 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 { pub fn content(&mut self, focus_handle: FocusHandle) -> &mut Self {
self.action_context = Some(content); self.action_context = Some(focus_handle);
self self
} }
@ -94,14 +98,19 @@ impl PopupMenu {
label: impl Into<SharedString>, label: impl Into<SharedString>,
action: Box<dyn Action>, action: Box<dyn Action>,
) -> &mut Self { ) -> &mut Self {
let window_handle = self.window_handle;
self.menu_items.push(PopupMenuItem::Item { self.menu_items.push(PopupMenuItem::Item {
icon, icon,
label: label.into(), label: label.into(),
handler: Rc::new(move |content, cx| { handler: Rc::new(move |handle, cx| {
if let Some(content) = &content { if let Some(handle) = handle {
cx.focus(content); 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 self
@ -129,13 +138,14 @@ impl PopupMenu {
} }
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
let content = self.action_context.as_ref(); let handle = self.action_context.as_ref();
match self.selected_index { match self.selected_index {
Some(index) => { Some(index) => {
let item = self.menu_items.get(index); let item = self.menu_items.get(index);
match item { match item {
Some(PopupMenuItem::Item { handler, .. }) => { Some(PopupMenuItem::Item { handler, .. }) => {
handler(content, cx); handler(handle, cx);
self.dismiss(&Dismiss, cx)
} }
_ => {} _ => {}
} }