Support custom renderers in iced_test through renderer::Headless trait

This commit is contained in:
Héctor Ramón Jiménez 2024-12-14 03:49:24 +01:00
parent 6572909ab5
commit 2cf4abf25b
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
12 changed files with 191 additions and 80 deletions

View file

@ -40,6 +40,7 @@ mod pixels;
mod point; mod point;
mod rectangle; mod rectangle;
mod rotation; mod rotation;
mod settings;
mod shadow; mod shadow;
mod shell; mod shell;
mod size; mod size;
@ -67,6 +68,7 @@ pub use point::Point;
pub use rectangle::Rectangle; pub use rectangle::Rectangle;
pub use renderer::Renderer; pub use renderer::Renderer;
pub use rotation::Rotation; pub use rotation::Rotation;
pub use settings::Settings;
pub use shadow::Shadow; pub use shadow::Shadow;
pub use shell::Shell; pub use shell::Shell;
pub use size::Size; pub use size::Size;

View file

@ -3,7 +3,8 @@
mod null; mod null;
use crate::{ use crate::{
Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector, Background, Border, Color, Font, Pixels, Rectangle, Shadow, Size,
Transformation, Vector,
}; };
/// A component that can be used by widgets to draw themselves on a screen. /// A component that can be used by widgets to draw themselves on a screen.
@ -100,3 +101,19 @@ impl Default for Style {
} }
} }
} }
/// A headless renderer is a renderer that can render offscreen without
/// a window nor a compositor.
pub trait Headless {
/// Creates a new [`Headless`] renderer;
fn new(default_font: Font, default_text_size: Pixels) -> Self;
/// Draws offscreen into a screenshot, returning a collection of
/// bytes representing the rendered pixels in RGBA order.
fn screenshot(
&mut self,
size: Size<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8>;
}

View file

@ -48,12 +48,3 @@ impl Default for Settings {
} }
} }
} }
impl From<Settings> for iced_winit::Settings {
fn from(settings: Settings) -> iced_winit::Settings {
iced_winit::Settings {
id: settings.id,
fonts: settings.fonts,
}
}
}

View file

@ -1 +1 @@
b41c73d214894bf5f94f787e5f265cff6500822b2d4a29a4ac0c847a71db7123 a7c2ac4b57f84416812e2134e48fe34db55a757d9176beedf5854a2f69532e32

View file

