431 lines
15 KiB
Rust
431 lines
15 KiB
Rust
use std::any::TypeId;
|
|
use std::rc::Rc;
|
|
|
|
use gpui::prelude::FluentBuilder;
|
|
use gpui::{
|
|
AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, ElementId, Entity,
|
|
FocusHandle, HitboxBehavior, InteractiveElement, IntoElement, MouseButton, ParentElement as _,
|
|
Pixels, Point, Render, ResizeEdge, Size, Styled, Tiling, WeakFocusHandle, Window, canvas, div,
|
|
point, px, size,
|
|
};
|
|
use gpui_base::TextSelectionLayer;
|
|
use theme::{
|
|
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
|
|
CLIENT_SIDE_DECORATION_SHADOW, base_text_size, surface_shadows,
|
|
};
|
|
|
|
use crate::dialog::Dialog;
|
|
use crate::notification::{Notification, NotificationList};
|
|
|
|
#[derive(Clone)]
|
|
#[allow(clippy::type_complexity)]
|
|
pub(crate) struct ActiveDialog {
|
|
focus_handle: FocusHandle,
|
|
/// The previous focused handle before opening the dialog.
|
|
previous_focused_handle: Option<WeakFocusHandle>,
|
|
builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
|
|
}
|
|
|
|
impl ActiveDialog {
|
|
fn new(
|
|
focus_handle: FocusHandle,
|
|
previous_focused_handle: Option<WeakFocusHandle>,
|
|
builder: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
|
|
) -> Self {
|
|
Self {
|
|
focus_handle,
|
|
previous_focused_handle,
|
|
builder: Rc::new(builder),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Root is a view for the App window for as the top level view (Must be the first view in the window).
|
|
///
|
|
/// It is used to manage the Dialog, and Notification.
|
|
pub struct Root {
|
|
/// All active dialogs
|
|
pub(crate) active_dialogs: Vec<ActiveDialog>,
|
|
|
|
/// Notification layer
|
|
pub(crate) notification: Entity<NotificationList>,
|
|
|
|
/// App view
|
|
view: AnyView,
|
|
}
|
|
|
|
impl Root {
|
|
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
Self {
|
|
active_dialogs: Vec::new(),
|
|
notification: cx.new(|cx| NotificationList::new(window, cx)),
|
|
view,
|
|
}
|
|
}
|
|
|
|
pub fn update<F>(window: &mut Window, cx: &mut App, f: F)
|
|
where
|
|
F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) + 'static,
|
|
{
|
|
if let Some(Some(root)) = window.root::<Root>() {
|
|
root.update(cx, |root, cx| f(root, window, cx));
|
|
}
|
|
}
|
|
|
|
pub fn read<'a>(window: &'a mut Window, cx: &'a mut App) -> &'a Self {
|
|
window
|
|
.root::<Root>()
|
|
.expect("The window root view should be of type `ui::Root`.")
|
|
.unwrap()
|
|
.read(cx)
|
|
}
|
|
|
|
pub fn view(&self) -> &AnyView {
|
|
&self.view
|
|
}
|
|
|
|
/// Render the notification layer.
|
|
pub fn render_notification_layer(
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<impl IntoElement + use<>> {
|
|
let root = window.root::<Root>()??;
|
|
|
|
Some(
|
|
div()
|
|
.absolute()
|
|
.inset_0()
|
|
.child(root.read(cx).notification.clone()),
|
|
)
|
|
}
|
|
|
|
/// Render the dialog layer.
|
|
pub fn render_dialog_layer(
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Option<impl IntoElement + use<>> {
|
|
let root = window.root::<Root>()??;
|
|
let active_dialogs = root.read(cx).active_dialogs.clone();
|
|
|
|
if active_dialogs.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut show_overlay_ix = None;
|
|
|
|
let mut dialogs = active_dialogs
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, active_dialog)| {
|
|
let mut dialog = Dialog::new(window, cx);
|
|
|
|
dialog = (active_dialog.builder)(dialog, window, cx);
|
|
|
|
// Give the dialog the focus handle, because `dialog` is a temporary value, is not possible to
|
|
// keep the focus handle in the dialog.
|
|
//
|
|
// So we keep the focus handle in the `active_dialog`, this is owned by the `Root`.
|
|
dialog.focus_handle = active_dialog.focus_handle.clone();
|
|
|
|
dialog.layer_ix = i;
|
|
// Find the dialog which one needs to show overlay.
|
|
if dialog.has_overlay() {
|
|
show_overlay_ix = Some(i);
|
|
}
|
|
|
|
dialog
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if let Some(ix) = show_overlay_ix
|
|
&& let Some(dialog) = dialogs.get_mut(ix)
|
|
{
|
|
dialog.overlay_visible = true;
|
|
}
|
|
|
|
Some(div().children(dialogs))
|
|
}
|
|
|
|
/// Open a dialog.
|
|
pub fn open_dialog<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
|
|
where
|
|
F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
|
|
{
|
|
let previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
|
|
let focus_handle = cx.focus_handle();
|
|
focus_handle.focus(window, cx);
|
|
|
|
self.active_dialogs.push(ActiveDialog::new(
|
|
focus_handle,
|
|
previous_focused_handle,
|
|
builder,
|
|
));
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
/// Close the topmost dialog.
|
|
pub fn close_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(handle) = self
|
|
.active_dialogs
|
|
.pop()
|
|
.and_then(|d| d.previous_focused_handle)
|
|
.and_then(|h| h.upgrade())
|
|
{
|
|
window.focus(&handle, cx);
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
/// Close all dialogs.
|
|
pub fn close_all_dialogs(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.active_dialogs.clear();
|
|
|
|
let previous_focused_handle = self
|
|
.active_dialogs
|
|
.first()
|
|
.and_then(|d| d.previous_focused_handle.clone());
|
|
|
|
if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
|
|
window.focus(&handle, cx);
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
/// Push a notification to the notification layer.
|
|
pub fn push_notification<T>(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>)
|
|
where
|
|
T: Into<Notification>,
|
|
{
|
|
self.notification
|
|
.update(cx, |view, cx| view.push(note, window, cx));
|
|
cx.notify();
|
|
}
|
|
|
|
/// Clear a notification by its type.
|
|
pub fn clear_notification<T: Sized + 'static>(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<'_, Root>,
|
|
) {
|
|
self.notification.update(cx, |view, cx| {
|
|
let id = TypeId::of::<T>();
|
|
view.close(id, window, cx);
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
/// Clear a notification by its type.
|
|
pub fn clear_notification_by_id<T: Sized + 'static>(
|
|
&mut self,
|
|
key: impl Into<ElementId>,
|
|
window: &mut Window,
|
|
cx: &mut Context<'_, Root>,
|
|
) {
|
|
self.notification.update(cx, |view, cx| {
|
|
let id = (TypeId::of::<T>(), key.into());
|
|
view.close(id, window, cx);
|
|
});
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
impl Render for Root {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let rem_size = px(base_text_size());
|
|
let font_family = cx.theme().font_sans.clone();
|
|
let decorations = window.window_decorations();
|
|
|
|
// Set the base font size
|
|
window.set_rem_size(rem_size);
|
|
|
|
// Set the client inset (linux only)
|
|
match decorations {
|
|
Decorations::Client { .. } => window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW),
|
|
Decorations::Server => window.set_client_inset(px(0.0)),
|
|
}
|
|
|
|
div()
|
|
.id("window")
|
|
.size_full()
|
|
.map(|div| match decorations {
|
|
Decorations::Server => div,
|
|
Decorations::Client { tiling } => div
|
|
.bg(gpui::transparent_black())
|
|
.child(
|
|
canvas(
|
|
|_bounds, window, _cx| {
|
|
window.insert_hitbox(
|
|
Bounds::new(
|
|
point(px(0.0), px(0.0)),
|
|
window.window_bounds().get_bounds().size,
|
|
),
|
|
HitboxBehavior::Normal,
|
|
)
|
|
},
|
|
move |_bounds, hitbox, window, _cx| {
|
|
let mouse = window.mouse_position();
|
|
let size = window.window_bounds().get_bounds().size;
|
|
|
|
let Some(edge) =
|
|
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
window.set_cursor_style(
|
|
match edge {
|
|
ResizeEdge::Top | ResizeEdge::Bottom => {
|
|
CursorStyle::ResizeUpDown
|
|
}
|
|
ResizeEdge::Left | ResizeEdge::Right => {
|
|
CursorStyle::ResizeLeftRight
|
|
}
|
|
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
|
CursorStyle::ResizeUpLeftDownRight
|
|
}
|
|
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
|
CursorStyle::ResizeUpRightDownLeft
|
|
}
|
|
},
|
|
&hitbox,
|
|
);
|
|
},
|
|
)
|
|
.size_full()
|
|
.absolute(),
|
|
)
|
|
.when(!(tiling.top || tiling.right), |div| {
|
|
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.top || tiling.left), |div| {
|
|
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.bottom || tiling.right), |div| {
|
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.bottom || tiling.left), |div| {
|
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
|
|
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
|
|
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
|
|
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
|
|
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
|
|
let size = window.window_bounds().get_bounds().size;
|
|
let pos = e.position;
|
|
|
|
if let Some(edge) =
|
|
resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
|
{
|
|
window.start_window_resize(edge)
|
|
};
|
|
}),
|
|
})
|
|
.child(
|
|
div()
|
|
.map(|div| match decorations {
|
|
Decorations::Server => div,
|
|
Decorations::Client { tiling } => div
|
|
.border_color(cx.theme().border)
|
|
.when(!(tiling.top || tiling.right), |div| {
|
|
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.top || tiling.left), |div| {
|
|
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.bottom || tiling.right), |div| {
|
|
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!(tiling.bottom || tiling.left), |div| {
|
|
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
})
|
|
.when(!tiling.top, |div| {
|
|
div.border_t(CLIENT_SIDE_DECORATION_BORDER)
|
|
})
|
|
.when(!tiling.bottom, |div| {
|
|
div.border_b(CLIENT_SIDE_DECORATION_BORDER)
|
|
})
|
|
.when(!tiling.left, |div| {
|
|
div.border_l(CLIENT_SIDE_DECORATION_BORDER)
|
|
})
|
|
.when(!tiling.right, |div| {
|
|
div.border_r(CLIENT_SIDE_DECORATION_BORDER)
|
|
})
|
|
.when(!tiling.is_tiled() && cx.theme().shadow, |div| {
|
|
div.shadow(surface_shadows())
|
|
}),
|
|
})
|
|
.on_mouse_move(|_e, _, cx| {
|
|
cx.stop_propagation();
|
|
})
|
|
.size_full()
|
|
.font_family(font_family)
|
|
.bg(cx.theme().bg)
|
|
.text_color(cx.theme().text)
|
|
.child(TextSelectionLayer)
|
|
.child(self.view.clone()),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get the window resize edge.
|
|
fn resize_edge(
|
|
pos: Point<Pixels>,
|
|
shadow_size: Pixels,
|
|
window_size: Size<Pixels>,
|
|
tiling: Tiling,
|
|
) -> Option<ResizeEdge> {
|
|
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
|
|
if bounds.contains(&pos) {
|
|
return None;
|
|
}
|
|
|
|
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
|
|
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
|
|
if !tiling.top && top_left_bounds.contains(&pos) {
|
|
return Some(ResizeEdge::TopLeft);
|
|
}
|
|
|
|
let top_right_bounds = Bounds::new(
|
|
Point::new(window_size.width - corner_size.width, px(0.)),
|
|
corner_size,
|
|
);
|
|
if !tiling.top && top_right_bounds.contains(&pos) {
|
|
return Some(ResizeEdge::TopRight);
|
|
}
|
|
|
|
let bottom_left_bounds = Bounds::new(
|
|
Point::new(px(0.), window_size.height - corner_size.height),
|
|
corner_size,
|
|
);
|
|
if !tiling.bottom && bottom_left_bounds.contains(&pos) {
|
|
return Some(ResizeEdge::BottomLeft);
|
|
}
|
|
|
|
let bottom_right_bounds = Bounds::new(
|
|
Point::new(
|
|
window_size.width - corner_size.width,
|
|
window_size.height - corner_size.height,
|
|
),
|
|
corner_size,
|
|
);
|
|
if !tiling.bottom && bottom_right_bounds.contains(&pos) {
|
|
return Some(ResizeEdge::BottomRight);
|
|
}
|
|
|
|
if !tiling.top && pos.y < shadow_size {
|
|
Some(ResizeEdge::Top)
|
|
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
|
|
Some(ResizeEdge::Bottom)
|
|
} else if !tiling.left && pos.x < shadow_size {
|
|
Some(ResizeEdge::Left)
|
|
} else if !tiling.right && pos.x > window_size.width - shadow_size {
|
|
Some(ResizeEdge::Right)
|
|
} else {
|
|
None
|
|
}
|
|
}
|