use crate::dialog::{ANIMATION_DURATION, CancelDialog, ConfirmDialog, Dialog};
use crate::elements::TooltipOverlay;
use crate::{
ActiveTheme, AnyView, App, AppContext, Context, Entity, FocusHandle, FocusTrapManager,
InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, StyleRefinement,
Styled, StyledExt as _, WeakFocusHandle, Window, WindowBackgroundAppearance, div,
};
use std::rc::Rc;
actions!(root, [Tab, TabPrev]);
const CONTEXT: &str = "Root";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DialogId(u64);
pub struct Root {
style: StyleRefinement,
view: AnyView,
pub(crate) active_dialogs: Vec<ActiveDialog>,
next_dialog_id: u64,
pending_focus_restore: Option<WeakFocusHandle>,
pub(crate) tooltip_overlay: Entity<TooltipOverlay>,
}
#[derive(Clone)]
pub(crate) struct ActiveDialog {
pub(crate) id: DialogId,
focus_handle: FocusHandle,
previous_focused_handle: Option<WeakFocusHandle>,
builder: Rc<dyn Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static>,
}
impl ActiveDialog {
pub(crate) fn new(
id: DialogId,
focus_handle: FocusHandle,
previous_focused_handle: Option<WeakFocusHandle>,
builder: impl Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
) -> Self {
Self {
id,
focus_handle,
previous_focused_handle,
builder: Rc::new(builder),
}
}
}
impl Root {
pub fn new(view: impl Into<AnyView>, cx: &mut Context<Self>) -> Self {
cx.key_bindings().borrow_mut().add_bindings([
KeyBinding::new("tab", Tab, Some(CONTEXT)),
KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)),
KeyBinding::new("escape", CancelDialog, Some("Dialog")),
KeyBinding::new("enter", ConfirmDialog, Some("Dialog")),
]);
Self {
style: StyleRefinement::default(),
view: view.into(),
active_dialogs: Vec::new(),
next_dialog_id: 0,
pending_focus_restore: None,
tooltip_overlay: cx.new(|_| TooltipOverlay::new()),
}
}
pub fn update<F, R>(window: &mut Window, cx: &mut App, f: F) -> R
where
F: FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
{
let root = window
.root::<Root>()
.flatten()
.expect("BUG: window first layer should be a rgpui::Root.");
root.update(cx, |root, cx| f(root, window, cx))
}
pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self {
&window
.root::<Root>()
.expect("The window root view should be of type `rgpui::Root`.")
.unwrap()
.read(cx)
}
pub(crate) fn tooltip_overlay(window: &Window, cx: &App) -> Option<Entity<TooltipOverlay>> {
let root = window.root::<Root>()??;
Some(root.read(cx).tooltip_overlay.clone())
}
pub fn view(&self) -> &AnyView {
&self.view
}
pub(crate) fn root_view_downcast<V: 'static>(
root_view: AnyView,
cx: &App,
) -> Result<Entity<V>, AnyView> {
if let Ok(view) = root_view.clone().downcast::<V>() {
return Ok(view);
}
if let Ok(root) = root_view.clone().downcast::<Root>() {
if let Ok(view) = root.read(cx).view().clone().downcast::<V>() {
return Ok(view);
}
}
Err(root_view)
}
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(cx);
dialog = (active_dialog.builder)(dialog, window, cx);
dialog.focus_handle = active_dialog.focus_handle.clone();
dialog.layer_ix = i;
if dialog.has_overlay() {
show_overlay_ix = Some(i);
}
dialog
})
.collect::<Vec<_>>();
if let Some(ix) = show_overlay_ix {
if let Some(dialog) = dialogs.get_mut(ix) {
dialog.props.overlay_visible = true;
}
}
Some(div().children(dialogs))
}
pub fn open_dialog<F>(
&mut self,
build: F,
window: &mut Window,
cx: &mut Context<'_, Root>,
) -> DialogId
where
F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static,
{
let mut previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
if let Some(pending_handle) = self.pending_focus_restore.take() {
previous_focused_handle = Some(pending_handle);
}
let focus_handle = cx.focus_handle();
focus_handle.focus(window, cx);
let id = DialogId(self.next_dialog_id);
self.next_dialog_id += 1;
self.active_dialogs.push(ActiveDialog::new(
id,
focus_handle,
previous_focused_handle,
build,
));
cx.notify();
id
}
pub fn close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
if let Some(handle) = self.close_dialog_internal() {
window.focus(&handle, cx);
}
cx.notify();
}
pub fn close_dialog_by(
&mut self,
id: DialogId,
window: &mut Window,
cx: &mut Context<'_, Root>,
) -> bool {
let Some(ix) = self.active_dialogs.iter().position(|d| d.id == id) else {
return false;
};
let is_top = ix + 1 == self.active_dialogs.len();
let dialog = self.active_dialogs.remove(ix);
if is_top && let Some(previous) = dialog.previous_focused_handle.and_then(|h| h.upgrade()) {
window.focus(&previous, cx);
}
cx.notify();
true
}
pub(crate) fn defer_close_dialog(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
if let Some(handle) = self.close_dialog_internal() {
let dialogs_count = self.active_dialogs.len();
self.pending_focus_restore = Some(handle.downgrade());
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(*ANIMATION_DURATION).await;
let _ = this.update_in(cx, |this, window, cx| {
let current_dialogs_count = this.active_dialogs.len();
if current_dialogs_count == dialogs_count {
window.focus(&handle, cx);
}
this.pending_focus_restore = None;
});
})
.detach();
}
cx.notify();
}
pub fn close_all_dialogs(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
let previous_focused_handle = self
.active_dialogs
.first()
.and_then(|d| d.previous_focused_handle.clone());
self.active_dialogs.clear();
if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
window.focus(&handle, cx);
}
cx.notify();
}
fn close_dialog_internal(&mut self) -> Option<FocusHandle> {
self.active_dialogs
.pop()
.and_then(|d| d.previous_focused_handle)
.and_then(|h| h.upgrade())
}
fn on_action_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
if let Some(container_focus_handle) = FocusTrapManager::find_active_trap(window, cx) {
let before_focus = window.focused(cx);
window.focus_next(cx);
if !container_focus_handle.contains_focused(window, cx) {
let mut attempts = 0;
const MAX_ATTEMPTS: usize = 100;
while !container_focus_handle.contains_focused(window, cx)
&& attempts < MAX_ATTEMPTS
{
window.focus_next(cx);
attempts += 1;
if window.focused(cx) == before_focus {
break;
}
}
}
return;
}
window.focus_next(cx);
}
fn on_action_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
if let Some(container_focus_handle) = FocusTrapManager::find_active_trap(window, cx) {
let before_focus = window.focused(cx);
window.focus_prev(cx);
if !container_focus_handle.contains_focused(window, cx) {
let mut attempts = 0;
const MAX_ATTEMPTS: usize = 100;
while !container_focus_handle.contains_focused(window, cx)
&& attempts < MAX_ATTEMPTS
{
window.focus_prev(cx);
attempts += 1;
if window.focused(cx) == before_focus {
break;
}
}
}
return;
}
window.focus_prev(cx);
}
}
impl Styled for Root {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Render for Root {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
window.set_rem_size(cx.theme().font_size);
let is_transparent = window.background_appearance() != WindowBackgroundAppearance::Opaque;
let bg_color = cx.theme().tokens.background;
let root = div()
.id("root")
.key_context(CONTEXT)
.on_action(cx.listener(Self::on_action_tab))
.on_action(cx.listener(Self::on_action_tab_prev))
.relative()
.size_full()
.grid()
.grid_cols(1)
.grid_rows(1)
.font_family(cx.theme().font_family.clone())
.text_color(cx.theme().foreground)
.refine_style(&self.style);
let root = if is_transparent {
root
} else {
root.bg(bg_color)
};
root.child(self.view.clone())
.child(self.tooltip_overlay.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestView;
impl Render for TestView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
}
}
#[test]
fn test_root_creation() {
let _ = std::any::type_name::<Root>;
let _ = std::any::type_name::<TestView>;
}
}