Detailed changes
@@ -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.
@@ -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(),
+ }
+ }
+}