gpui and gpui_platform now come from Zed's repo by git at 1a28cff, the code gpui-pre 0.3.7 republishes, so the gallery can later build for the browser. gpui takes stacker, as 0.2.2 always did, and gpui_platform takes font-kit, without which macOS draws no text; runtime_shaders moves to gpui_platform. The API moved: focus, blur and Tab steps take cx, Corner is Anchor, shaped lines paint with an alignment, defer_draw takes a content mask, the inspector registers a factory, async updates return their value, and wrapped lines are shared, not cloned. Updates through a window context go through the window, so a closed window still ends them as it did. Where the new gpui behaves differently, Ely keeps its old intent. Holds and reveals under a resting pointer survive typing through gpui's input-modality-independent hover; previews the keyboard also moves keep the default, and the calendar's leave ignores a key press. Enter reaches a button inside the code editor, since gpui now arms a key click on the press. The event logger skips a lone modifier. A video and an image grid keep their shape in a flex column under taffy 0.13, as a picture does. AGENTS.md records the new stack and the rules this port changes; the rest of the old gpui and taffy rules are re-tested in T44c.
289 lines
9.9 KiB
Rust
289 lines
9.9 KiB
Rust
use gpui::{
|
|
AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, KeyUpEvent, Keystroke,
|
|
ParentElement, Render, TestAppContext, VisualTestContext, Window, div, prelude::*, px, size,
|
|
};
|
|
|
|
use super::{WindowManager, WindowSwitcher};
|
|
use crate::{buttons::Button, primitives::FocusScope, theme::Theme};
|
|
|
|
struct Blank;
|
|
|
|
impl Render for Blank {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
}
|
|
}
|
|
|
|
struct Desk {
|
|
root: FocusHandle,
|
|
open: bool,
|
|
enters_above: usize,
|
|
}
|
|
|
|
impl Render for Desk {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let (opener, closer, above) = (cx.entity(), cx.entity(), cx.entity());
|
|
let scope = FocusScope::new(&self.root)
|
|
.size_full()
|
|
.child(Button::new("opener", "Windows").on_click(move |_, _, cx| {
|
|
opener.update(cx, |desk, cx| {
|
|
desk.open = true;
|
|
cx.notify();
|
|
})
|
|
}))
|
|
.when(self.open, |desk| {
|
|
desk.child(WindowSwitcher::new("switcher", move |_, cx| {
|
|
closer.update(cx, |desk, cx| {
|
|
desk.open = false;
|
|
cx.notify();
|
|
})
|
|
}))
|
|
});
|
|
div()
|
|
.size_full()
|
|
.on_key_down(move |event, _, cx| {
|
|
if event.keystroke.key == "enter" {
|
|
above.update(cx, |desk, _| desk.enters_above += 1);
|
|
}
|
|
})
|
|
.child(scope)
|
|
}
|
|
}
|
|
|
|
fn settle(cx: &mut VisualTestContext) {
|
|
cx.run_until_parked();
|
|
cx.update(|window, _| window.refresh());
|
|
cx.run_until_parked();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn the_switcher_switches_on_release_so_its_opener_stays_shut(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (desk, cx) = cx.add_window_view(|_, cx| Desk {
|
|
root: cx.focus_handle(),
|
|
open: false,
|
|
enters_above: 0,
|
|
});
|
|
cx.update(|_, cx| {
|
|
WindowManager::open("Notes", size(px(320.0), px(240.0)), cx, |_, cx| {
|
|
cx.new(|_| Blank)
|
|
})
|
|
.expect("a managed window");
|
|
});
|
|
settle(cx);
|
|
cx.update(|window, cx| window.focus_next(cx));
|
|
press("enter", cx);
|
|
assert!(desk.read_with(cx, |desk, _| desk.open));
|
|
let above = desk.read_with(cx, |desk, _| desk.enters_above);
|
|
press("enter", cx);
|
|
assert!(!desk.read_with(cx, |desk, _| desk.open));
|
|
assert_eq!(desk.read_with(cx, |desk, _| desk.enters_above), above);
|
|
}
|
|
|
|
struct Platformed;
|
|
|
|
impl Render for Platformed {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.flex()
|
|
.child(
|
|
div()
|
|
.debug_selector(|| "controls".into())
|
|
.child(super::WindowControls::new("controls")),
|
|
)
|
|
.child(
|
|
div()
|
|
.debug_selector(|| "caps".into())
|
|
.child(crate::typography::Kbd::new("secondary-s")),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Window buttons and key caps follow the theme's platform: Windows draws three wide caption buttons and spells Ctrl.
|
|
#[gpui::test]
|
|
fn the_themes_platform_draws_the_buttons_and_the_caps(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
cx.update(|cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Mac));
|
|
let (_, cx) = cx.add_window_view(|_, _| Platformed);
|
|
settle(cx);
|
|
let width = |name: &'static str, cx: &mut VisualTestContext| {
|
|
cx.debug_bounds(name).expect("a measured part").size.width
|
|
};
|
|
let (lights, command) = (width("controls", cx), width("caps", cx));
|
|
cx.update(|_, cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Windows));
|
|
settle(cx);
|
|
assert!(
|
|
width("controls", cx) > lights * 2.0,
|
|
"caption buttons wider than lights"
|
|
);
|
|
assert!(
|
|
width("caps", cx) > command,
|
|
"Ctrl wider than the command mark"
|
|
);
|
|
}
|
|
|
|
struct LongTitle;
|
|
|
|
impl Render for LongTitle {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
div().w(px(280.0)).flex().flex_col().child(
|
|
super::TitleBar::new("bar")
|
|
.title("Ely — Projects at a narrow width, and more besides")
|
|
.platform(crate::theme::Platform::Windows),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// At 280px a Windows bar's long title gives way, and its close button ends inside the bar.
|
|
#[gpui::test]
|
|
fn a_long_title_leaves_the_caption_buttons_in_the_bar(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (_, cx) = cx.add_window_view(|_, _| LongTitle);
|
|
settle(cx);
|
|
let close = cx.debug_bounds("caption-Close").expect("a close button");
|
|
assert!(close.right() <= px(280.0), "{close:?} inside the bar");
|
|
}
|
|
|
|
struct Wrapped;
|
|
|
|
impl Render for Wrapped {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let windows = crate::theme::Platform::Windows;
|
|
div()
|
|
.w(px(280.0))
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
div()
|
|
.debug_selector(|| "row".into())
|
|
.flex()
|
|
.flex_wrap()
|
|
.items_center()
|
|
.gap_3()
|
|
.child(super::WindowControls::new("controls").platform(windows))
|
|
.child(div().debug_selector(|| "combo".into()).child(
|
|
crate::typography::KbdCombo::new("secondary-shift-k secondary-shift-s"),
|
|
)),
|
|
)
|
|
.child(div().h(px(20.0)))
|
|
}
|
|
}
|
|
|
|
/// Caption buttons keep their own height beside caps that wrap below them, so the caps end inside the row.
|
|
#[gpui::test]
|
|
fn caption_buttons_keep_their_height_in_a_wrapping_row(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
cx.update(|cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Windows));
|
|
let (_, cx) = cx.add_window_view(|_, _| Wrapped);
|
|
settle(cx);
|
|
let row = cx.debug_bounds("row").expect("the row");
|
|
let combo = cx.debug_bounds("combo").expect("the caps");
|
|
assert!(combo.top() > row.top(), "the caps wrap below the buttons");
|
|
assert!(combo.bottom() <= row.bottom(), "{combo:?} inside {row:?}");
|
|
}
|
|
|
|
struct Bar(Option<&'static str>, crate::theme::Platform);
|
|
|
|
impl Render for Bar {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let bar = super::TitleBar::new("bar").platform(self.1);
|
|
div()
|
|
.w(px(280.0))
|
|
.flex()
|
|
.flex_col()
|
|
.child(
|
|
div()
|
|
.debug_selector(|| "bar-box".into())
|
|
.child(match self.0 {
|
|
Some(title) => bar.title(title),
|
|
None => bar,
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A Windows bar with no title still keeps its buttons at its right end.
|
|
#[gpui::test]
|
|
fn an_untitled_windows_bar_keeps_its_buttons_at_the_end(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (_, cx) = cx.add_window_view(|_, _| Bar(None, crate::theme::Platform::Windows));
|
|
settle(cx);
|
|
let close = cx.debug_bounds("caption-Close").expect("a close button");
|
|
assert_eq!(close.right(), px(280.0));
|
|
}
|
|
|
|
/// A long centered title ends before the Linux round buttons.
|
|
#[gpui::test]
|
|
fn a_long_centered_title_ends_before_the_buttons(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let long = "Ely — Projects at a narrow width, and more besides";
|
|
let (_, cx) = cx.add_window_view(|_, _| Bar(Some(long), crate::theme::Platform::Linux));
|
|
settle(cx);
|
|
let title = cx.debug_bounds("titlebar-title").expect("the title");
|
|
let controls = cx.debug_bounds("titlebar-controls").expect("the buttons");
|
|
assert!(
|
|
title.right() <= controls.left(),
|
|
"{title:?} before {controls:?}"
|
|
);
|
|
}
|
|
|
|
/// At half the rem size the caption buttons shrink with the bar and stay inside it.
|
|
#[gpui::test]
|
|
fn caption_buttons_follow_the_windows_rem(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (_, cx) = cx.add_window_view(|_, _| Bar(None, crate::theme::Platform::Windows));
|
|
cx.update(|window, _| window.set_rem_size(px(8.0)));
|
|
settle(cx);
|
|
let bar = cx.debug_bounds("bar-box").expect("the bar");
|
|
let close = cx.debug_bounds("caption-Close").expect("a close button");
|
|
assert!(
|
|
close.top() >= bar.top() && close.bottom() <= bar.bottom(),
|
|
"{close:?} inside {bar:?}"
|
|
);
|
|
}
|
|
|
|
/// A short title sits at the bar's center, whatever stands at either end.
|
|
#[gpui::test]
|
|
fn a_short_title_sits_at_the_center(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (_, cx) = cx.add_window_view(|_, _| Bar(Some("Notes"), crate::theme::Platform::Linux));
|
|
settle(cx);
|
|
let title = cx.debug_bounds("titlebar-title").expect("the title");
|
|
let middle = title.left() + title.size.width / 2.0;
|
|
assert!((middle - px(140.0)).abs() < px(1.0), "{title:?} centered");
|
|
}
|
|
|
|
struct Splash;
|
|
|
|
impl Render for Splash {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
.size_full()
|
|
.child(super::SplashScreen::new("Ely").progress(Some(0.5)))
|
|
}
|
|
}
|
|
|
|
/// The splash's bar spans its column, though the column centers what it holds.
|
|
#[gpui::test]
|
|
fn the_splash_bar_spans_its_column(cx: &mut TestAppContext) {
|
|
cx.update(Theme::init);
|
|
let (_, cx) = cx.add_window_view(|_, _| Splash);
|
|
settle(cx);
|
|
let fill = cx.debug_bounds("progress-fill-0").expect("the bar fills");
|
|
let width = cx.update(|window, _| window.viewport_size().width);
|
|
let column = (width - px(80.0)) * 0.6;
|
|
assert!(
|
|
(fill.size.width - column * 0.5).abs() < px(1.0),
|
|
"half the column, not {fill:?}"
|
|
);
|
|
}
|