Compare commits
7 Commits
fix_devcon
...
ex-pointer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d49a8e04e6 | ||
|
|
1f34525634 | ||
|
|
da6c2a172c | ||
|
|
f2409f2605 | ||
|
|
ce1c228e6e | ||
|
|
96ddbd4e13 | ||
|
|
f224d2a923 |
@@ -28,6 +28,8 @@ pub use entity_map::*;
|
|||||||
use http_client::{HttpClient, Url};
|
use http_client::{HttpClient, Url};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_app::*;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use test_context::*;
|
pub use test_context::*;
|
||||||
use util::{ResultExt, debug_panic};
|
use util::{ResultExt, debug_panic};
|
||||||
|
|
||||||
@@ -51,6 +53,8 @@ mod async_context;
|
|||||||
mod context;
|
mod context;
|
||||||
mod entity_map;
|
mod entity_map;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
mod test_app;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
mod test_context;
|
mod test_context;
|
||||||
|
|
||||||
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
||||||
|
|||||||
605
crates/gpui/src/app/test_app.rs
Normal file
605
crates/gpui/src/app/test_app.rs
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
//! A clean testing API for GPUI applications.
|
||||||
|
//!
|
||||||
|
//! `TestApp` provides a simpler alternative to `TestAppContext` with:
|
||||||
|
//! - Automatic effect flushing after updates
|
||||||
|
//! - Clean window creation and inspection
|
||||||
|
//! - Input simulation helpers
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```ignore
|
||||||
|
//! #[test]
|
||||||
|
//! fn test_my_view() {
|
||||||
|
//! let mut app = TestApp::new();
|
||||||
|
//!
|
||||||
|
//! let mut window = app.open_window(|window, cx| {
|
||||||
|
//! MyView::new(window, cx)
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! window.update(|view, window, cx| {
|
||||||
|
//! view.do_something(cx);
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! // Check rendered state
|
||||||
|
//! assert_eq!(window.title(), Some("Expected Title"));
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
|
||||||
|
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||||
|
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
|
||||||
|
SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
|
||||||
|
WindowHandle, WindowOptions, app::GpuiMode,
|
||||||
|
};
|
||||||
|
use rand::{SeedableRng, rngs::StdRng};
|
||||||
|
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
/// A test application context with a clean API.
|
||||||
|
///
|
||||||
|
/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
|
||||||
|
/// each update and provides simpler window management.
|
||||||
|
pub struct TestApp {
|
||||||
|
app: Rc<AppCell>,
|
||||||
|
platform: Rc<TestPlatform>,
|
||||||
|
background_executor: BackgroundExecutor,
|
||||||
|
foreground_executor: ForegroundExecutor,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
dispatcher: TestDispatcher,
|
||||||
|
text_system: Arc<TextSystem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestApp {
|
||||||
|
/// Create a new test application.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_seed(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new test application with a specific random seed.
|
||||||
|
pub fn with_seed(seed: u64) -> Self {
|
||||||
|
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
|
||||||
|
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||||
|
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||||
|
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||||
|
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||||
|
let asset_source = Arc::new(());
|
||||||
|
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||||
|
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
|
|
||||||
|
let mut app = App::new_app(platform.clone(), asset_source, http_client);
|
||||||
|
app.borrow_mut().mode = GpuiMode::test();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
app,
|
||||||
|
platform,
|
||||||
|
background_executor,
|
||||||
|
foreground_executor,
|
||||||
|
dispatcher,
|
||||||
|
text_system,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a closure with mutable access to the App context.
|
||||||
|
/// Automatically runs until parked after the closure completes.
|
||||||
|
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||||
|
let result = {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
app.update(f)
|
||||||
|
};
|
||||||
|
self.run_until_parked();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a closure with read-only access to the App context.
|
||||||
|
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
f(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new entity in the app.
|
||||||
|
pub fn new_entity<T: 'static>(
|
||||||
|
&mut self,
|
||||||
|
build: impl FnOnce(&mut Context<T>) -> T,
|
||||||
|
) -> Entity<T> {
|
||||||
|
self.update(|cx| cx.new(build))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an entity.
|
||||||
|
pub fn update_entity<T: 'static, R>(
|
||||||
|
&mut self,
|
||||||
|
entity: &Entity<T>,
|
||||||
|
f: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||||
|
) -> R {
|
||||||
|
self.update(|cx| entity.update(cx, f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an entity.
|
||||||
|
pub fn read_entity<T: 'static, R>(
|
||||||
|
&self,
|
||||||
|
entity: &Entity<T>,
|
||||||
|
f: impl FnOnce(&T, &App) -> R,
|
||||||
|
) -> R {
|
||||||
|
self.read(|cx| f(entity.read(cx), cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a test window with the given root view.
|
||||||
|
pub fn open_window<V: Render + 'static>(
|
||||||
|
&mut self,
|
||||||
|
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||||
|
) -> TestWindow<V> {
|
||||||
|
let bounds = self.read(|cx| Bounds::maximized(None, cx));
|
||||||
|
let handle = self.update(|cx| {
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|window, cx| cx.new(|cx| build_view(window, cx)),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
TestWindow {
|
||||||
|
handle,
|
||||||
|
app: self.app.clone(),
|
||||||
|
platform: self.platform.clone(),
|
||||||
|
background_executor: self.background_executor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a test window with specific options.
|
||||||
|
pub fn open_window_with_options<V: Render + 'static>(
|
||||||
|
&mut self,
|
||||||
|
options: WindowOptions,
|
||||||
|
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||||
|
) -> TestWindow<V> {
|
||||||
|
let handle = self.update(|cx| {
|
||||||
|
cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
TestWindow {
|
||||||
|
handle,
|
||||||
|
app: self.app.clone(),
|
||||||
|
platform: self.platform.clone(),
|
||||||
|
background_executor: self.background_executor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run pending tasks until there's nothing left to do.
|
||||||
|
pub fn run_until_parked(&self) {
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance the simulated clock by the given duration.
|
||||||
|
pub fn advance_clock(&self, duration: Duration) {
|
||||||
|
self.background_executor.advance_clock(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a future on the foreground executor.
|
||||||
|
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = R> + 'static,
|
||||||
|
R: 'static,
|
||||||
|
{
|
||||||
|
self.foreground_executor.spawn(f(self.to_async()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a future on the background executor.
|
||||||
|
pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||||
|
where
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
self.background_executor.spawn(future)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an async handle to the app.
|
||||||
|
pub fn to_async(&self) -> AsyncApp {
|
||||||
|
AsyncApp {
|
||||||
|
app: Rc::downgrade(&self.app),
|
||||||
|
background_executor: self.background_executor.clone(),
|
||||||
|
foreground_executor: self.foreground_executor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the background executor.
|
||||||
|
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||||
|
&self.background_executor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the foreground executor.
|
||||||
|
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||||
|
&self.foreground_executor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text system.
|
||||||
|
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||||
|
&self.text_system
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a global of the given type exists.
|
||||||
|
pub fn has_global<G: Global>(&self) -> bool {
|
||||||
|
self.read(|cx| cx.has_global::<G>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a global value.
|
||||||
|
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||||
|
self.update(|cx| cx.set_global(global));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a global value.
|
||||||
|
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
|
||||||
|
self.read(|cx| f(cx.global(), cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a global value.
|
||||||
|
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||||
|
self.update(|cx| cx.update_global(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform simulation methods
|
||||||
|
|
||||||
|
/// Write text to the simulated clipboard.
|
||||||
|
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||||
|
self.platform.write_to_clipboard(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read from the simulated clipboard.
|
||||||
|
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||||
|
self.platform.read_from_clipboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get URLs that have been opened via `cx.open_url()`.
|
||||||
|
pub fn opened_url(&self) -> Option<String> {
|
||||||
|
self.platform.opened_url.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a file path prompt is pending.
|
||||||
|
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.platform.did_prompt_for_new_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate answering a path selection dialog.
|
||||||
|
pub fn simulate_new_path_selection(
|
||||||
|
&self,
|
||||||
|
select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||||
|
) {
|
||||||
|
self.platform.simulate_new_path_selection(select);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a prompt dialog is pending.
|
||||||
|
pub fn has_pending_prompt(&self) -> bool {
|
||||||
|
self.platform.has_pending_prompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate answering a prompt dialog.
|
||||||
|
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||||
|
self.platform.simulate_prompt_answer(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all open windows.
|
||||||
|
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||||
|
self.read(|cx| cx.windows())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestApp {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A test window with inspection and simulation capabilities.
|
||||||
|
pub struct TestWindow<V> {
|
||||||
|
handle: WindowHandle<V>,
|
||||||
|
app: Rc<AppCell>,
|
||||||
|
platform: Rc<TestPlatform>,
|
||||||
|
background_executor: BackgroundExecutor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: 'static + Render> TestWindow<V> {
|
||||||
|
/// Get the window handle.
|
||||||
|
pub fn handle(&self) -> WindowHandle<V> {
|
||||||
|
self.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the root view entity.
|
||||||
|
pub fn root(&self) -> Entity<V> {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
let any_handle: AnyWindowHandle = self.handle.into();
|
||||||
|
app.update_window(any_handle, |root_view, _, _| {
|
||||||
|
root_view.downcast::<V>().expect("root view type mismatch")
|
||||||
|
})
|
||||||
|
.expect("window not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the root view.
|
||||||
|
/// Automatically draws the window after the update to ensure the scene is current.
|
||||||
|
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
|
||||||
|
let result = {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
let any_handle: AnyWindowHandle = self.handle.into();
|
||||||
|
app.update_window(any_handle, |root_view, window, cx| {
|
||||||
|
let view = root_view.downcast::<V>().expect("root view type mismatch");
|
||||||
|
view.update(cx, |view, cx| f(view, window, cx))
|
||||||
|
})
|
||||||
|
.expect("window not found")
|
||||||
|
};
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
self.draw();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the root view.
|
||||||
|
pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
let view = self
|
||||||
|
.app
|
||||||
|
.borrow()
|
||||||
|
.windows
|
||||||
|
.get(self.handle.window_id())
|
||||||
|
.and_then(|w| w.as_ref())
|
||||||
|
.and_then(|w| w.root.clone())
|
||||||
|
.and_then(|r| r.downcast::<V>().ok())
|
||||||
|
.expect("window or root view not found");
|
||||||
|
f(view.read(&app), &app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the window title.
|
||||||
|
pub fn title(&self) -> Option<String> {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
app.read_window(&self.handle, |_, _cx| {
|
||||||
|
// TODO: expose title through Window API
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a keystroke.
|
||||||
|
/// Automatically draws the window after the keystroke.
|
||||||
|
pub fn simulate_keystroke(&mut self, keystroke: &str) {
|
||||||
|
let keystroke = Keystroke::parse(keystroke).unwrap();
|
||||||
|
{
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
let any_handle: AnyWindowHandle = self.handle.into();
|
||||||
|
app.update_window(any_handle, |_, window, cx| {
|
||||||
|
window.dispatch_keystroke(keystroke, cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
self.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate multiple keystrokes (space-separated).
|
||||||
|
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||||
|
for keystroke in keystrokes.split(' ') {
|
||||||
|
self.simulate_keystroke(keystroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate typing text.
|
||||||
|
pub fn simulate_input(&mut self, input: &str) {
|
||||||
|
for char in input.chars() {
|
||||||
|
self.simulate_keystroke(&char.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse move.
|
||||||
|
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
|
||||||
|
self.simulate_event(MouseMoveEvent {
|
||||||
|
position,
|
||||||
|
modifiers: Default::default(),
|
||||||
|
pressed_button: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse down event.
|
||||||
|
pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||||
|
self.simulate_event(MouseDownEvent {
|
||||||
|
position,
|
||||||
|
button,
|
||||||
|
modifiers: Default::default(),
|
||||||
|
click_count: 1,
|
||||||
|
first_mouse: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a mouse up event.
|
||||||
|
pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||||
|
self.simulate_event(MouseUpEvent {
|
||||||
|
position,
|
||||||
|
button,
|
||||||
|
modifiers: Default::default(),
|
||||||
|
click_count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a click at the given position.
|
||||||
|
pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||||
|
self.simulate_mouse_down(position, button);
|
||||||
|
self.simulate_mouse_up(position, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a scroll event.
|
||||||
|
pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
|
||||||
|
self.simulate_event(crate::ScrollWheelEvent {
|
||||||
|
position,
|
||||||
|
delta: crate::ScrollDelta::Pixels(delta),
|
||||||
|
modifiers: Default::default(),
|
||||||
|
touch_phase: crate::TouchPhase::Moved,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate an input event.
|
||||||
|
/// Automatically draws the window after the event.
|
||||||
|
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
|
||||||
|
let platform_input = event.to_platform_input();
|
||||||
|
{
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
let any_handle: AnyWindowHandle = self.handle.into();
|
||||||
|
app.update_window(any_handle, |_, window, cx| {
|
||||||
|
window.dispatch_event(platform_input, cx);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
self.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate resizing the window.
|
||||||
|
/// Automatically draws the window after the resize.
|
||||||
|
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
|
||||||
|
let window_id = self.handle.window_id();
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
if let Some(Some(window)) = app.windows.get_mut(window_id) {
|
||||||
|
if let Some(test_window) = window.platform_window.as_test() {
|
||||||
|
test_window.simulate_resize(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(app);
|
||||||
|
self.background_executor.run_until_parked();
|
||||||
|
self.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force a redraw of the window.
|
||||||
|
pub fn draw(&mut self) {
|
||||||
|
let mut app = self.app.borrow_mut();
|
||||||
|
let any_handle: AnyWindowHandle = self.handle.into();
|
||||||
|
app.update_window(any_handle, |_, window, cx| {
|
||||||
|
window.draw(cx).clear();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a snapshot of the rendered scene for inspection.
|
||||||
|
/// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
|
||||||
|
pub fn scene_snapshot(&self) -> SceneSnapshot {
|
||||||
|
let app = self.app.borrow();
|
||||||
|
let window = app
|
||||||
|
.windows
|
||||||
|
.get(self.handle.window_id())
|
||||||
|
.and_then(|w| w.as_ref())
|
||||||
|
.expect("window not found");
|
||||||
|
window.rendered_frame.scene.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the named diagnostic quads recorded during imperative paint, without inspecting the
|
||||||
|
/// rest of the scene snapshot.
|
||||||
|
///
|
||||||
|
/// This is useful for tests that want a stable, semantic view of layout/paint geometry without
|
||||||
|
/// coupling to the low-level quad/glyph output.
|
||||||
|
pub fn diagnostic_quads(&self) -> Vec<crate::scene::test_scene::DiagnosticQuad> {
|
||||||
|
self.scene_snapshot().diagnostic_quads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> Clone for TestWindow<V> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
handle: self.handle,
|
||||||
|
app: self.app.clone(),
|
||||||
|
platform: self.platform.clone(),
|
||||||
|
background_executor: self.background_executor.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{FocusHandle, Focusable, div, prelude::*};
|
||||||
|
|
||||||
|
struct Counter {
|
||||||
|
count: usize,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
Self {
|
||||||
|
count: 0,
|
||||||
|
focus_handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment(&mut self, _cx: &mut Context<Self>) {
|
||||||
|
self.count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for Counter {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Counter {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().child(format!("Count: {}", self.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_usage() {
|
||||||
|
let mut app = TestApp::new();
|
||||||
|
|
||||||
|
let mut window = app.open_window(Counter::new);
|
||||||
|
|
||||||
|
window.update(|counter, _window, cx| {
|
||||||
|
counter.increment(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.read(|counter, _| {
|
||||||
|
assert_eq!(counter.count, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_entity_creation() {
|
||||||
|
let mut app = TestApp::new();
|
||||||
|
|
||||||
|
let entity = app.new_entity(|cx| Counter {
|
||||||
|
count: 42,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.read_entity(&entity, |counter, _| {
|
||||||
|
assert_eq!(counter.count, 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.update_entity(&entity, |counter, _cx| {
|
||||||
|
counter.count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.read_entity(&entity, |counter, _| {
|
||||||
|
assert_eq!(counter.count, 43);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_globals() {
|
||||||
|
let mut app = TestApp::new();
|
||||||
|
|
||||||
|
struct MyGlobal(String);
|
||||||
|
impl Global for MyGlobal {}
|
||||||
|
|
||||||
|
assert!(!app.has_global::<MyGlobal>());
|
||||||
|
|
||||||
|
app.set_global(MyGlobal("hello".into()));
|
||||||
|
|
||||||
|
assert!(app.has_global::<MyGlobal>());
|
||||||
|
|
||||||
|
app.read_global::<MyGlobal, _>(|global, _| {
|
||||||
|
assert_eq!(global.0, "hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.update_global::<MyGlobal, _>(|global, _| {
|
||||||
|
global.0 = "world".into();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.read_global::<MyGlobal, _>(|global, _| {
|
||||||
|
assert_eq!(global.0, "world");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ use crate::{
|
|||||||
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
||||||
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
||||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestPlatformWindow,
|
||||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
TestScreenCaptureSource, TextSystem, VisualContext, Window, WindowBounds, WindowHandle,
|
||||||
WindowHandle, WindowOptions, app::GpuiMode,
|
WindowOptions, app::GpuiMode,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use futures::{Stream, StreamExt, channel::oneshot};
|
use futures::{Stream, StreamExt, channel::oneshot};
|
||||||
@@ -220,7 +220,7 @@ impl TestAppContext {
|
|||||||
f(&cx)
|
f(&cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
/// Adds a new window. The Window will always be backed by a `TestPlatformWindow` which
|
||||||
/// can be retrieved with `self.test_window(handle)`
|
/// can be retrieved with `self.test_window(handle)`
|
||||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||||
where
|
where
|
||||||
@@ -465,8 +465,8 @@ impl TestAppContext {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `TestWindow` backing the given handle.
|
/// Returns the `TestPlatformWindow` backing the given handle.
|
||||||
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestPlatformWindow {
|
||||||
self.app
|
self.app
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.windows
|
.windows
|
||||||
|
|||||||
@@ -808,6 +808,15 @@ impl LinearColorStop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Background {
|
impl Background {
|
||||||
|
/// Returns the solid color if this is a solid background, None otherwise.
|
||||||
|
pub fn as_solid(&self) -> Option<Hsla> {
|
||||||
|
if self.tag == BackgroundTag::Solid {
|
||||||
|
Some(self.solid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Use specified color space for color interpolation.
|
/// Use specified color space for color interpolation.
|
||||||
///
|
///
|
||||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
|
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
TestDisplay, TestPlatformWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
@@ -26,7 +26,7 @@ pub(crate) struct TestPlatform {
|
|||||||
background_executor: BackgroundExecutor,
|
background_executor: BackgroundExecutor,
|
||||||
foreground_executor: ForegroundExecutor,
|
foreground_executor: ForegroundExecutor,
|
||||||
|
|
||||||
pub(crate) active_window: RefCell<Option<TestWindow>>,
|
pub(crate) active_window: RefCell<Option<TestPlatformWindow>>,
|
||||||
active_display: Rc<dyn PlatformDisplay>,
|
active_display: Rc<dyn PlatformDisplay>,
|
||||||
active_cursor: Mutex<CursorStyle>,
|
active_cursor: Mutex<CursorStyle>,
|
||||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||||
@@ -196,7 +196,7 @@ impl TestPlatform {
|
|||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
|
pub(crate) fn set_active_window(&self, window: Option<TestPlatformWindow>) {
|
||||||
let executor = self.foreground_executor();
|
let executor = self.foreground_executor();
|
||||||
let previous_window = self.active_window.borrow_mut().take();
|
let previous_window = self.active_window.borrow_mut().take();
|
||||||
self.active_window.borrow_mut().clone_from(&window);
|
self.active_window.borrow_mut().clone_from(&window);
|
||||||
@@ -314,7 +314,7 @@ impl Platform for TestPlatform {
|
|||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
params: WindowParams,
|
params: WindowParams,
|
||||||
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
||||||
let window = TestWindow::new(
|
let window = TestPlatformWindow::new(
|
||||||
handle,
|
handle,
|
||||||
params,
|
params,
|
||||||
self.weak.clone(),
|
self.weak.clone(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::{
|
|||||||
sync::{self, Arc},
|
sync::{self, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct TestWindowState {
|
pub(crate) struct TestPlatformWindowState {
|
||||||
pub(crate) bounds: Bounds<Pixels>,
|
pub(crate) bounds: Bounds<Pixels>,
|
||||||
pub(crate) handle: AnyWindowHandle,
|
pub(crate) handle: AnyWindowHandle,
|
||||||
display: Rc<dyn PlatformDisplay>,
|
display: Rc<dyn PlatformDisplay>,
|
||||||
@@ -32,9 +32,9 @@ pub(crate) struct TestWindowState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
|
pub(crate) struct TestPlatformWindow(pub(crate) Rc<Mutex<TestPlatformWindowState>>);
|
||||||
|
|
||||||
impl HasWindowHandle for TestWindow {
|
impl HasWindowHandle for TestPlatformWindow {
|
||||||
fn window_handle(
|
fn window_handle(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||||
@@ -42,7 +42,7 @@ impl HasWindowHandle for TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasDisplayHandle for TestWindow {
|
impl HasDisplayHandle for TestPlatformWindow {
|
||||||
fn display_handle(
|
fn display_handle(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||||
@@ -50,14 +50,14 @@ impl HasDisplayHandle for TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestWindow {
|
impl TestPlatformWindow {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
params: WindowParams,
|
params: WindowParams,
|
||||||
platform: Weak<TestPlatform>,
|
platform: Weak<TestPlatform>,
|
||||||
display: Rc<dyn PlatformDisplay>,
|
display: Rc<dyn PlatformDisplay>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self(Rc::new(Mutex::new(TestWindowState {
|
Self(Rc::new(Mutex::new(TestPlatformWindowState {
|
||||||
bounds: params.bounds,
|
bounds: params.bounds,
|
||||||
display,
|
display,
|
||||||
platform,
|
platform,
|
||||||
@@ -111,7 +111,7 @@ impl TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformWindow for TestWindow {
|
impl PlatformWindow for TestPlatformWindow {
|
||||||
fn bounds(&self) -> Bounds<Pixels> {
|
fn bounds(&self) -> Bounds<Pixels> {
|
||||||
self.0.lock().bounds
|
self.0.lock().bounds
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ impl PlatformWindow for TestWindow {
|
|||||||
self.0.lock().sprite_atlas.clone()
|
self.0.lock().sprite_atlas.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,126 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
|||||||
|
|
||||||
pub(crate) type DrawOrder = u32;
|
pub(crate) type DrawOrder = u32;
|
||||||
|
|
||||||
|
/// Test-only scene snapshot for inspecting rendered content.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub mod test_scene {
|
||||||
|
use crate::{Bounds, Hsla, Point, ScaledPixels, SharedString};
|
||||||
|
|
||||||
|
/// A rendered quad (background, border, cursor, selection, etc.)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderedQuad {
|
||||||
|
/// Bounds in scaled pixels.
|
||||||
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
|
/// Background color (if solid).
|
||||||
|
pub background_color: Option<Hsla>,
|
||||||
|
/// Border color.
|
||||||
|
pub border_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named diagnostic quad for tests and debugging of imperative paint logic.
|
||||||
|
///
|
||||||
|
/// This is not necessarily a "real" painted quad; it is metadata recorded alongside a scene.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DiagnosticQuad {
|
||||||
|
/// A stable name that test code can filter by.
|
||||||
|
pub name: SharedString,
|
||||||
|
/// Bounds in scaled pixels.
|
||||||
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
|
/// Optional color hint (useful when visualizing).
|
||||||
|
pub color: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rendered text glyph.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderedGlyph {
|
||||||
|
/// Origin position in scaled pixels.
|
||||||
|
pub origin: Point<ScaledPixels>,
|
||||||
|
/// Size in scaled pixels.
|
||||||
|
pub size: crate::Size<ScaledPixels>,
|
||||||
|
/// Color of the glyph.
|
||||||
|
pub color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of scene contents for testing.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct SceneSnapshot {
|
||||||
|
/// All rendered quads.
|
||||||
|
pub quads: Vec<RenderedQuad>,
|
||||||
|
/// All rendered text glyphs.
|
||||||
|
pub glyphs: Vec<RenderedGlyph>,
|
||||||
|
/// Named diagnostic quads recorded by imperative drawing code for tests/debugging.
|
||||||
|
pub diagnostic_quads: Vec<DiagnosticQuad>,
|
||||||
|
/// Number of shadow primitives.
|
||||||
|
pub shadow_count: usize,
|
||||||
|
/// Number of path primitives.
|
||||||
|
pub path_count: usize,
|
||||||
|
/// Number of underline primitives.
|
||||||
|
pub underline_count: usize,
|
||||||
|
/// Number of polychrome sprites (images, emoji).
|
||||||
|
pub polychrome_sprite_count: usize,
|
||||||
|
/// Number of surface primitives.
|
||||||
|
pub surface_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SceneSnapshot {
|
||||||
|
/// Get unique Y positions of quads, sorted.
|
||||||
|
pub fn quad_y_positions(&self) -> Vec<f32> {
|
||||||
|
let mut positions: Vec<f32> = self.quads.iter().map(|q| q.bounds.origin.y.0).collect();
|
||||||
|
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
positions.dedup();
|
||||||
|
positions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unique Y positions of glyphs, sorted.
|
||||||
|
pub fn glyph_y_positions(&self) -> Vec<f32> {
|
||||||
|
let mut positions: Vec<f32> = self.glyphs.iter().map(|g| g.origin.y.0).collect();
|
||||||
|
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
positions.dedup();
|
||||||
|
positions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find quads within a Y range.
|
||||||
|
pub fn quads_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedQuad> {
|
||||||
|
self.quads
|
||||||
|
.iter()
|
||||||
|
.filter(|q| {
|
||||||
|
let y = q.bounds.origin.y.0;
|
||||||
|
y >= min_y && y < max_y
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find glyphs within a Y range.
|
||||||
|
pub fn glyphs_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedGlyph> {
|
||||||
|
self.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| {
|
||||||
|
let y = g.origin.y.0;
|
||||||
|
y >= min_y && y < max_y
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debug summary string.
|
||||||
|
pub fn summary(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"quads: {}, glyphs: {}, diagnostic_quads: {}, shadows: {}, paths: {}, underlines: {}, polychrome: {}, surfaces: {}",
|
||||||
|
self.quads.len(),
|
||||||
|
self.glyphs.len(),
|
||||||
|
self.diagnostic_quads.len(),
|
||||||
|
self.shadow_count,
|
||||||
|
self.path_count,
|
||||||
|
self.underline_count,
|
||||||
|
self.polychrome_sprite_count,
|
||||||
|
self.surface_count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_scene::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Scene {
|
pub(crate) struct Scene {
|
||||||
pub(crate) paint_operations: Vec<PaintOperation>,
|
pub(crate) paint_operations: Vec<PaintOperation>,
|
||||||
@@ -32,6 +152,8 @@ pub(crate) struct Scene {
|
|||||||
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
||||||
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
||||||
pub(crate) surfaces: Vec<PaintSurface>,
|
pub(crate) surfaces: Vec<PaintSurface>,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub(crate) diagnostic_quads: Vec<test_scene::DiagnosticQuad>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
@@ -46,6 +168,8 @@ impl Scene {
|
|||||||
self.monochrome_sprites.clear();
|
self.monochrome_sprites.clear();
|
||||||
self.polychrome_sprites.clear();
|
self.polychrome_sprites.clear();
|
||||||
self.surfaces.clear();
|
self.surfaces.clear();
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
self.diagnostic_quads.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
@@ -124,6 +248,41 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a snapshot of the scene for testing.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn snapshot(&self) -> SceneSnapshot {
|
||||||
|
let quads = self
|
||||||
|
.quads
|
||||||
|
.iter()
|
||||||
|
.map(|q| RenderedQuad {
|
||||||
|
bounds: q.bounds,
|
||||||
|
background_color: q.background.as_solid(),
|
||||||
|
border_color: q.border_color,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let glyphs = self
|
||||||
|
.monochrome_sprites
|
||||||
|
.iter()
|
||||||
|
.map(|s| RenderedGlyph {
|
||||||
|
origin: s.bounds.origin,
|
||||||
|
size: s.bounds.size,
|
||||||
|
color: s.color,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SceneSnapshot {
|
||||||
|
quads,
|
||||||
|
glyphs,
|
||||||
|
diagnostic_quads: self.diagnostic_quads.clone(),
|
||||||
|
shadow_count: self.shadows.len(),
|
||||||
|
path_count: self.paths.len(),
|
||||||
|
underline_count: self.underlines.len(),
|
||||||
|
polychrome_sprite_count: self.polychrome_sprites.len(),
|
||||||
|
surface_count: self.surfaces.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn finish(&mut self) {
|
pub fn finish(&mut self) {
|
||||||
self.shadows.sort_by_key(|shadow| shadow.order);
|
self.shadows.sort_by_key(|shadow| shadow.order);
|
||||||
self.quads.sort_by_key(|quad| quad.order);
|
self.quads.sort_by_key(|quad| quad.order);
|
||||||
@@ -134,6 +293,10 @@ impl Scene {
|
|||||||
self.polychrome_sprites
|
self.polychrome_sprites
|
||||||
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
||||||
self.surfaces.sort_by_key(|surface| surface.order);
|
self.surfaces.sort_by_key(|surface| surface.order);
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
self.diagnostic_quads
|
||||||
|
.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -620,7 +783,7 @@ impl Default for TransformationMatrix {
|
|||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub(crate) struct MonochromeSprite {
|
pub(crate) struct MonochromeSprite {
|
||||||
pub order: DrawOrder,
|
pub order: DrawOrder,
|
||||||
pub pad: u32, // align to 8 bytes
|
pub pad: u32,
|
||||||
pub bounds: Bounds<ScaledPixels>,
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
pub content_mask: ContentMask<ScaledPixels>,
|
pub content_mask: ContentMask<ScaledPixels>,
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
@@ -638,7 +801,7 @@ impl From<MonochromeSprite> for Primitive {
|
|||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub(crate) struct PolychromeSprite {
|
pub(crate) struct PolychromeSprite {
|
||||||
pub order: DrawOrder,
|
pub order: DrawOrder,
|
||||||
pub pad: u32, // align to 8 bytes
|
pub pad: u32,
|
||||||
pub grayscale: bool,
|
pub grayscale: bool,
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
pub bounds: Bounds<ScaledPixels>,
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
|
|||||||
@@ -506,6 +506,10 @@ impl HitboxId {
|
|||||||
///
|
///
|
||||||
/// See [`Hitbox::is_hovered`] for details.
|
/// See [`Hitbox::is_hovered`] for details.
|
||||||
pub fn is_hovered(self, window: &Window) -> bool {
|
pub fn is_hovered(self, window: &Window) -> bool {
|
||||||
|
// If this hitbox has captured the pointer, it's always considered hovered
|
||||||
|
if window.captured_hitbox == Some(self) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
let hit_test = &window.mouse_hit_test;
|
let hit_test = &window.mouse_hit_test;
|
||||||
for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
|
for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
|
||||||
if self == *id {
|
if self == *id {
|
||||||
@@ -760,6 +764,11 @@ impl Frame {
|
|||||||
self.tab_stops.clear();
|
self.tab_stops.clear();
|
||||||
self.focus = None;
|
self.focus = None;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
{
|
||||||
|
self.debug_bounds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
{
|
{
|
||||||
self.next_inspector_instance_ids.clear();
|
self.next_inspector_instance_ids.clear();
|
||||||
@@ -887,6 +896,9 @@ pub struct Window {
|
|||||||
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
|
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
|
||||||
prompt: Option<RenderablePromptHandle>,
|
prompt: Option<RenderablePromptHandle>,
|
||||||
pub(crate) client_inset: Option<Pixels>,
|
pub(crate) client_inset: Option<Pixels>,
|
||||||
|
/// The hitbox that has captured the pointer, if any.
|
||||||
|
/// While captured, mouse events route to this hitbox regardless of hit testing.
|
||||||
|
captured_hitbox: Option<HitboxId>,
|
||||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
inspector: Option<Entity<Inspector>>,
|
inspector: Option<Entity<Inspector>>,
|
||||||
}
|
}
|
||||||
@@ -1311,6 +1323,7 @@ impl Window {
|
|||||||
prompt: None,
|
prompt: None,
|
||||||
client_inset: None,
|
client_inset: None,
|
||||||
image_cache_stack: Vec::new(),
|
image_cache_stack: Vec::new(),
|
||||||
|
captured_hitbox: None,
|
||||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
inspector: None,
|
inspector: None,
|
||||||
})
|
})
|
||||||
@@ -1994,6 +2007,26 @@ impl Window {
|
|||||||
self.mouse_position
|
self.mouse_position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up
|
||||||
|
/// events will be routed to listeners that check this hitbox's `is_hovered` status,
|
||||||
|
/// regardless of actual hit testing. This enables drag operations that continue
|
||||||
|
/// even when the pointer moves outside the element's bounds.
|
||||||
|
///
|
||||||
|
/// The capture is automatically released on mouse up.
|
||||||
|
pub fn capture_pointer(&mut self, hitbox_id: HitboxId) {
|
||||||
|
self.captured_hitbox = Some(hitbox_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases any active pointer capture.
|
||||||
|
pub fn release_pointer(&mut self) {
|
||||||
|
self.captured_hitbox = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the hitbox that has captured the pointer, if any.
|
||||||
|
pub fn captured_hitbox(&self) -> Option<HitboxId> {
|
||||||
|
self.captured_hitbox
|
||||||
|
}
|
||||||
|
|
||||||
/// The current state of the keyboard's modifiers
|
/// The current state of the keyboard's modifiers
|
||||||
pub fn modifiers(&self) -> Modifiers {
|
pub fn modifiers(&self) -> Modifiers {
|
||||||
self.modifiers
|
self.modifiers
|
||||||
@@ -2966,6 +2999,41 @@ impl Window {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
/// Record a named diagnostic quad for test/debug snapshots.
|
||||||
|
///
|
||||||
|
/// This is intended for debugging and asserting against imperative painting logic. The
|
||||||
|
/// recorded quad does not affect rendering; it is captured alongside the rendered scene and
|
||||||
|
/// exposed via `scene_snapshot()`.
|
||||||
|
pub fn record_diagnostic_quad(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<SharedString>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
color: Option<Hsla>,
|
||||||
|
) {
|
||||||
|
self.invalidator.debug_assert_paint();
|
||||||
|
|
||||||
|
let scale_factor = self.scale_factor();
|
||||||
|
self.next_frame.scene.diagnostic_quads.push(crate::test_scene::DiagnosticQuad {
|
||||||
|
name: name.into(),
|
||||||
|
bounds: bounds.scale(scale_factor),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
#[inline]
|
||||||
|
/// Record a named diagnostic quad for test/debug snapshots.
|
||||||
|
///
|
||||||
|
/// This is a no-op unless tests or the `test-support` feature are enabled.
|
||||||
|
pub fn record_diagnostic_quad(
|
||||||
|
&mut self,
|
||||||
|
_name: impl Into<SharedString>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_color: Option<Hsla>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||||
///
|
///
|
||||||
/// This method should only be called as part of the paint phase of element drawing.
|
/// This method should only be called as part of the paint phase of element drawing.
|
||||||
@@ -3850,6 +3918,11 @@ impl Window {
|
|||||||
self.refresh();
|
self.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-release pointer capture on mouse up
|
||||||
|
if event.is::<MouseUpEvent>() && self.captured_hitbox.is_some() {
|
||||||
|
self.captured_hitbox = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {
|
fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {
|
||||||
|
|||||||
Reference in New Issue
Block a user