mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-04 06:34:26 +00:00
GPUI custom window prompts (#8980)
This adds a GPUI fallback for window prompts. Linux does not support this feature by default, so we have to implement it ourselves. This implementation also makes it possible for GPUI clients to override the platform prompts with their own implementations. This is just a first pass. These alerts are not keyboard accessible yet, does not reflect the prompt level, they're implemented in-window, rather than as popups, and the whole feature need a pass from a designer. Regardless, this gets us one step closer to Linux support :) <img width="650" alt="Screenshot 2024-03-06 at 5 58 08 PM" src="https://github.com/zed-industries/zed/assets/2280405/972ebb55-fd1f-4066-969c-a87f63b22a6f"> Release Notes: - N/A
This commit is contained in:
parent
c8e03ce42a
commit
c0edb5bd6c
12 changed files with 339 additions and 28 deletions
|
@ -28,14 +28,14 @@ use util::{
|
|||
ResultExt,
|
||||
};
|
||||
|
||||
use crate::WindowAppearance;
|
||||
use crate::{
|
||||
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
|
||||
AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke,
|
||||
LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement,
|
||||
TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId,
|
||||
LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, PromptBuilder,
|
||||
PromptHandle, PromptLevel, Render, RenderablePromptHandle, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
|
||||
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
|
||||
mod async_context;
|
||||
|
@ -242,6 +242,7 @@ pub struct AppContext {
|
|||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||
pub(crate) propagate_event: bool,
|
||||
pub(crate) prompt_builder: Option<PromptBuilder>,
|
||||
}
|
||||
|
||||
impl AppContext {
|
||||
|
@ -301,6 +302,7 @@ impl AppContext {
|
|||
quit_observers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
propagate_event: true,
|
||||
prompt_builder: Some(PromptBuilder::Default),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -1207,6 +1209,23 @@ impl AppContext {
|
|||
pub fn has_active_drag(&self) -> bool {
|
||||
self.active_drag.is_some()
|
||||
}
|
||||
|
||||
/// Set the prompt renderer for GPUI. This will replace the default or platform specific
|
||||
/// prompts with this custom implementation.
|
||||
pub fn set_prompt_builder(
|
||||
&mut self,
|
||||
renderer: impl Fn(
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
PromptHandle,
|
||||
&mut WindowContext,
|
||||
) -> RenderablePromptHandle
|
||||
+ 'static,
|
||||
) {
|
||||
self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for AppContext {
|
||||
|
|
|
@ -247,6 +247,16 @@ pub fn transparent_black() -> Hsla {
|
|||
}
|
||||
}
|
||||
|
||||
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
|
||||
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
|
||||
Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: lightness.clamp(0., 1.),
|
||||
a: opacity.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure white in [`Hsla`]
|
||||
pub fn white() -> Hsla {
|
||||
Hsla {
|
||||
|
|
|
@ -499,7 +499,7 @@ pub trait InteractiveElement: Sized {
|
|||
self
|
||||
}
|
||||
|
||||
/// Assign this elements
|
||||
/// Assign this element an ID, so that it can be used with interactivity
|
||||
fn id(mut self, id: impl Into<ElementId>) -> Stateful<Self> {
|
||||
self.interactivity().element_id = Some(id.into());
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize>;
|
||||
) -> Option<oneshot::Receiver<usize>>;
|
||||
fn activate(&self);
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_edited(&mut self, edited: bool);
|
||||
|
|
|
@ -310,15 +310,14 @@ impl PlatformWindow for WaylandWindow {
|
|||
self.0.inner.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> Receiver<usize> {
|
||||
unimplemented!()
|
||||
) -> Option<Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
|
|
|
@ -399,15 +399,14 @@ impl PlatformWindow for X11Window {
|
|||
self.0.inner.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn prompt(
|
||||
&self,
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
) -> futures::channel::oneshot::Receiver<usize> {
|
||||
unimplemented!()
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
|
|
|
@ -840,7 +840,7 @@ impl PlatformWindow for MacWindow {
|
|||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
) -> Option<oneshot::Receiver<usize>> {
|
||||
// macOs applies overrides to modal window buttons after they are added.
|
||||
// Two most important for this logic are:
|
||||
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
|
||||
|
@ -913,7 +913,7 @@ impl PlatformWindow for MacWindow {
|
|||
})
|
||||
.detach();
|
||||
|
||||
done_rx
|
||||
Some(done_rx)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -169,13 +169,15 @@ impl PlatformWindow for TestWindow {
|
|||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
) -> futures::channel::oneshot::Receiver<usize> {
|
||||
self.0
|
||||
.lock()
|
||||
.platform
|
||||
.upgrade()
|
||||
.expect("platform dropped")
|
||||
.prompt()
|
||||
) -> Option<futures::channel::oneshot::Receiver<usize>> {
|
||||
Some(
|
||||
self.0
|
||||
.lock()
|
||||
.platform
|
||||
.upgrade()
|
||||
.expect("platform dropped")
|
||||
.prompt(),
|
||||
)
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
|
|
|
@ -746,7 +746,7 @@ impl PlatformWindow for WindowsWindow {
|
|||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> Receiver<usize> {
|
||||
) -> Option<Receiver<usize>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ impl<V> Eq for WeakView<V> {}
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct AnyView {
|
||||
model: AnyModel,
|
||||
request_layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement),
|
||||
pub(crate) request_layout: fn(&AnyView, &mut ElementContext) -> (LayoutId, AnyElement),
|
||||
cache: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,10 @@ use std::{
|
|||
use util::{measure, ResultExt};
|
||||
|
||||
mod element_cx;
|
||||
mod prompts;
|
||||
|
||||
pub use element_cx::*;
|
||||
pub use prompts::*;
|
||||
|
||||
const ACTIVE_DRAG_Z_INDEX: u16 = 1;
|
||||
|
||||
|
@ -280,6 +283,7 @@ pub struct Window {
|
|||
pub(crate) focus: Option<FocusId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
prompt: Option<RenderablePromptHandle>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
@ -473,6 +477,7 @@ impl Window {
|
|||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
prompt: None,
|
||||
}
|
||||
}
|
||||
fn new_focus_listener(
|
||||
|
@ -960,6 +965,7 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
|
||||
let root_view = self.window.root_view.take().unwrap();
|
||||
let mut prompt = self.window.prompt.take();
|
||||
self.with_element_context(|cx| {
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| {
|
||||
|
@ -978,10 +984,24 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
|
||||
let available_space = cx.window.viewport_size.map(Into::into);
|
||||
root_view.draw(Point::default(), available_space, cx);
|
||||
|
||||
let origin = Point::default();
|
||||
cx.paint_view(root_view.entity_id(), |cx| {
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
let (layout_id, mut rendered_element) =
|
||||
(root_view.request_layout)(&root_view, cx);
|
||||
cx.compute_layout(layout_id, available_space);
|
||||
rendered_element.paint(cx);
|
||||
|
||||
if let Some(prompt) = &mut prompt {
|
||||
prompt.paint(cx).draw(origin, available_space, cx)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
self.window.prompt = prompt;
|
||||
|
||||
if let Some(active_drag) = self.app.active_drag.take() {
|
||||
self.with_element_context(|cx| {
|
||||
|
@ -1551,15 +1571,48 @@ impl<'a> WindowContext<'a> {
|
|||
/// The provided message will be presented, along with buttons for each answer.
|
||||
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
|
||||
pub fn prompt(
|
||||
&self,
|
||||
&mut self,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
self.window
|
||||
.platform_window
|
||||
.prompt(level, message, detail, answers)
|
||||
let prompt_builder = self.app.prompt_builder.take();
|
||||
let Some(prompt_builder) = prompt_builder else {
|
||||
unreachable!("Re-entrant window prompting is not supported by GPUI");
|
||||
};
|
||||
|
||||
let receiver = match &prompt_builder {
|
||||
PromptBuilder::Default => self
|
||||
.window
|
||||
.platform_window
|
||||
.prompt(level, message, detail, answers)
|
||||
.unwrap_or_else(|| {
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, answers)
|
||||
}),
|
||||
PromptBuilder::Custom(_) => {
|
||||
self.build_custom_prompt(&prompt_builder, level, message, detail, answers)
|
||||
}
|
||||
};
|
||||
|
||||
self.app.prompt_builder = Some(prompt_builder);
|
||||
|
||||
receiver
|
||||
}
|
||||
|
||||
fn build_custom_prompt(
|
||||
&mut self,
|
||||
prompt_builder: &PromptBuilder,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let handle = PromptHandle::new(sender);
|
||||
let handle = (prompt_builder)(level, message, detail, answers, handle, self);
|
||||
self.window.prompt = Some(handle);
|
||||
receiver
|
||||
}
|
||||
|
||||
/// Returns all available actions for the focused element.
|
||||
|
|
229
crates/gpui/src/window/prompts.rs
Normal file
229
crates/gpui/src/window/prompts.rs
Normal file
|
@ -0,0 +1,229 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
|
||||
use crate::{
|
||||
div, opaque_grey, white, AnyElement, AnyView, ElementContext, EventEmitter, FocusHandle,
|
||||
FocusableView, InteractiveElement, IntoElement, ParentElement, PromptLevel, Render,
|
||||
StatefulInteractiveElement, Styled, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
/// The event emitted when a prompt's option is selected.
|
||||
/// The usize is the index of the selected option, from the actions
|
||||
/// passed to the prompt.
|
||||
pub struct PromptResponse(pub usize);
|
||||
|
||||
/// A prompt that can be rendered in the window.
|
||||
pub trait Prompt: EventEmitter<PromptResponse> + FocusableView {}
|
||||
|
||||
impl<V: EventEmitter<PromptResponse> + FocusableView> Prompt for V {}
|
||||
|
||||
/// A handle to a prompt that can be used to interact with it.
|
||||
pub struct PromptHandle {
|
||||
sender: oneshot::Sender<usize>,
|
||||
}
|
||||
|
||||
impl PromptHandle {
|
||||
pub(crate) fn new(sender: oneshot::Sender<usize>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
|
||||
/// Construct a new prompt handle from a view of the appropriate types
|
||||
pub fn with_view<V: Prompt>(
|
||||
self,
|
||||
view: View<V>,
|
||||
cx: &mut WindowContext,
|
||||
) -> RenderablePromptHandle {
|
||||
let mut sender = Some(self.sender);
|
||||
let previous_focus = cx.focused();
|
||||
cx.subscribe(&view, move |_, e: &PromptResponse, cx| {
|
||||
if let Some(sender) = sender.take() {
|
||||
sender.send(e.0).ok();
|
||||
cx.window.prompt.take();
|
||||
if let Some(previous_focus) = &previous_focus {
|
||||
cx.focus(&previous_focus);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.focus_view(&view);
|
||||
|
||||
RenderablePromptHandle {
|
||||
view: Box::new(view),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A prompt handle capable of being rendered in a window.
|
||||
pub struct RenderablePromptHandle {
|
||||
view: Box<dyn PromptViewHandle>,
|
||||
}
|
||||
|
||||
impl RenderablePromptHandle {
|
||||
pub(crate) fn paint(&mut self, _: &mut ElementContext) -> AnyElement {
|
||||
self.view.any_view().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this function in conjunction with [AppContext::set_prompt_renderer] to force
|
||||
/// GPUI to always use the fallback prompt renderer.
|
||||
pub fn fallback_prompt_renderer(
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
actions: &[&str],
|
||||
handle: PromptHandle,
|
||||
cx: &mut WindowContext,
|
||||
) -> RenderablePromptHandle {
|
||||
let renderer = cx.new_view({
|
||||
|cx| FallbackPromptRenderer {
|
||||
_level: level,
|
||||
message: message.to_string(),
|
||||
detail: detail.map(ToString::to_string),
|
||||
actions: actions.iter().map(ToString::to_string).collect(),
|
||||
focus: cx.focus_handle(),
|
||||
}
|
||||
});
|
||||
|
||||
handle.with_view(renderer, cx)
|
||||
}
|
||||
|
||||
/// The default GPUI fallback for rendering prompts, when the platform doesn't support it.
|
||||
pub struct FallbackPromptRenderer {
|
||||
_level: PromptLevel,
|
||||
message: String,
|
||||
detail: Option<String>,
|
||||
actions: Vec<String>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for FallbackPromptRenderer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let prompt = div()
|
||||
.cursor_default()
|
||||
.track_focus(&self.focus)
|
||||
.w_72()
|
||||
.bg(white())
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.p_3()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(div().overflow_hidden().child(self.message.clone())),
|
||||
)
|
||||
.children(self.detail.clone().map(|detail| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.text_sm()
|
||||
.mb_2()
|
||||
.child(div().child(detail))
|
||||
}))
|
||||
.children(self.actions.iter().enumerate().map(|(ix, action)| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.border_1()
|
||||
.border_color(opaque_grey(0.2, 0.5))
|
||||
.mt_1()
|
||||
.rounded_sm()
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.child(action.clone())
|
||||
.id(ix)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptResponse(ix));
|
||||
}))
|
||||
}));
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.z_index(u16::MAX)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.bg(opaque_grey(0.5, 0.6))
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(prompt),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PromptResponse> for FallbackPromptRenderer {}
|
||||
|
||||
impl FocusableView for FallbackPromptRenderer {
|
||||
fn focus_handle(&self, _: &crate::AppContext) -> FocusHandle {
|
||||
self.focus.clone()
|
||||
}
|
||||
}
|
||||
|
||||
trait PromptViewHandle {
|
||||
fn any_view(&self) -> AnyView;
|
||||
}
|
||||
|
||||
impl<V: Prompt> PromptViewHandle for View<V> {
|
||||
fn any_view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PromptBuilder {
|
||||
Default,
|
||||
Custom(
|
||||
Box<
|
||||
dyn Fn(
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
PromptHandle,
|
||||
&mut WindowContext,
|
||||
) -> RenderablePromptHandle,
|
||||
>,
|
||||
),
|
||||
}
|
||||
|
||||
impl Deref for PromptBuilder {
|
||||
type Target = dyn Fn(
|
||||
PromptLevel,
|
||||
&str,
|
||||
Option<&str>,
|
||||
&[&str],
|
||||
PromptHandle,
|
||||
&mut WindowContext,
|
||||
) -> RenderablePromptHandle;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Default => &fallback_prompt_renderer,
|
||||
Self::Custom(f) => f.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue