- overlays::Dialog caps its card at the window, a title bar's height clear at each end, and scrolls its body while heading and actions stay; the fullscreen body scrolls too; test a_tall_dialog_keeps_its_actions_in_the_window - ToolCallCard's tool name gives way with an Ellipsis, so time and chevron stay in a narrow column; the gallery shows one at 280px - the Agent script shuts the card it opened, so each theme's shot opens it - AGENTS: dialogs fit the window
392 lines
12 KiB
Rust
392 lines
12 KiB
Rust
use std::time::Duration;
|
|
|
|
use gpui::{
|
|
AppContext as _, Context, Entity, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
|
KeyUpEvent, Keystroke, Modifiers, ParentElement, Render, Styled, TestAppContext,
|
|
VisualTestContext, Window, div, point, px,
|
|
};
|
|
|
|
mod guides;
|
|
|
|
use super::{AlertDialog, ConfirmDialog, Dialog, HoverCard, Popover, PromptDialog};
|
|
use crate::{
|
|
buttons::Button,
|
|
forms::TextInput,
|
|
primitives::{FocusNext, FocusScope, Severity},
|
|
theme::{ActiveTheme, ControlSize, Theme},
|
|
};
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Open {
|
|
Plain,
|
|
Alert,
|
|
Confirm,
|
|
Prompt,
|
|
Tall,
|
|
}
|
|
|
|
/// A popover, a hover card and one dialog at a time, recording what they do.
|
|
struct Stage {
|
|
root: FocusHandle,
|
|
before: FocusHandle,
|
|
field: Entity<TextInput>,
|
|
open: Option<Open>,
|
|
log: Vec<String>,
|
|
}
|
|
|
|
impl Stage {
|
|
fn note(
|
|
&self,
|
|
cx: &mut Context<Self>,
|
|
what: &'static str,
|
|
) -> impl Fn(&mut Window, &mut gpui::App) + Clone + 'static {
|
|
let view = cx.entity();
|
|
move |_, cx| view.update(cx, |stage, _| stage.log.push(what.into()))
|
|
}
|
|
|
|
fn shut(&self, cx: &mut Context<Self>) -> impl Fn(&mut Window, &mut gpui::App) + 'static {
|
|
let view = cx.entity();
|
|
move |_, cx| {
|
|
view.update(cx, |stage, cx| {
|
|
stage.open = None;
|
|
cx.notify();
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for Stage {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (copy, mail, follow) = (
|
|
self.note(cx, "copy"),
|
|
self.note(cx, "mail"),
|
|
self.note(cx, "follow"),
|
|
);
|
|
let popover = Popover::new("share", "Share", move |_, _| {
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.child(div().h(px(60.0)))
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("copy", "Copy link")
|
|
.on_click(move |_, window, cx| copy(window, cx)),
|
|
)
|
|
.child(
|
|
Button::new("mail", "Email")
|
|
.on_click(move |_, window, cx| mail(window, cx)),
|
|
),
|
|
)
|
|
});
|
|
let beneath = self.note(cx, "beneath");
|
|
let card = HoverCard::new(
|
|
"ada",
|
|
div().id("ada-name").w(px(120.0)).h(px(24.0)).child("Ada"),
|
|
move |_, _| {
|
|
let follow = follow.clone();
|
|
Button::new("follow", "Follow").on_click(move |_, window, cx| follow(window, cx))
|
|
},
|
|
);
|
|
let (shut, confirmed, done) = (
|
|
self.shut(cx),
|
|
self.note(cx, "confirmed"),
|
|
self.note(cx, "done"),
|
|
);
|
|
let sent = cx.entity();
|
|
let overlay = self.open.map(|open| match open {
|
|
Open::Plain => Dialog::new("plain", "Plain", shut)
|
|
.detail("A line of detail")
|
|
.into_any_element(),
|
|
Open::Alert => AlertDialog::new("alert", Severity::Warning, "Alert", "Look", shut)
|
|
.into_any_element(),
|
|
Open::Confirm => ConfirmDialog::new("confirm", "Delete", "Gone for good", shut)
|
|
.confirm("Delete")
|
|
.destructive()
|
|
.on_confirm(move |window, cx| confirmed(window, cx))
|
|
.into_any_element(),
|
|
Open::Prompt => PromptDialog::new("prompt", "Rename", &self.field, shut)
|
|
.check(|text| match text {
|
|
"" => Err("Needs a name".into()),
|
|
text if text.contains('/') => Err("No slashes".into()),
|
|
_ => Ok(()),
|
|
})
|
|
.on_submit(move |text, _, cx| {
|
|
let text = format!("sent {text}");
|
|
sent.update(cx, |stage, _| stage.log.push(text));
|
|
})
|
|
.into_any_element(),
|
|
Open::Tall => Dialog::new("tall", "Tall", shut)
|
|
.child(div().h(px(2000.0)))
|
|
.action(move |_| {
|
|
Button::new("tall-done", "Done").on_click(move |_, window, cx| done(window, cx))
|
|
})
|
|
.into_any_element(),
|
|
});
|
|
FocusScope::new(&self.root)
|
|
.size_full()
|
|
.child(popover)
|
|
.child(div().h(px(52.0)))
|
|
.child(
|
|
Button::new("beneath", "Beneath")
|
|
.on_click(move |_, window, cx| beneath(window, cx)),
|
|
)
|
|
.child(div().h(px(80.0)))
|
|
.child(Button::new("elsewhere", "Elsewhere"))
|
|
.child(div().h(px(12.0)))
|
|
.child(card)
|
|
.child(div().id("before").track_focus(&self.before))
|
|
.children(overlay)
|
|
}
|
|
}
|
|
|
|
pub(super) fn settle(cx: &mut VisualTestContext) {
|
|
cx.run_until_parked();
|
|
cx.update(|window, _| window.refresh());
|
|
cx.run_until_parked();
|
|
}
|
|
|
|
pub(super) fn press(key: &str, cx: &mut VisualTestContext) {
|
|
cx.simulate_keystrokes(key);
|
|
settle(cx);
|
|
cx.simulate_event(KeyUpEvent {
|
|
keystroke: Keystroke::parse(key).expect("a key"),
|
|
});
|
|
settle(cx);
|
|
}
|
|
|
|
pub(super) fn click(x: f32, y: f32, cx: &mut VisualTestContext) {
|
|
let at = point(px(x), px(y));
|
|
cx.simulate_mouse_move(at, None, Modifiers::none());
|
|
cx.simulate_click(at, Modifiers::none());
|
|
settle(cx);
|
|
}
|
|
|
|
fn stage(cx: &mut TestAppContext) -> (Entity<Stage>, &mut VisualTestContext) {
|
|
cx.update(|cx| {
|
|
Theme::init(cx);
|
|
crate::forms::bind_keys(cx);
|
|
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
|
|
});
|
|
let (view, cx) = cx.add_window_view(|window, cx| Stage {
|
|
root: cx.focus_handle(),
|
|
before: cx.focus_handle(),
|
|
field: cx.new(|cx| TextInput::new(window, cx)),
|
|
open: None,
|
|
log: Vec::new(),
|
|
});
|
|
settle(cx);
|
|
(view, cx)
|
|
}
|
|
|
|
fn log(view: &Entity<Stage>, cx: &mut VisualTestContext) -> Vec<String> {
|
|
view.read_with(cx, |stage, _| stage.log.clone())
|
|
}
|
|
|
|
fn show(view: &Entity<Stage>, open: Open, cx: &mut VisualTestContext) {
|
|
let before = view.read_with(cx, |stage, _| stage.before.clone());
|
|
cx.update(|window, _| window.focus(&before));
|
|
view.update(cx, |stage, cx| {
|
|
stage.open = Some(open);
|
|
cx.notify();
|
|
});
|
|
settle(cx);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn tab_walks_inside_an_open_popover(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
cx.update(|window, _| window.focus_next());
|
|
press("enter", cx);
|
|
for _ in 0..3 {
|
|
press("tab", cx);
|
|
}
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["copy"]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn escape_closes_the_popover_and_hands_focus_back(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
cx.update(|window, _| window.focus_next());
|
|
press("enter", cx);
|
|
press("escape", cx);
|
|
press("enter", cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["copy"], "focus came back to the trigger");
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_press_on_a_button_outside_closes_the_popover(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
click(12.0, 12.0, cx);
|
|
click(20.0, 200.0, cx);
|
|
click(40.0, 127.0, cx);
|
|
assert!(log(&view, cx).is_empty());
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_card_opens_after_a_rest_and_holds_while_on_it(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
cx.simulate_mouse_move(point(px(20.0), px(240.0)), None, Modifiers::none());
|
|
settle(cx);
|
|
cx.executor().advance_clock(Duration::from_millis(600));
|
|
settle(cx);
|
|
click(40.0, 290.0, cx);
|
|
assert_eq!(log(&view, cx), ["follow"]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn escape_closes_a_dialog_and_hands_focus_back(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Plain, cx);
|
|
press("escape", cx);
|
|
assert!(view.read_with(cx, |stage, _| stage.open.is_none()));
|
|
let before = view.read_with(cx, |stage, _| stage.before.clone());
|
|
assert!(cx.update(|window, _| before.is_focused(window)));
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_scrim_closes_a_dialog_but_not_an_alert(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Alert, cx);
|
|
click(8.0, 8.0, cx);
|
|
assert!(view.read_with(cx, |stage, _| stage.open == Some(Open::Alert)));
|
|
view.update(cx, |stage, cx| {
|
|
stage.open = None;
|
|
cx.notify();
|
|
});
|
|
settle(cx);
|
|
show(&view, Open::Plain, cx);
|
|
click(8.0, 8.0, cx);
|
|
assert!(view.read_with(cx, |stage, _| stage.open.is_none()));
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn confirm_runs_from_the_keyboard(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Confirm, cx);
|
|
press("tab", cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["confirmed"]);
|
|
assert!(view.read_with(cx, |stage, _| stage.open.is_none()));
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_prompt_sends_only_what_passes(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Prompt, cx);
|
|
cx.simulate_input("a/b");
|
|
settle(cx);
|
|
press("enter", cx);
|
|
assert!(log(&view, cx).is_empty(), "a slash went through");
|
|
press("secondary-a", cx);
|
|
cx.simulate_input("notes");
|
|
settle(cx);
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["sent notes"]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_prompt_opens_with_its_text_selected(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
let field = view.read_with(cx, |stage, _| stage.field.clone());
|
|
field.update(cx, |input, cx| input.set_text("Draft", cx));
|
|
show(&view, Open::Prompt, cx);
|
|
cx.simulate_input("notes");
|
|
settle(cx);
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["sent notes"]);
|
|
}
|
|
|
|
fn before_focused(view: &Entity<Stage>, cx: &mut VisualTestContext) -> bool {
|
|
let before = view.read_with(cx, |stage, _| stage.before.clone());
|
|
cx.update(|window, _| before.is_focused(window))
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn every_dialog_button_hands_focus_back(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Alert, cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert!(before_focused(&view, cx), "alert button");
|
|
show(&view, Open::Confirm, cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert!(before_focused(&view, cx), "confirm cancel");
|
|
show(&view, Open::Confirm, cx);
|
|
press("tab", cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert!(before_focused(&view, cx), "confirm confirm");
|
|
show(&view, Open::Prompt, cx);
|
|
cx.simulate_input("notes");
|
|
settle(cx);
|
|
press("enter", cx);
|
|
assert!(before_focused(&view, cx), "prompt submit");
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_press_on_the_panel_stays_off_what_lies_beneath(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
click(12.0, 12.0, cx);
|
|
click(30.0, 94.0, cx);
|
|
assert!(
|
|
log(&view, cx).is_empty(),
|
|
"the press went through the panel"
|
|
);
|
|
click(40.0, 127.0, cx);
|
|
assert_eq!(log(&view, cx), ["copy"], "the panel stayed open");
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn enter_on_a_prompt_button_runs_that_button_alone(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
show(&view, Open::Prompt, cx);
|
|
cx.simulate_input("notes");
|
|
settle(cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert!(log(&view, cx).is_empty(), "cancel submitted");
|
|
show(&view, Open::Prompt, cx);
|
|
cx.simulate_input("notes");
|
|
settle(cx);
|
|
press("tab", cx);
|
|
press("tab", cx);
|
|
press("enter", cx);
|
|
assert_eq!(log(&view, cx), ["sent notes"]);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn a_tall_dialog_keeps_its_actions_in_the_window(cx: &mut TestAppContext) {
|
|
let (view, cx) = stage(cx);
|
|
cx.update(|_, cx| Theme::update(cx, |theme| theme.reduced_motion = true));
|
|
show(&view, Open::Tall, cx);
|
|
std::thread::sleep(Duration::from_millis(2));
|
|
settle(cx);
|
|
let (size, margin, width, button) = cx.update(|window, cx| {
|
|
let (theme, rem) = (cx.theme(), window.rem_size());
|
|
(
|
|
window.viewport_size(),
|
|
theme.titlebar_height().to_pixels(rem),
|
|
theme.dialog_width().to_pixels(rem),
|
|
theme.control_height(ControlSize::Md).to_pixels(rem),
|
|
)
|
|
});
|
|
let padding = px(24.0);
|
|
let x = size.width / 2.0 + width / 2.0 - padding - px(12.0);
|
|
let y = size.height - margin - padding - button / 2.0;
|
|
click(f32::from(x), f32::from(y), cx);
|
|
assert_eq!(
|
|
log(&view, cx),
|
|
["done"],
|
|
"the button sits at the card's foot, inside the window"
|
|
);
|
|
}
|