From 2cf4abf25bb5702635c19a22353399db8cef7be3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 14 Dec 2024 03:49:24 +0100 Subject: Support custom renderers in `iced_test` through `renderer::Headless` trait --- core/src/lib.rs | 2 + core/src/renderer.rs | 19 ++- core/src/settings.rs | 50 ++++++++ examples/todos/snapshots/creates_a_new_task.sha256 | 2 +- examples/todos/src/main.rs | 19 ++- graphics/src/text.rs | 3 +- renderer/src/lib.rs | 28 ++++ src/lib.rs | 6 +- src/settings.rs | 59 --------- test/Cargo.toml | 3 +- test/src/lib.rs | 142 +++++++++++++-------- tiny_skia/src/lib.rs | 25 +++- winit/src/settings.rs | 11 ++ 13 files changed, 240 insertions(+), 129 deletions(-) create mode 100644 core/src/settings.rs delete mode 100644 src/settings.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index df599f45..645f7a90 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -40,6 +40,7 @@ mod pixels; mod point; mod rectangle; mod rotation; +mod settings; mod shadow; mod shell; mod size; @@ -67,6 +68,7 @@ pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; pub use rotation::Rotation; +pub use settings::Settings; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 6684517f..68e070e8 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -3,7 +3,8 @@ mod null; 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. @@ -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, + scale_factor: f32, + background_color: Color, + ) -> Vec; +} diff --git a/core/src/settings.rs b/core/src/settings.rs new file mode 100644 index 00000000..36bbb699 --- /dev/null +++ b/core/src/settings.rs @@ -0,0 +1,50 @@ +//! Configure your application. +use crate::{Font, Pixels}; + +use std::borrow::Cow; + +/// The settings of an iced program. +#[derive(Debug, Clone)] +pub struct Settings { + /// The identifier of the application. + /// + /// If provided, this identifier may be used to identify the application or + /// communicate with it through the windowing system. + pub id: Option, + + /// The fonts to load on boot. + pub fonts: Vec>, + + /// The default [`Font`] to be used. + /// + /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). + pub default_font: Font, + + /// The text size that will be used by default. + /// + /// The default value is `16.0`. + pub default_text_size: Pixels, + + /// If set to true, the renderer will try to perform antialiasing for some + /// primitives. + /// + /// Enabling it can produce a smoother result in some widgets, like the + /// [`Canvas`], at a performance cost. + /// + /// By default, it is disabled. + /// + /// [`Canvas`]: crate::widget::Canvas + pub antialiasing: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + id: None, + fonts: Vec::new(), + default_font: Font::default(), + default_text_size: Pixels(16.0), + antialiasing: false, + } + } +} diff --git a/examples/todos/snapshots/creates_a_new_task.sha256 b/examples/todos/snapshots/creates_a_new_task.sha256 index 75d5f2ce..15efcd77 100644 --- a/examples/todos/snapshots/creates_a_new_task.sha256 +++ b/examples/todos/snapshots/creates_a_new_task.sha256 @@ -1 +1 @@ -b41c73d214894bf5f94f787e5f265cff6500822b2d4a29a4ac0c847a71db7123 \ No newline at end of file +a7c2ac4b57f84416812e2134e48fe34db55a757d9176beedf5854a2f69532e32 \ No newline at end of file diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 1d4aa8a4..fe23cbc9 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -590,16 +590,25 @@ impl SavedState { mod tests { use super::*; - use iced_test::{interface, load_font, selector, Error}; + use iced::Settings; + use iced_test::{selector, Error, Simulator}; + + fn simulator(todos: &Todos) -> Simulator { + Simulator::with_settings( + Settings { + fonts: vec![Todos::ICON_FONT.into()], + ..Settings::default() + }, + todos.view(), + ) + } #[test] fn it_creates_a_new_task() -> Result<(), Error> { - load_font(Todos::ICON_FONT)?; - let (mut todos, _command) = Todos::new(); 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")?; ui.typewrite("Create the universe"); @@ -609,7 +618,7 @@ mod tests { 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 snapshot = ui.snapshot()?; diff --git a/graphics/src/text.rs b/graphics/src/text.rs index e6f06b61..f954e318 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -146,7 +146,8 @@ impl Text { /// 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/ #[cfg(feature = "fira-sans")] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 220542e1..ee20a458 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -23,6 +23,9 @@ pub type Compositor = renderer::Compositor; #[cfg(all(feature = "wgpu", feature = "tiny-skia"))] mod renderer { + use crate::core::renderer; + use crate::core::{Color, Font, Pixels, Size}; + pub type Renderer = crate::fallback::Renderer< iced_wgpu::Renderer, iced_tiny_skia::Renderer, @@ -32,6 +35,31 @@ mod renderer { iced_wgpu::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, + scale_factor: f32, + background_color: Color, + ) -> Vec { + 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")))] diff --git a/src/lib.rs b/src/lib.rs index d13ee7d0..427e789c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -491,7 +491,6 @@ mod program; pub mod application; pub mod daemon; -pub mod settings; pub mod time; pub mod window; @@ -506,8 +505,8 @@ pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, - Theme, Transformation, Vector, + Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings, + Shadow, Size, Theme, Transformation, Vector, }; pub use crate::runtime::exit; pub use iced_futures::Subscription; @@ -626,7 +625,6 @@ pub use executor::Executor; pub use font::Font; pub use program::Program; pub use renderer::Renderer; -pub use settings::Settings; pub use task::Task; #[doc(inline)] diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index ebac7a86..00000000 --- a/src/settings.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! Configure your application. -use crate::{Font, Pixels}; - -use std::borrow::Cow; - -/// The settings of an iced program. -#[derive(Debug, Clone)] -pub struct Settings { - /// The identifier of the application. - /// - /// If provided, this identifier may be used to identify the application or - /// communicate with it through the windowing system. - pub id: Option, - - /// The fonts to load on boot. - pub fonts: Vec>, - - /// The default [`Font`] to be used. - /// - /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). - pub default_font: Font, - - /// The text size that will be used by default. - /// - /// The default value is `16.0`. - pub default_text_size: Pixels, - - /// If set to true, the renderer will try to perform antialiasing for some - /// primitives. - /// - /// Enabling it can produce a smoother result in some widgets, like the - /// [`Canvas`], at a performance cost. - /// - /// By default, it is disabled. - /// - /// [`Canvas`]: crate::widget::Canvas - pub antialiasing: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - id: None, - fonts: Vec::new(), - default_font: Font::default(), - default_text_size: Pixels(16.0), - antialiasing: false, - } - } -} - -impl From for iced_winit::Settings { - fn from(settings: Settings) -> iced_winit::Settings { - iced_winit::Settings { - id: settings.id, - fonts: settings.fonts, - } - } -} diff --git a/test/Cargo.toml b/test/Cargo.toml index c63a9e14..ff6cb38a 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -15,10 +15,9 @@ workspace = true [dependencies] iced_runtime.workspace = true -iced_tiny_skia.workspace = true iced_renderer.workspace = true -iced_renderer.features = ["tiny-skia", "fira-sans"] +iced_renderer.features = ["fira-sans"] png.workspace = true sha2.workspace = true diff --git a/test/src/lib.rs b/test/src/lib.rs index 232b447e..6c1d6bdc 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -7,7 +7,6 @@ pub use selector::Selector; use iced_renderer as renderer; use iced_runtime as runtime; use iced_runtime::core; -use iced_tiny_skia as tiny_skia; use crate::core::clipboard; use crate::core::keyboard; @@ -16,8 +15,7 @@ use crate::core::theme; use crate::core::time; use crate::core::widget; use crate::core::window; -use crate::core::{Element, Event, Font, Pixels, Rectangle, Size, SmolStr}; -use crate::renderer::Renderer; +use crate::core::{Element, Event, Font, Rectangle, Settings, Size, SmolStr}; use crate::runtime::user_interface; use crate::runtime::UserInterface; @@ -27,32 +25,17 @@ use std::io; use std::path::Path; use std::sync::Arc; -pub fn interface<'a, Message, Theme>( +pub fn simulator<'a, Message, Theme, Renderer>( element: impl Into>, -) -> Interface<'a, Message, Theme, Renderer> { - let size = Size::new(512.0, 512.0); - - let mut renderer = Renderer::Secondary(tiny_skia::Renderer::new( - Font::with_name("Fira Sans"), - Pixels(16.0), - )); - - let raw = UserInterface::build( - element, - size, - user_interface::Cache::default(), - &mut renderer, - ); - - Interface { - raw, - renderer, - size, - messages: Vec::new(), - } +) -> Simulator<'a, Message, Theme, Renderer> +where + Theme: Default + theme::Base, + Renderer: core::Renderer + core::renderer::Headless, +{ + Simulator::new(element) } -pub fn load_font(font: impl Into>) -> Result<(), Error> { +fn load_font(font: impl Into>) -> Result<(), Error> { renderer::graphics::text::font_system() .write() .expect("Write to font system") @@ -61,21 +44,78 @@ pub fn load_font(font: impl Into>) -> Result<(), Error> { 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>, renderer: Renderer, - size: Size, + window_size: Size, messages: Vec, } pub struct Target { - bounds: Rectangle, + pub bounds: Rectangle, } -impl Interface<'_, Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> where Theme: Default + theme::Base, + Renderer: core::Renderer + core::renderer::Headless, { + pub fn new( + element: impl Into>, + ) -> Self { + Self::with_settings(Settings::default(), element) + } + + pub fn with_settings( + settings: Settings, + element: impl Into>, + ) -> Self { + Self::with_settings_and_size( + settings, + window::Settings::default().size, + element, + ) + } + + pub fn with_settings_and_size( + settings: Settings, + window_size: impl Into, + element: impl Into>, + ) -> 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( &mut self, selector: impl Into, @@ -301,34 +341,26 @@ where 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( - Size::new( - (self.size.width * scale_factor).round() as u32, - (self.size.height * scale_factor).round() as u32, - ), - f64::from(scale_factor), - ); + let physical_size = Size::new( + (self.window_size.width * scale_factor).round() as u32, + (self.window_size.height * scale_factor).round() as u32, + ); - let rgba = tiny_skia::window::compositor::screenshot::<&str>( - renderer, - &viewport, - base.background_color, - &[], - ); + let rgba = self.renderer.screenshot( + physical_size, + scale_factor, + base.background_color, + ); - Ok(Snapshot { - screenshot: window::Screenshot::new( - rgba, - viewport.physical_size(), - viewport.scale_factor(), - ), - }) - } else { - unreachable!() - } + Ok(Snapshot { + screenshot: window::Screenshot::new( + rgba, + physical_size, + f64::from(scale_factor), + ), + }) } pub fn into_messages(self) -> impl IntoIterator { diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 758921d4..a42f1de4 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -405,3 +405,26 @@ impl core::svg::Renderer for Renderer { impl compositor::Default for Renderer { 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, + scale_factor: f32, + background_color: Color, + ) -> Vec { + let viewport = + Viewport::with_physical_size(size, f64::from(scale_factor)); + + window::compositor::screenshot::<&str>( + self, + &viewport, + background_color, + &[], + ) + } +} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 78368a04..e2bf8abf 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,4 +1,6 @@ //! Configure your application. +use crate::core; + use std::borrow::Cow; /// The settings of an application. @@ -13,3 +15,12 @@ pub struct Settings { /// The fonts to load on boot. pub fonts: Vec>, } + +impl From for Settings { + fn from(settings: core::Settings) -> Self { + Self { + id: settings.id, + fonts: settings.fonts, + } + } +} -- cgit