use ely_gpui_component::{ buttons::{Button, ButtonVariant}, forms::{Input, TextInput}, overlays::{AlertDialog, ConfirmDialog, Dialog, PromptDialog}, primitives::Severity, theme::ActiveTheme, typography::Caption, }; use gpui::{ AnyElement, App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, prelude::*, }; use crate::{ probe::probe, ui::{keep, row, section, set, specimen}, }; /// Which dialog is open, if any. #[derive(Clone, Copy, PartialEq)] enum Open { Profile, Alert, Confirm, Prompt, Fullscreen, } fn opener( state: &Entity>, id: &'static str, label: &'static str, open: Open, ) -> impl IntoElement { let state = state.clone(); probe( id, Button::new(id, label).on_click(move |_, _, cx| set(&state, Some(open), cx)), ) } pub fn dialogs(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let open = keep("overlay-open", || None::, window, cx); let said = keep("overlay-said", || None::, window, cx); let (name, email, rename) = { let make = |window: &mut Window, cx: &mut App, text: &str| { let text = text.to_string(); cx.new(|cx| { let mut input = TextInput::new(window, cx); input.set_text(text, cx); input }) }; let fields = window.use_keyed_state("overlay-fields", cx, |window, cx| { ( make(window, cx, "Ada Lovelace"), make(window, cx, "ada@ely.dev"), make(window, cx, "Roadmap"), ) }); fields.read(cx).clone() }; let shut = { let open = open.clone(); move |_: &mut Window, cx: &mut App| set(&open, None, cx) }; let caption = said .read(cx) .clone() .map_or("each opens over a scrim; Escape closes it".into(), |said| { said.to_string() }); let overlay: Option = (*open.read(cx)).map(|which| { let (saved, confirmed, renamed) = (said.clone(), said.clone(), said.clone()); match which { Open::Profile => Dialog::new("profile", "Edit profile", shut.clone()) .detail("How others see you across the workspace.") .child( div() .flex() .flex_col() .gap_3() .child(Input::new(&name)) .child(Input::new(&email)), ) .action(|close| { Button::new("profile-cancel", "Cancel") .variant(ButtonVariant::Ghost) .on_click(move |_, window, cx| close(window, cx)) }) .action(move |close| { Button::new("profile-save", "Save") .primary() .on_click(move |_, window, cx| { set(&saved, Some("profile saved".into()), cx); close(window, cx); }) }) .into_any_element(), Open::Alert => AlertDialog::new( "expired", Severity::Warning, "Session expired", "Sign in again to keep your changes. Nothing you wrote is lost.", shut.clone(), ) .label("Sign in") .into_any_element(), Open::Confirm => ConfirmDialog::new( "delete", "Delete this project?", "Its pages, files and history go with it. This cannot be undone.", shut.clone(), ) .confirm("Delete project") .destructive() .on_confirm(move |_, cx| set(&confirmed, Some("project deleted".into()), cx)) .into_any_element(), Open::Prompt => PromptDialog::new("rename", "Rename", &rename, shut.clone()) .label("New name") .submit("Rename") .check(|text| match text.trim() { "" => Err("A name cannot be empty.".into()), text if text.contains('/') => Err("Names cannot hold a slash.".into()), _ => Ok(()), }) .on_submit(move |text, _, cx| { set(&renamed, Some(format!("renamed to {text}").into()), cx) }) .into_any_element(), Open::Fullscreen => Dialog::new("notes", "Release notes", shut.clone()) .detail("Version 0.9 ยท September 2026") .fullscreen() .child( div() .flex() .flex_col() .gap_3() .max_w( cx.theme() .container_width(ely_gpui_component::theme::ContainerSize::Sm), ) .child(Caption::new("Menus nest, filter and follow their buttons.")) .child(Caption::new("Palettes pick on release, as buttons do.")) .child(Caption::new("Overlays hand focus back on every close.")), ) .into_any_element(), } }); section( "Dialog / Modal / AlertDialog / ConfirmDialog / PromptDialog / FullscreenDialog", "A card over a scrim; Tab stays inside and focus returns on close. An alert holds until acknowledged, a confirm asks first, a prompt checks its one line, and a fullscreen dialog fills the window.", cx, ) .child(specimen( caption, row() .child(opener(&open, "edit-profile", "Edit profile", Open::Profile)) .child(opener(&open, "alert", "Session expired", Open::Alert)) .child(opener(&open, "confirm", "Delete project", Open::Confirm)) .child({ let (open, rename) = (open.clone(), rename.clone()); probe( "prompt", Button::new("prompt", "Rename").on_click(move |_, _, cx| { rename.update(cx, |input, cx| input.set_text("Roadmap", cx)); set(&open, Some(Open::Prompt), cx); }), ) }) .child(opener(&open, "fullscreen", "Release notes", Open::Fullscreen)), cx, )) .children(overlay) } pub fn pointers(cx: &App) -> impl IntoElement + use<> { section( "Tooltip / Sheet / Drawer / DropdownPanel / InlinePopup / ToastViewport", "They live with the pieces they are made of.", cx, ) .child(Caption::new( "Tooltip is primitives::Tooltip, on Primitives, chapter 1. Sheet and Drawer are layout::Sheet and layout::Drawer, on Layout, chapter 3. DropdownPanel and InlinePopup are the Popover above. ToastViewport holds toasts, so it lands with Feedback, chapter 10.", )) }