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:
parent
4d9077fe85
commit
eb43c78cea
7 changed files with 433 additions and 157 deletions
76
Cargo.lock
generated
76
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue