Support custom renderers in iced_test through renderer::Headless trait
This commit is contained in:
parent
6572909ab5
commit
2cf4abf25b
12 changed files with 191 additions and 80 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
b41c73d214894bf5f94f787e5f265cff6500822b2d4a29a4ac0c847a71db7123
|
a7c2ac4b57f84416812e2134e48fe34db55a757d9176beedf5854a2f69532e32
|
||||||
|
|
@ -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()?;
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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")))]
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
144
test/src/lib.rs
144
test/src/lib.rs
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue