diff --git a/examples/gallery/pages/misc/mod.rs b/examples/gallery/pages/misc/mod.rs index 8eb6b4b..d68a1b5 100644 --- a/examples/gallery/pages/misc/mod.rs +++ b/examples/gallery/pages/misc/mod.rs @@ -63,6 +63,7 @@ pub const PAGE: Page = Page { Step::Key("escape"), Step::DownAt("cookies", 230.0, 84.0), Step::UpAt("cookies", 230.0, 84.0), + Step::HoverAt("cookies", 20.0, 20.0), Step::Shot("cookies-choosing"), ], }; diff --git a/examples/gallery/pages/misc/notices.rs b/examples/gallery/pages/misc/notices.rs index 07ca54b..1d0ba34 100644 --- a/examples/gallery/pages/misc/notices.rs +++ b/examples/gallery/pages/misc/notices.rs @@ -17,7 +17,11 @@ fn packages() -> [Package; 4] { "Ely GPUI Component", "0.1.0", "MIT OR Apache-2.0", - include_str!("../../../../LICENSE-MIT"), + concat!( + include_str!("../../../../LICENSE-MIT"), + "\n\n", + include_str!("../../../../LICENSE-APACHE") + ), ), Package::new( "Lucide", diff --git a/src/forms/switch.rs b/src/forms/switch.rs index db5700c..5da6e96 100644 --- a/src/forms/switch.rs +++ b/src/forms/switch.rs @@ -140,6 +140,12 @@ impl RenderOnce for Switch { }) }) .child(rail) - .children(self.label) + .children(self.label.map(|label| { + div() + .debug_selector(|| "switch-label".into()) + .flex_1() + .min_w_0() + .child(label) + })) } } diff --git a/src/forms/tests/other.rs b/src/forms/tests/other.rs index 87d970c..aaf784e 100644 --- a/src/forms/tests/other.rs +++ b/src/forms/tests/other.rs @@ -5,7 +5,7 @@ use gpui::{ use super::setup; use crate::{ - forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke}, + forms::{Checkbox, Choice, IconPicker, InlineEdit, RadioGroup, SignaturePad, Stroke, Switch}, primitives::{FocusNext, IconName}, }; @@ -145,7 +145,7 @@ fn an_inline_edit_takes_tab_opens_on_enter_and_takes_focus_back(cx: &mut TestApp ); } -/// A checkbox and a radio whose labels run past their narrow box. +/// A checkbox, a radio and a switch whose labels run past their narrow box. struct Worded; impl Render for Worded { @@ -158,6 +158,7 @@ impl Render for Worded { "worded-radios", [Choice::new("one", label)], )) + .child(Switch::new("worded-switch", false).label(label)) } } @@ -166,7 +167,7 @@ fn long_labels_wrap_inside_their_box(cx: &mut TestAppContext) { setup(cx); let (_, cx) = cx.add_window_view(|_, _| Worded); cx.run_until_parked(); - for selector in ["checkbox-label", "radio-label"] { + for selector in ["checkbox-label", "radio-label", "switch-label"] { let label = cx.debug_bounds(selector).expect("the label draws"); assert!(label.right() <= px(200.0), "{selector}: {label:?}"); } diff --git a/src/lists/item.rs b/src/lists/item.rs index addd984..7e50f49 100644 --- a/src/lists/item.rs +++ b/src/lists/item.rs @@ -153,7 +153,10 @@ impl RenderOnce for ListItem { } else { gpui::transparent_black() }) - .when(self.selected, |row| row.bg(colors.active)) + .when(self.selected, |row| { + row.debug_selector(|| format!("item-selected-{}", self.title)) + .bg(colors.active) + }) .when(self.disabled, |row| row.opacity(0.5)) .when_some(self.on_click.filter(|_| pressable), |row, on_click| { row.cursor_pointer() @@ -182,7 +185,8 @@ impl RenderOnce for ListItem { }), ) .children(self.description.map(|text| { - line(text) + line(text.clone()) + .debug_selector(|| format!("item-description-{text}")) .text_size(theme.text_size(TextSize::Sm)) .text_color(colors.fg_muted) })) diff --git a/src/misc/cookies.rs b/src/misc/cookies.rs index 8639852..e6ac249 100644 --- a/src/misc/cookies.rs +++ b/src/misc/cookies.rs @@ -1,7 +1,8 @@ use std::rc::Rc; use gpui::{ - App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, + App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + Styled, Window, div, }; use crate::{ @@ -104,13 +105,18 @@ impl RenderOnce for CookieBanner { .flex() .flex_col() .gap_3() - .child(row( - Switch::new((id.clone(), "necessary"), true) - .label("Necessary") - .disabled(true), - "Keeps the site working; always on.".into(), - cx, - )) + .child({ + let (on, rests) = (true, true); + div() + .debug_selector(|| format!("cookie-necessary-{on}-{rests}")) + .child(row( + Switch::new((id.clone(), "necessary"), on) + .label("Necessary") + .disabled(rests), + "Keeps the site working; always on.".into(), + cx, + )) + }) .children(rows) }); let (all, none, saved) = (answer.clone(), answer.clone(), answer); diff --git a/src/misc/tests/notices.rs b/src/misc/tests/notices.rs index d2c6575..a41750c 100644 --- a/src/misc/tests/notices.rs +++ b/src/misc/tests/notices.rs @@ -1,9 +1,13 @@ use std::cell::{Cell, RefCell}; -use gpui::{AnyElement, IntoElement, TestAppContext}; +use gpui::{ + AnyElement, App, Context, FocusHandle, IntoElement, Modifiers, ParentElement, Render, Styled, + TestAppContext, Window, div, point, px, +}; -use super::{at_root, press, shown, stage}; +use super::{at_root, press, settle, shown, stage}; use crate::misc::{ConsentDialog, CookieBanner, LicenseViewer, Package}; +use crate::{buttons::Button, primitives::FocusScope, theme::Theme}; thread_local! { static SAID: RefCell> = const { RefCell::new(Vec::new()) }; @@ -24,20 +28,25 @@ fn licenses() -> AnyElement { [ Package::new("Alpha", "1.0", "MIT", "Permission is hereby granted"), Package::new("Beta", "2.3", "ISC", "Permission to use, copy, modify"), + Package::new("Gamma", "0.9", "OFL-1.1", "Permission is hereby granted"), ], ) .into_any_element() } -/// The first package's license shows; a move down the list shows the next one's. +/// Each row reads its version and license; the first license shows, and moves down the list show the next ones, the list marking each. #[gpui::test] fn a_license_viewer_shows_the_chosen_license(cx: &mut TestAppContext) { let (_, cx) = stage(licenses, cx); - assert!(shown("license-Alpha", cx)); + assert!(shown("license-Alpha", cx) && shown("item-description-2.3 ยท ISC", cx)); press("tab", cx); press("down", cx); - press("space", cx); - assert!(shown("license-Beta", cx)); + assert!(shown("license-Beta", cx) && shown("item-selected-Beta", cx)); + press("down", cx); + assert!( + shown("license-Gamma", cx), + "the list kept its mark on Beta, so its cursor went on from there" + ); } fn consent() -> AnyElement { @@ -78,13 +87,77 @@ fn consent_waits_for_the_box(cx: &mut TestAppContext) { assert_eq!(said(), ["accepted"], "Accept answers alone"); } -/// Escape declines, once. +/// Escape, Decline and a press on the scrim each decline, once. #[gpui::test] -fn escape_declines_consent(cx: &mut TestAppContext) { +fn every_other_way_out_declines(cx: &mut TestAppContext) { let (_, cx) = stage(consent, cx); press("tab", cx); press("escape", cx); - assert_eq!(said(), ["declined"]); + OPEN.set(true); + cx.update(|window, _| window.refresh()); + settle(cx); + press("tab", cx); + press("tab", cx); + press("enter", cx); + OPEN.set(true); + cx.update(|window, _| window.refresh()); + settle(cx); + cx.simulate_click(point(px(3.0), px(3.0)), Modifiers::none()); + settle(cx); + assert_eq!(said(), ["declined", "declined", "declined"]); +} + +/// A page with a button that opens the consent dialog, which answers into `said`. +struct Opener { + root: FocusHandle, + button: FocusHandle, +} + +impl Render for Opener { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let view = cx.entity(); + FocusScope::new(&self.root) + .root() + .size_full() + .child( + Button::new("open", "Terms") + .focus_handle(&self.button) + .on_click(move |_, _, cx| { + OPEN.set(true); + view.update(cx, |_, cx| cx.notify()); + }), + ) + .child(div().child(consent())) + } +} + +/// Accept hands focus back to the button that opened the terms. +#[gpui::test] +fn consent_hands_focus_back_to_its_opener(cx: &mut TestAppContext) { + OPEN.set(false); + cx.update(|cx: &mut App| { + Theme::init(cx); + crate::forms::bind_keys(cx); + cx.bind_keys([ + gpui::KeyBinding::new("tab", crate::primitives::FocusNext, None), + gpui::KeyBinding::new("shift-tab", crate::primitives::FocusPrev, None), + ]); + }); + let (view, cx) = cx.add_window_view(|_, cx| Opener { + root: cx.focus_handle(), + button: cx.focus_handle(), + }); + let button = view.read_with(cx, |opener, _| opener.button.clone()); + cx.update(|window, _| window.focus(&button)); + settle(cx); + press("enter", cx); + press("tab", cx); + press("space", cx); + press("tab", cx); + press("tab", cx); + press("enter", cx); + assert_eq!(said(), ["accepted"]); + assert!(cx.update(|window, _| button.is_focused(window))); } fn cookies() -> AnyElement { @@ -106,6 +179,10 @@ fn a_cookie_banner_hands_over_the_kinds_chosen(cx: &mut TestAppContext) { press("tab", cx); press("enter", cx); assert!(!at_root(cx), "Save choices took Choose's focus"); + assert!( + shown("cookie-necessary-true-true", cx), + "necessary cookies stay on" + ); for _ in 0..3 { press("shift-tab", cx); }