@ -590,16 +590,25 @@ impl SavedState {
mod tests { mod tests {
use super::*; use super::*;
use iced_test::{interface, load_font, selector, Error}; use iced::Settings;
use iced_test::{selector, Error, Simulator};
fn simulator(todos: &Todos) -> Simulator<Message> {
Simulator::with_settings(
Settings {
fonts: vec![Todos::ICON_FONT.into()],
..Settings::default()
},
todos.view(),
)
}
#[test] #[test]
fn it_creates_a_new_task() -> Result<(), Error> { fn it_creates_a_new_task() -> Result<(), Error> {
load_font(Todos::ICON_FONT)?;
let (mut todos, _command) = Todos::new(); let (mut todos, _command) = Todos::new();
let _command = todos.update(Message::Loaded(Err(LoadError::File))); let _command = todos.update(Message::Loaded(Err(LoadError::File)));
let mut ui = interface(todos.view()); let mut ui = simulator(&todos);
let _input = ui.click("new-task")?; let _input = ui.click("new-task")?;
ui.typewrite("Create the universe"); ui.typewrite("Create the universe");
@ -609,7 +618,7 @@ mod tests {
let _command = todos.update(message); let _command = todos.update(message);
} }
let mut ui = interface(todos.view()); let mut ui = simulator(&todos);
let _ = ui.find(selector::text("Create the universe"))?; let _ = ui.find(selector::text("Create the universe"))?;
let snapshot = ui.snapshot()?; let snapshot = ui.snapshot()?;

View file

@ -146,7 +146,8 @@ impl Text {
/// The regular variant of the [Fira Sans] font. /// The regular variant of the [Fira Sans] font.
/// ///
/// It is loaded as part of the default fonts in Wasm builds. /// It is loaded as part of the default fonts when the `fira-sans`
/// feature is enabled.
/// ///
/// [Fira Sans]: https://mozilla.github.io/Fira/ /// [Fira Sans]: https://mozilla.github.io/Fira/
#[cfg(feature = "fira-sans")] #[cfg(feature = "fira-sans")]

View file

@ -23,6 +23,9 @@ pub type Compositor = renderer::Compositor;
#[cfg(all(feature = "wgpu", feature = "tiny-skia"))] #[cfg(all(feature = "wgpu", feature = "tiny-skia"))]
mod renderer { mod renderer {
use crate::core::renderer;
use crate::core::{Color, Font, Pixels, Size};
pub type Renderer = crate::fallback::Renderer< pub type Renderer = crate::fallback::Renderer<
iced_wgpu::Renderer, iced_wgpu::Renderer,
iced_tiny_skia::Renderer, iced_tiny_skia::Renderer,
@ -32,6 +35,31 @@ mod renderer {
iced_wgpu::window::Compositor, iced_wgpu::window::Compositor,
iced_tiny_skia::window::Compositor, iced_tiny_skia::window::Compositor,
>; >;
impl renderer::Headless for Renderer {
fn new(default_font: Font, default_text_size: Pixels) -> Self {
Self::Secondary(iced_tiny_skia::Renderer::new(
default_font,
default_text_size,
))
}
fn screenshot(
&mut self,
size: Size<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8> {
match self {
crate::fallback::Renderer::Primary(_) => unreachable!(
"iced_wgpu does not support headless mode yet!"
),
crate::fallback::Renderer::Secondary(renderer) => {
renderer.screenshot(size, scale_factor, background_color)
}
}
}
}
} }
#[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))] #[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))]

View file

@ -491,7 +491,6 @@ mod program;
pub mod application; pub mod application;
pub mod daemon; pub mod daemon;
pub mod settings;
pub mod time; pub mod time;
pub mod window; pub mod window;
@ -506,8 +505,8 @@ pub use crate::core::padding;
pub use crate::core::theme; pub use crate::core::theme;
pub use crate::core::{ pub use crate::core::{
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings,
Theme, Transformation, Vector, Shadow, Size, Theme, Transformation, Vector,
}; };
pub use crate::runtime::exit; pub use crate::runtime::exit;
pub use iced_futures::Subscription; pub use iced_futures::Subscription;
@ -626,7 +625,6 @@ pub use executor::Executor;
pub use font::Font; pub use font::Font;
pub use program::Program; pub use program::Program;
pub use renderer::Renderer; pub use renderer::Renderer;
pub use settings::Settings;
pub use task::Task; pub use task::Task;
#[doc(inline)] #[doc(inline)]

View file

@ -15,10 +15,9 @@ workspace = true
[dependencies] [dependencies]
iced_runtime.workspace = true iced_runtime.workspace = true
iced_tiny_skia.workspace = true
iced_renderer.workspace = true iced_renderer.workspace = true
iced_renderer.features = ["tiny-skia", "fira-sans"] iced_renderer.features = ["fira-sans"]
png.workspace = true png.workspace = true
sha2.workspace = true sha2.workspace = true

View file

@ -7,7 +7,6 @@ pub use selector::Selector;
use iced_renderer as renderer; use iced_renderer as renderer;
use iced_runtime as runtime; use iced_runtime as runtime;
use iced_runtime::core; use iced_runtime::core;
use iced_tiny_skia as tiny_skia;
use crate::core::clipboard; use crate::core::clipboard;
use crate::core::keyboard; use crate::core::keyboard;
@ -16,8 +15,7 @@ use crate::core::theme;
use crate::core::time; use crate::core::time;
use crate::core::widget; use crate::core::widget;
use crate::core::window; use crate::core::window;
use crate::core::{Element, Event, Font, Pixels, Rectangle, Size, SmolStr}; use crate::core::{Element, Event, Font, Rectangle, Settings, Size, SmolStr};
use crate::renderer::Renderer;
use crate::runtime::user_interface; use crate::runtime::user_interface;
use crate::runtime::UserInterface; use crate::runtime::UserInterface;
@ -27,32 +25,17 @@ use std::io;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
pub fn interface<'a, Message, Theme>( pub fn simulator<'a, Message, Theme, Renderer>(
element: impl Into<Element<'a, Message, Theme, Renderer>>, element: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Interface<'a, Message, Theme, Renderer> { ) -> Simulator<'a, Message, Theme, Renderer>
let size = Size::new(512.0, 512.0); where
Theme: Default + theme::Base,
let mut renderer = Renderer::Secondary(tiny_skia::Renderer::new( Renderer: core::Renderer + core::renderer::Headless,
Font::with_name("Fira Sans"), {
Pixels(16.0), Simulator::new(element)
));
let raw = UserInterface::build(
element,
size,
user_interface::Cache::default(),
&mut renderer,
);
Interface {
raw,
renderer,
size,
messages: Vec::new(),
}
} }
pub fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> { fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> {
renderer::graphics::text::font_system() renderer::graphics::text::font_system()
.write() .write()
.expect("Write to font system") .expect("Write to font system")
@ -61,21 +44,78 @@ pub fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> {
Ok(()) Ok(())
} }
pub struct Interface<'a, Message, Theme, Renderer> { pub struct Simulator<
'a,
Message,
Theme = core::Theme,
Renderer = renderer::Renderer,
> {
raw: UserInterface<'a, Message, Theme, Renderer>, raw: UserInterface<'a, Message, Theme, Renderer>,
renderer: Renderer, renderer: Renderer,
size: Size, window_size: Size,
messages: Vec<Message>, messages: Vec<Message>,
} }
pub struct Target { pub struct Target {
bounds: Rectangle, pub bounds: Rectangle,
} }
impl<Message, Theme> Interface<'_, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer>
where where
Theme: Default + theme::Base, Theme: Default + theme::Base,
Renderer: core::Renderer + core::renderer::Headless,
{ {
pub fn new(
element: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self::with_settings(Settings::default(), element)
}
pub fn with_settings(
settings: Settings,
element: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self::with_settings_and_size(
settings,
window::Settings::default().size,
element,
)
}
pub fn with_settings_and_size(
settings: Settings,
window_size: impl Into<Size>,
element: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
let window_size = window_size.into();
let default_font = match settings.default_font {
Font::DEFAULT => Font::with_name("Fira Sans"),
_ => settings.default_font,
};
for font in settings.fonts {
load_font(font).expect("Font must be valid");
}
let mut renderer =
Renderer::new(default_font, settings.default_text_size);
let raw = UserInterface::build(
element,
window_size,
user_interface::Cache::default(),
&mut renderer,
);
Simulator {
raw,
renderer,
window_size,
messages: Vec::new(),
}
}
pub fn find( pub fn find(
&mut self, &mut self,
selector: impl Into<Selector>, selector: impl Into<Selector>,
@ -301,34 +341,26 @@ where
mouse::Cursor::Unavailable, mouse::Cursor::Unavailable,
); );
if let Renderer::Secondary(renderer) = &mut self.renderer { let scale_factor = 2.0;
let scale_factor = 2.0;
let viewport = renderer::graphics::Viewport::with_physical_size( let physical_size = Size::new(
Size::new( (self.window_size.width * scale_factor).round() as u32,
(self.size.width * scale_factor).round() as u32, (self.window_size.height * scale_factor).round() as u32,
(self.size.height * scale_factor).round() as u32, );
),
let rgba = self.renderer.screenshot(
physical_size,
scale_factor,
base.background_color,
);
Ok(Snapshot {
screenshot: window::Screenshot::new(
rgba,
physical_size,
f64::from(scale_factor), f64::from(scale_factor),
); ),
})
let rgba = tiny_skia::window::compositor::screenshot::<&str>(
renderer,
&viewport,
base.background_color,
&[],
);
Ok(Snapshot {
screenshot: window::Screenshot::new(
rgba,
viewport.physical_size(),
viewport.scale_factor(),
),
})
} else {
unreachable!()
}
} }
pub fn into_messages(self) -> impl IntoIterator<Item = Message> { pub fn into_messages(self) -> impl IntoIterator<Item = Message> {

View file

@ -29,7 +29,7 @@ pub use geometry::Geometry;
use crate::core::renderer; use crate::core::renderer;
use crate::core::{ use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Transformation, Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
}; };
use crate::engine::Engine; use crate::engine::Engine;
use crate::graphics::compositor; use crate::graphics::compositor;
@ -405,3 +405,26 @@ impl core::svg::Renderer for Renderer {
impl compositor::Default for Renderer { impl compositor::Default for Renderer {
type Compositor = window::Compositor; type Compositor = window::Compositor;
} }
impl renderer::Headless for Renderer {
fn new(default_font: Font, default_text_size: Pixels) -> Self {
Self::new(default_font, default_text_size)
}
fn screenshot(
&mut self,
size: Size<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8> {
let viewport =
Viewport::with_physical_size(size, f64::from(scale_factor));
window::compositor::screenshot::<&str>(
self,
&viewport,
background_color,
&[],
)
}
}

View file

@ -1,4 +1,6 @@
//! Configure your application. //! Configure your application.
use crate::core;
use std::borrow::Cow; use std::borrow::Cow;
/// The settings of an application. /// The settings of an application.
@ -13,3 +15,12 @@ pub struct Settings {
/// The fonts to load on boot. /// The fonts to load on boot.
pub fonts: Vec<Cow<'static, [u8]>>, pub fonts: Vec<Cow<'static, [u8]>>,
} }
impl From<core::Settings> for Settings {
fn from(settings: core::Settings) -> Self {
Self {
id: settings.id,
fonts: settings.fonts,
}
}
}