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]]
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",

View file

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

View file

@ -12,10 +12,6 @@ mod story_workspace;
actions!(main_menu, [Quit, Copy]);
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);
cx.bind_keys([

View file

@ -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<Form>,
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<Self>) {
println!("You have clicked copy");
self.message = "You have clicked copy".to_string();
}
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>) {
println!("You have clicked paste");
self.message = "You have clicked paste".to_string();
}
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_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(

View file

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

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 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<M: ManagedView> {
id: ElementId,
anchor: AnchorCorner,
trigger: Option<Box<dyn FnOnce(&WindowContext) -> AnyElement + 'static>>,
content: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
mouse_button: MouseButton,
mode: PopupMode,
}
impl<M> Popover<M>
where
M: ManagedView,
{
/// Create a new Popover with `view` mode.
pub fn new(id: impl Into<ElementId>) -> 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<M> {
trigger_layout_id: Option<LayoutId>,
popover_layout_id: Option<LayoutId>,
popover_element: Option<AnyElement>,
trigger_element: Option<AnyElement>,
content_view: Rc<RefCell<Option<View<M>>>>,
@ -160,6 +182,7 @@ impl<M> Default for PopoverElementState<M> {
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<Bounds<Pixels>>,
popover_bounds: Option<Bounds<Pixels>>,
}
impl<M: ManagedView> Element for Popover<M> {
type RequestLayoutState = PopoverElementState<M>;
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
@ -188,44 +211,60 @@ impl<M: ManagedView> Element for Popover<M> {
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<M: ManagedView> Element for Popover<M> {
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<M: ManagedView> Element for Popover<M> {
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<M: ManagedView> Element for Popover<M> {
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<M: ManagedView> Element for Popover<M> {
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<M: ManagedView> Element for Popover<M> {
}
}
*old_content_view1.borrow_mut() = None;
close_popover(cx);
cx.refresh();
})
.detach();
@ -326,6 +397,239 @@ impl<M: ManagedView> Element for Popover<M> {
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 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<FocusHandle>,
menu_items: Vec<PopupMenuItem>,
selected_index: Option<usize>,
@ -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<SharedString>,
action: Box<dyn Action>,
) -> &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<Self>) {
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)
}
_ => {}
}