diff --git a/core/src/image.rs b/core/src/image.rs index 9540140c..be9eb1d8 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,8 +1,6 @@ //! Load and draw raster graphics. -pub use bytes::Bytes; - use crate::border; -use crate::{Radians, Rectangle, Size}; +use crate::{Bytes, Radians, Rectangle, Size}; use rustc_hash::FxHasher; diff --git a/core/src/lib.rs b/core/src/lib.rs index e75ef2a7..06d5d19e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -83,6 +83,7 @@ pub use transformation::Transformation; pub use vector::Vector; pub use widget::Widget; +pub use bytes::Bytes; pub use smol_str::SmolStr; /// A function that can _never_ be called. diff --git a/core/src/window/screenshot.rs b/core/src/window/screenshot.rs index 5bb9334b..0bd03dc4 100644 --- a/core/src/window/screenshot.rs +++ b/core/src/window/screenshot.rs @@ -1,7 +1,6 @@ //! Take screenshots of a window. -use crate::{Rectangle, Size}; +use crate::{Bytes, Rectangle, Size}; -use bytes::Bytes; use std::fmt::{Debug, Formatter}; /// Data of a screenshot, captured with `window::screenshot()`. @@ -9,8 +8,8 @@ use std::fmt::{Debug, Formatter}; /// The `bytes` of this screenshot will always be ordered as `RGBA` in the `sRGB` color space. #[derive(Clone)] pub struct Screenshot { - /// The bytes of the [`Screenshot`]. - pub bytes: Bytes, + /// The RGBA bytes of the [`Screenshot`]. + pub rgba: Bytes, /// The size of the [`Screenshot`] in physical pixels. pub size: Size, /// The scale factor of the [`Screenshot`]. This can be useful when converting between widget @@ -23,7 +22,7 @@ impl Debug for Screenshot { write!( f, "Screenshot: {{ \n bytes: {}\n scale: {}\n size: {:?} }}", - self.bytes.len(), + self.rgba.len(), self.scale_factor, self.size ) @@ -33,12 +32,12 @@ impl Debug for Screenshot { impl Screenshot { /// Creates a new [`Screenshot`]. pub fn new( - bytes: impl Into, + rgba: impl Into, size: Size, scale_factor: f32, ) -> Self { Self { - bytes: bytes.into(), + rgba: rgba.into(), size, scale_factor, } @@ -65,7 +64,7 @@ impl Screenshot { let column_range = region.x as usize * PIXEL_SIZE ..(region.x + region.width) as usize * PIXEL_SIZE; - let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold( + let chopped = self.rgba.chunks(bytes_per_row).enumerate().fold( vec![], |mut acc, (row, bytes)| { if row_range.contains(&row) { @@ -77,7 +76,7 @@ impl Screenshot { ); Ok(Self { - bytes: Bytes::from(chopped), + rgba: Bytes::from(chopped), size: Size::new(region.width, region.height), scale_factor: self.scale_factor, }) @@ -86,13 +85,13 @@ impl Screenshot { impl AsRef<[u8]> for Screenshot { fn as_ref(&self) -> &[u8] { - &self.bytes + &self.rgba } } impl From for Bytes { fn from(screenshot: Screenshot) -> Self { - screenshot.bytes + screenshot.rgba } } diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 100eca94..86827858 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -59,7 +59,7 @@ impl Example { image::Handle::from_rgba( screenshot.size.width, screenshot.size.height, - screenshot.bytes, + screenshot.rgba, ), )); } @@ -105,7 +105,7 @@ impl Example { image::Handle::from_rgba( screenshot.size.width, screenshot.size.height, - screenshot.bytes, + screenshot.rgba, ), )); self.crop_error = None; @@ -243,7 +243,7 @@ async fn save_to_png(screenshot: Screenshot) -> Result { tokio::task::spawn_blocking(move || { img::save_buffer( &path, - &screenshot.bytes, + &screenshot.rgba, screenshot.size.width, screenshot.size.height, ColorType::Rgba8, diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6dee5b66..2510cf5b 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -20,7 +20,8 @@ pub fn main() -> iced::Result { application().run() } -fn application() -> Application> { +fn application() -> Application> +{ iced::application(Todos::new, Todos::update, Todos::view) .subscription(Todos::subscription) .title(Todos::title) diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 70acc7e0..c069fbf9 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,9 +2,9 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::Rectangle; use crate::core::image; use crate::core::svg; +use crate::core::{Bytes, Rectangle}; /// A raster or vector image. #[allow(missing_docs)] @@ -39,7 +39,7 @@ impl Image { /// An image buffer. #[cfg(feature = "image")] -pub type Buffer = ::image::ImageBuffer<::image::Rgba, image::Bytes>; +pub type Buffer = ::image::ImageBuffer<::image::Rgba, Bytes>; #[cfg(feature = "image")] /// Tries to load an image by its [`Handle`]. @@ -127,11 +127,7 @@ pub fn load(handle: &image::Handle) -> Result { let rgba = operation.perform(image).into_rgba8(); - ( - rgba.width(), - rgba.height(), - image::Bytes::from(rgba.into_raw()), - ) + (rgba.width(), rgba.height(), Bytes::from(rgba.into_raw())) } image::Handle::Bytes(_, bytes) => { let image = ::image::load_from_memory(bytes).map_err(to_error)?; @@ -143,11 +139,7 @@ pub fn load(handle: &image::Handle) -> Result { let rgba = operation.perform(image).into_rgba8(); - ( - rgba.width(), - rgba.height(), - image::Bytes::from(rgba.into_raw()), - ) + (rgba.width(), rgba.height(), Bytes::from(rgba.into_raw())) } image::Handle::Rgba { width, diff --git a/test/src/emulator.rs b/test/src/emulator.rs index cc013a45..bdb2415c 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -2,8 +2,10 @@ use crate::core; use crate::core::mouse; use crate::core::renderer; +use crate::core::time::Instant; use crate::core::widget; -use crate::core::{Element, Point, Size}; +use crate::core::window; +use crate::core::{Bytes, Element, Point, Size}; use crate::instruction; use crate::program; use crate::program::Program; @@ -15,7 +17,6 @@ use crate::runtime::futures::subscription; use crate::runtime::futures::{Executor, Runtime}; use crate::runtime::task; use crate::runtime::user_interface; -use crate::runtime::window; use crate::runtime::{Task, UserInterface}; use crate::{Instruction, Selector}; @@ -209,50 +210,55 @@ impl Emulator

{ // TODO dbg!(action); } - runtime::Action::Window(action) => match action { - window::Action::Open(id, _settings, sender) => { - self.window = id; + runtime::Action::Window(action) => { + use crate::runtime::window; - let _ = sender.send(self.window); - } - window::Action::GetOldest(sender) - | window::Action::GetLatest(sender) => { - let _ = sender.send(Some(self.window)); - } - window::Action::GetSize(id, sender) => { - if id == self.window { - let _ = sender.send(self.size); + match action { + window::Action::Open(id, _settings, sender) => { + self.window = id; + + let _ = sender.send(self.window); + } + window::Action::GetOldest(sender) + | window::Action::GetLatest(sender) => { + let _ = sender.send(Some(self.window)); + } + window::Action::GetSize(id, sender) => { + if id == self.window { + let _ = sender.send(self.size); + } + } + window::Action::GetMaximized(id, sender) => { + if id == self.window { + let _ = sender.send(false); + } + } + window::Action::GetMinimized(id, sender) => { + if id == self.window { + let _ = sender.send(None); + } + } + window::Action::GetPosition(id, sender) => { + if id == self.window { + let _ = sender.send(Some(Point::ORIGIN)); + } + } + window::Action::GetScaleFactor(id, sender) => { + if id == self.window { + let _ = sender.send(1.0); + } + } + window::Action::GetMode(id, sender) => { + if id == self.window { + let _ = + sender.send(core::window::Mode::Windowed); + } + } + _ => { + // Ignored } } - window::Action::GetMaximized(id, sender) => { - if id == self.window { - let _ = sender.send(false); - } - } - window::Action::GetMinimized(id, sender) => { - if id == self.window { - let _ = sender.send(None); - } - } - window::Action::GetPosition(id, sender) => { - if id == self.window { - let _ = sender.send(Some(Point::ORIGIN)); - } - } - window::Action::GetScaleFactor(id, sender) => { - if id == self.window { - let _ = sender.send(1.0); - } - } - window::Action::GetMode(id, sender) => { - if id == self.window { - let _ = sender.send(core::window::Mode::Windowed); - } - } - _ => { - // Ignored - } - }, + } runtime::Action::System(action) => { // TODO dbg!(action); @@ -435,6 +441,62 @@ impl Emulator

{ program.theme(&self.state, self.window) } + /// Takes a [`window::Screenshot`] of the current state of the [`Emulator`]. + pub fn screenshot( + &mut self, + program: &P, + theme: &P::Theme, + scale_factor: f32, + ) -> window::Screenshot { + use core::renderer::Headless; + + let style = program.style(&self.state, theme); + + let mut user_interface = UserInterface::build( + program.view(&self.state, self.window), + self.size, + self.cache.take().unwrap(), + &mut self.renderer, + ); + + // TODO: Nested redraws! + let _ = user_interface.update( + &[core::Event::Window(window::Event::RedrawRequested( + Instant::now(), + ))], + mouse::Cursor::Unavailable, + &mut self.renderer, + &mut self.clipboard, + &mut Vec::new(), + ); + + user_interface.draw( + &mut self.renderer, + theme, + &renderer::Style { + text_color: style.text_color, + }, + mouse::Cursor::Unavailable, + ); + + let physical_size = Size::new( + (self.size.width * scale_factor).round() as u32, + (self.size.height * scale_factor).round() as u32, + ); + + let rgba = self.renderer.screenshot( + physical_size, + scale_factor, + style.background_color, + ); + + window::Screenshot { + rgba: Bytes::from(rgba), + size: physical_size, + scale_factor, + } + } + /// Turns the [`Emulator`] into its internal state. pub fn into_state(self) -> (P::State, core::window::Id) { (self.state, self.window) diff --git a/test/src/lib.rs b/test/src/lib.rs index ba5c8c9c..b13e779e 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -104,6 +104,10 @@ pub use instruction::Instruction; pub use selector::Selector; pub use simulator::{Simulator, simulator}; +use crate::core::Size; +use crate::core::time::{Duration, Instant}; +use crate::core::window; + use std::path::Path; /// Runs an [`Ice`] test suite for the given [`Program`](program::Program). @@ -213,3 +217,50 @@ pub fn run( Ok(()) } + +/// Takes a screenshot of the given [`Program`](program::Program) with the given theme, viewport, +/// and scale factor after running it for the given [`Duration`]. +pub fn screenshot( + program: P, + theme: &P::Theme, + viewport: impl Into, + scale_factor: f32, + duration: Duration, +) -> window::Screenshot { + use crate::runtime::futures::futures::channel::mpsc; + + let (sender, mut receiver) = mpsc::channel(100); + + let mut emulator = Emulator::new( + sender, + &program, + emulator::Mode::Immediate, + viewport.into(), + ); + + let start = Instant::now(); + + loop { + if let Some(event) = receiver.try_next().ok().flatten() { + match event { + emulator::Event::Action(action) => { + emulator.perform(&program, action); + } + emulator::Event::Failed(_) => { + unreachable!( + "no instructions should be executed during a screenshot" + ); + } + emulator::Event::Ready => {} + } + } + + if start.elapsed() >= duration { + break; + } + + std::thread::sleep(Duration::from_millis(1)); + } + + emulator.screenshot(&program, theme, scale_factor) +} diff --git a/test/src/simulator.rs b/test/src/simulator.rs index b0a9a5d1..010c521a 100644 --- a/test/src/simulator.rs +++ b/test/src/simulator.rs @@ -276,7 +276,7 @@ impl Snapshot { let mut bytes = vec![0; n]; let info = reader.next_frame(&mut bytes)?; - Ok(self.screenshot.bytes == bytes[..info.buffer_size()]) + Ok(self.screenshot.rgba == bytes[..info.buffer_size()]) } else { if let Some(directory) = path.parent() { fs::create_dir_all(directory)?; @@ -292,7 +292,7 @@ impl Snapshot { encoder.set_color(png::ColorType::Rgba); let mut writer = encoder.write_header()?; - writer.write_image_data(&self.screenshot.bytes)?; + writer.write_image_data(&self.screenshot.rgba)?; writer.finish()?; Ok(true) @@ -311,7 +311,7 @@ impl Snapshot { let hash = { let mut hasher = Sha256::new(); - hasher.update(&self.screenshot.bytes); + hasher.update(&self.screenshot.rgba); format!("{:x}", hasher.finalize()) }; diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs index 111e2216..5e8059e8 100644 --- a/wgpu/src/image/cache.rs +++ b/wgpu/src/image/cache.rs @@ -410,6 +410,7 @@ fn load_image<'a>( #[cfg(all(feature = "image", not(target_arch = "wasm32")))] mod worker { + use crate::core::Bytes; use crate::core::image; use crate::graphics::Shell; use crate::image::atlas::{self, Atlas}; @@ -505,7 +506,7 @@ mod worker { Load(image::Handle), Upload { handle: image::Handle, - rgba: image::Bytes, + rgba: Bytes, width: u32, height: u32, }, @@ -580,7 +581,7 @@ mod worker { handle: image::Handle, width: u32, height: u32, - rgba: image::Bytes, + rgba: Bytes, callback: fn(&Shell), ) { let mut encoder = self.device.create_command_encoder(