diff --git a/.gitignore b/.gitignore index e0eb9b7..12c2807 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +tests/_graphics/models **/*.rs.bk Cargo.lock *.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 40091e0..09b80b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ wgpu_glyph = { version = "0.3", optional = true, git = "https://github.com/hecrj [dev-dependencies] rand = "0.6" +env_logger = "0.6" diff --git a/examples/counter.rs b/examples/counter.rs index b520e5c..e6d1d3d 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -56,7 +56,7 @@ impl UserInterface for Counter { type Message = Message; type Renderer = Renderer; - fn react(&mut self, message: Message) { + fn react(&mut self, message: Message, _window: &mut Window) { match message { Message::IncrementPressed => { self.value += 1; diff --git a/examples/gamepad.rs b/examples/gamepad.rs index 0159201..cdc2478 100644 --- a/examples/gamepad.rs +++ b/examples/gamepad.rs @@ -71,7 +71,7 @@ impl UserInterface for GamepadExample { type Message = (); type Renderer = Renderer; - fn react(&mut self, _msg: ()) {} + fn react(&mut self, _msg: (), _window: &mut Window) {} fn layout(&mut self, window: &Window) -> Element<()> { Column::new() diff --git a/examples/image.rs b/examples/image.rs index 327140a..3e69366 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,10 +1,10 @@ use coffee::graphics::{ - Color, Frame, HorizontalAlignment, VerticalAlignment, Window, - WindowSettings, self, + self, Color, Frame, HorizontalAlignment, VerticalAlignment, Window, + WindowSettings, }; use coffee::load::Task; use coffee::ui::{ - Align, Column, Element, Justify, Renderer, Text, UserInterface, Image, + Align, Column, Element, Image, Justify, Renderer, Text, UserInterface, }; use coffee::{Game, Result, Timer}; @@ -27,11 +27,7 @@ impl Game for ImageScreen { fn load(_window: &Window) -> Task { graphics::Image::load("resources/ui.png") - .map(|image| { - ImageScreen { - image, - } - }) + .map(|image| ImageScreen { image }) } fn draw(&mut self, frame: &mut Frame, _timer: &Timer) { @@ -48,8 +44,7 @@ impl UserInterface for ImageScreen { type Message = (); type Renderer = Renderer; - fn react(&mut self, _message: ()) { - } + fn react(&mut self, _message: (), _window: &mut Window) {} fn layout(&mut self, window: &Window) -> Element<()> { Column::new() @@ -65,10 +60,7 @@ impl UserInterface for ImageScreen { .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center), ) - .push( - Image::new(&self.image) - .height(250) - ) + .push(Image::new(&self.image).height(250)) .into() } } diff --git a/examples/input.rs b/examples/input.rs index 49dc0bb..1670d58 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -164,7 +164,7 @@ impl UserInterface for InputExample { type Message = (); type Renderer = Renderer; - fn react(&mut self, _msg: ()) {} + fn react(&mut self, _msg: (), _window: &mut Window) {} fn layout(&mut self, window: &Window) -> Element<()> { let keys = self diff --git a/examples/mesh.rs b/examples/mesh.rs index 91b6cd9..7edee65 100644 --- a/examples/mesh.rs +++ b/examples/mesh.rs @@ -130,7 +130,7 @@ impl UserInterface for Example { type Message = Message; type Renderer = Renderer; - fn react(&mut self, msg: Message) { + fn react(&mut self, msg: Message, _window: &mut Window) { match msg { Message::ShapeSelected(shape) => { self.shape = shape; diff --git a/examples/particles.rs b/examples/particles.rs index 64a6435..cc5294d 100644 --- a/examples/particles.rs +++ b/examples/particles.rs @@ -167,7 +167,7 @@ impl UserInterface for Particles { type Message = Message; type Renderer = Renderer; - fn react(&mut self, msg: Message) { + fn react(&mut self, msg: Message, _window: &mut Window) { match msg { Message::ToggleInterpolation(interpolate) => { self.interpolate = interpolate; diff --git a/examples/progress.rs b/examples/progress.rs index c914fbd..ae13e68 100644 --- a/examples/progress.rs +++ b/examples/progress.rs @@ -4,8 +4,7 @@ use coffee::graphics::{ }; use coffee::load::Task; use coffee::ui::{ - Align, Column, Element, Justify, Renderer, Text, - UserInterface, ProgressBar, + Align, Column, Element, Justify, ProgressBar, Renderer, Text, UserInterface, }; use coffee::{Game, Result, Timer}; @@ -27,9 +26,7 @@ impl Game for Progress { type LoadingScreen = (); fn load(_window: &Window) -> Task { - Task::succeed(|| Progress { - value: 0.0, - }) + Task::succeed(|| Progress { value: 0.0 }) } fn draw(&mut self, frame: &mut Frame, timer: &Timer) { @@ -53,8 +50,7 @@ impl UserInterface for Progress { type Message = (); type Renderer = Renderer; - fn react(&mut self, _message: ()) { - } + fn react(&mut self, _message: (), _window: &mut Window) {} fn layout(&mut self, window: &Window) -> Element<()> { Column::new() @@ -70,10 +66,7 @@ impl UserInterface for Progress { .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center), ) - .push( - ProgressBar::new(self.value) - .width(400), - ) + .push(ProgressBar::new(self.value).width(400)) .into() } } diff --git a/examples/ui.rs b/examples/ui.rs index 7582b45..e366717 100644 --- a/examples/ui.rs +++ b/examples/ui.rs @@ -49,7 +49,7 @@ impl UserInterface for Tour { type Message = Message; type Renderer = Renderer; - fn react(&mut self, event: Message) { + fn react(&mut self, event: Message, _window: &mut Window) { match event { Message::BackPressed => { self.steps.go_back(); diff --git a/src/graphics/backend_gfx/mod.rs b/src/graphics/backend_gfx/mod.rs index 31180fa..8b8b0cb 100644 --- a/src/graphics/backend_gfx/mod.rs +++ b/src/graphics/backend_gfx/mod.rs @@ -110,6 +110,13 @@ impl Gpu { texture::Drawable::new(&mut self.factory, width, height) } + pub(super) fn read_drawable_texture_pixels( + &mut self, + drawable: &texture::Drawable, + ) -> image::DynamicImage { + drawable.read_pixels(&mut self.device, &mut self.factory) + } + pub(super) fn upload_font(&mut self, bytes: &'static [u8]) -> Font { Font::from_bytes(&mut self.factory, bytes) } diff --git a/src/graphics/backend_gfx/texture.rs b/src/graphics/backend_gfx/texture.rs index 98207fc..5cfbbb6 100644 --- a/src/graphics/backend_gfx/texture.rs +++ b/src/graphics/backend_gfx/texture.rs @@ -1,6 +1,8 @@ use image; use gfx::format::{ChannelTyped, SurfaceTyped}; +use gfx::memory::Typed; +use gfx::traits::FactoryExt; use gfx_core::factory::Factory; use gfx_device_gl as gl; @@ -11,7 +13,7 @@ use crate::graphics::Transformation; #[derive(Clone, Debug)] pub struct Texture { - texture: RawTexture, + raw: RawTexture, view: ShaderResource, width: u16, height: u16, @@ -27,7 +29,7 @@ impl Texture { let width = rgba.width() as u16; let height = rgba.height() as u16; - let (texture, view) = create_texture_array( + let (raw, view) = create_texture_array( factory, width, height, @@ -37,7 +39,7 @@ impl Texture { ); Texture { - texture, + raw, view, width, height, @@ -58,7 +60,7 @@ impl Texture { let raw_layers: Vec<&[u8]> = rgba.iter().map(|i| &i[..]).collect(); - let (texture, view) = create_texture_array( + let (raw, view) = create_texture_array( factory, width, height, @@ -68,7 +70,7 @@ impl Texture { ); Texture { - texture, + raw, view, width, height, @@ -77,7 +79,7 @@ impl Texture { } pub(super) fn handle(&self) -> &RawTexture { - &self.texture + &self.raw } pub(super) fn view(&self) -> &ShaderResource { @@ -101,17 +103,18 @@ pub struct Drawable { impl Drawable { pub fn new(factory: &mut gl::Factory, width: u16, height: u16) -> Drawable { - let (texture, view) = create_texture_array( + let (raw, view) = create_texture_array( factory, width, height, None, gfx::memory::Bind::SHADER_RESOURCE - | gfx::memory::Bind::RENDER_TARGET, + | gfx::memory::Bind::RENDER_TARGET + | gfx::memory::Bind::TRANSFER_SRC, ); let texture = Texture { - texture, + raw, view, width, height, @@ -139,6 +142,53 @@ impl Drawable { &self.target } + pub fn read_pixels( + &self, + device: &mut gl::Device, + factory: &mut gl::Factory, + ) -> image::DynamicImage { + let width = self.texture.width(); + let height = self.texture.height(); + + let download = factory + .create_download_buffer::(width as usize * height as usize * 4) + .expect("Create download buffer"); + + let mut encoder: gfx::Encoder = + factory.create_command_buffer().into(); + + encoder + .copy_texture_to_buffer_raw( + &self.texture.raw, + None, + gfx::texture::RawImageInfo { + xoffset: 0, + yoffset: 0, + zoffset: 0, + width, + height, + depth: 0, + format: ::get_format(), + mipmap: 0, + }, + download.raw(), + 0, + ) + .expect("Copy texture to raw buffer"); + + encoder.flush(device); + + let reader = factory.read_mapping(&download).expect("Read mapping"); + + let mut rgba = Vec::with_capacity(width as usize * height as usize * 4); + rgba.extend(reader.into_iter()); + + image::DynamicImage::ImageRgba8( + image::ImageBuffer::from_raw(width as u32, height as u32, rgba) + .expect("Create RGBA8 image"), + ) + } + pub fn render_transformation() -> Transformation { Transformation::nonuniform_scale(Vector::new(1.0, -1.0)) } diff --git a/src/graphics/backend_wgpu/mod.rs b/src/graphics/backend_wgpu/mod.rs index 8681ae8..ddb59aa 100644 --- a/src/graphics/backend_wgpu/mod.rs +++ b/src/graphics/backend_wgpu/mod.rs @@ -108,6 +108,13 @@ impl Gpu { ) } + pub(super) fn read_drawable_texture_pixels( + &mut self, + drawable: &texture::Drawable, + ) -> image::DynamicImage { + drawable.read_pixels(&mut self.device) + } + pub(super) fn upload_font(&mut self, bytes: &'static [u8]) -> Font { Font::from_bytes(&mut self.device, bytes) } diff --git a/src/graphics/backend_wgpu/texture.rs b/src/graphics/backend_wgpu/texture.rs index 863687d..cb24beb 100644 --- a/src/graphics/backend_wgpu/texture.rs +++ b/src/graphics/backend_wgpu/texture.rs @@ -7,7 +7,7 @@ use crate::graphics::Transformation; #[derive(Clone)] pub struct Texture { - texture: Rc, + raw: Rc, view: TargetView, binding: Rc, width: u16, @@ -45,7 +45,7 @@ impl Texture { ); Texture { - texture: Rc::new(texture), + raw: Rc::new(texture), view: Rc::new(view), binding: Rc::new(binding), width, @@ -78,7 +78,7 @@ impl Texture { ); Texture { - texture: Rc::new(texture), + raw: Rc::new(texture), view: Rc::new(view), binding: Rc::new(binding), width, @@ -122,11 +122,13 @@ impl Drawable { width, height, None, - wgpu::TextureUsage::OUTPUT_ATTACHMENT | wgpu::TextureUsage::SAMPLED, + wgpu::TextureUsage::OUTPUT_ATTACHMENT + | wgpu::TextureUsage::SAMPLED + | wgpu::TextureUsage::TRANSFER_SRC, ); let texture = Texture { - texture: Rc::new(texture), + raw: Rc::new(texture), view: Rc::new(view), binding: Rc::new(binding), width, @@ -145,6 +147,83 @@ impl Drawable { self.texture().view() } + pub fn read_pixels( + &self, + device: &mut wgpu::Device, + ) -> image::DynamicImage { + let texture = self.texture(); + + let buffer_size = 4 * texture.width() as u64 * texture.height() as u64; + + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + size: buffer_size, + usage: wgpu::BufferUsage::TRANSFER_DST + | wgpu::BufferUsage::TRANSFER_SRC + | wgpu::BufferUsage::MAP_READ, + }); + + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + todo: 0, + }); + + encoder.copy_texture_to_buffer( + wgpu::TextureCopyView { + texture: &texture.raw, + mip_level: 0, + array_layer: 0, + origin: wgpu::Origin3d { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + wgpu::BufferCopyView { + buffer: &buffer, + offset: 0, + row_pitch: 4 * texture.width() as u32, + image_height: texture.height() as u32, + }, + wgpu::Extent3d { + width: texture.width() as u32, + height: texture.height() as u32, + depth: 1, + }, + ); + + device.get_queue().submit(&[encoder.finish()]); + + use std::cell::RefCell; + + let pixels: Rc>>> = Rc::new(RefCell::new(None)); + let write = pixels.clone(); + + buffer.map_read_async(0, buffer_size, move |result| { + match result { + Ok(mapping) => { + *write.borrow_mut() = Some(mapping.data.to_vec()); + } + Err(_) => { + *write.borrow_mut() = Some(vec![]); + } + }; + }); + + device.poll(true); + + let data = pixels.borrow(); + let bgra = data.clone().unwrap(); + + image::DynamicImage::ImageBgra8( + image::ImageBuffer::from_raw( + texture.width() as u32, + texture.height() as u32, + bgra, + ) + .expect("Create BGRA8 image"), + ) + } + pub fn render_transformation() -> Transformation { Transformation::identity() } diff --git a/src/graphics/canvas.rs b/src/graphics/canvas.rs index c59d971..f715411 100644 --- a/src/graphics/canvas.rs +++ b/src/graphics/canvas.rs @@ -74,6 +74,15 @@ impl Canvas { ))], ); } + + /// Reads the pixels of the [`Canvas`]. + /// + /// _Note:_ This is a very slow operation. + /// + /// [`Canvas`]: struct.Canvas.html + pub fn read_pixels(&self, gpu: &mut Gpu) -> image::DynamicImage { + gpu.read_drawable_texture_pixels(&self.drawable) + } } impl std::fmt::Debug for Canvas { diff --git a/src/load/loading_screen.rs b/src/load/loading_screen.rs index 98e8021..5d60fc5 100644 --- a/src/load/loading_screen.rs +++ b/src/load/loading_screen.rs @@ -74,7 +74,7 @@ pub trait LoadingScreen { task: Task, window: &mut graphics::Window, ) -> Result { - task.run(window, |progress, window| { + task.run_with_window(window, |progress, window| { self.draw(progress, &mut window.frame()); window.swap_buffers(); }) diff --git a/src/load/task.rs b/src/load/task.rs index 1287df5..607706b 100644 --- a/src/load/task.rs +++ b/src/load/task.rs @@ -249,21 +249,24 @@ impl Task { } } - /// Runs a [`Task`] and obtain the produced value. + /// Runs a [`Task`] and obtains the produced value. /// - /// You can provide a function to keep track of [`Progress`]. + /// [`Task`]: struct.Task.html + pub fn run(self, gpu: &mut graphics::Gpu) -> Result { + let mut worker = Worker::Headless(gpu); + + (self.function)(&mut worker) + } + + /// Runs a [`Task`] and obtains the produced value. /// - /// As of now, this method needs a [`Window`] because tasks are mostly - /// meant to be used with loading screens. However, the [`Task`] abstraction - /// is generic enough to be useful in other scenarios and we could work on - /// removing this dependency. If you have a particular use case for them, - /// feel free to [open an issue] detailing it! + /// You can provide a function to keep track of [`Progress`]. /// /// [`Task`]: struct.Task.html /// [`Progress`]: struct.Progress.html /// [`Window`]: ../graphics/window/struct.Window.html /// [open an issue]: https://github.com/hecrj/coffee/issues - pub fn run( + pub(crate) fn run_with_window( self, window: &mut graphics::Window, mut on_progress: F, @@ -271,7 +274,7 @@ impl Task { where F: FnMut(&Progress, &mut graphics::Window) -> (), { - let mut worker = Worker { + let mut worker = Worker::Windowed { window, listener: &mut on_progress, progress: Progress { @@ -293,21 +296,37 @@ impl std::fmt::Debug for Task { } } -pub(crate) struct Worker<'a> { - window: &'a mut graphics::Window, - listener: &'a mut dyn FnMut(&Progress, &mut graphics::Window) -> (), - progress: Progress, +pub(crate) enum Worker<'a> { + Headless(&'a mut graphics::Gpu), + Windowed { + window: &'a mut graphics::Window, + listener: &'a mut dyn FnMut(&Progress, &mut graphics::Window) -> (), + progress: Progress, + }, } impl<'a> Worker<'a> { pub fn gpu(&mut self) -> &mut graphics::Gpu { - self.window.gpu() + match self { + Worker::Headless(gpu) => gpu, + Worker::Windowed { window, .. } => window.gpu(), + } } pub fn notify_progress(&mut self, work: u32) { - self.progress.work_completed += work; - - (self.listener)(&self.progress, self.window); + match self { + Worker::Headless(_) => {} + Worker::Windowed { + progress, + window, + listener, + .. + } => { + progress.work_completed += work; + + listener(&progress, window); + } + }; } pub fn with_stage( @@ -315,13 +334,24 @@ impl<'a> Worker<'a> { title: String, f: &Box) -> T>, ) -> T { - self.progress.stages.push(title); - self.notify_progress(0); + match self { + Worker::Headless(_) => f(self), + Worker::Windowed { .. } => { + if let Worker::Windowed { progress, .. } = self { + progress.stages.push(title); + } + + self.notify_progress(0); - let result = f(self); - let _ = self.progress.stages.pop(); + let result = f(self); - result + if let Worker::Windowed { progress, .. } = self { + let _ = progress.stages.pop(); + } + + result + } + } } } diff --git a/src/ui.rs b/src/ui.rs index 5837528..7e02b81 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -74,7 +74,7 @@ //! type Renderer = Renderer; //! //! // The update logic, called when a message is produced -//! fn react(&mut self, message: Message) { +//! fn react(&mut self, message: Message, _window: &mut Window) { //! // We update the counter value after an interaction here //! match message { //! Message::IncrementPressed => { @@ -145,8 +145,8 @@ pub mod widget; pub use self::core::{Align, Justify}; pub use renderer::{Configuration, Renderer}; pub use widget::{ - button, slider, Button, Checkbox, Radio, Slider, Text, progress_bar, ProgressBar, - image, Image, + button, image, progress_bar, slider, Button, Checkbox, Image, ProgressBar, + Radio, Slider, Text, }; /// A [`Column`] using the built-in [`Renderer`]. @@ -161,6 +161,12 @@ pub type Column<'a, Message> = widget::Column<'a, Message, Renderer>; /// [`Renderer`]: struct.Renderer.html pub type Row<'a, Message> = widget::Row<'a, Message, Renderer>; +/// A [`Panel`] using the built-in [`Renderer`]. +/// +/// [`Panel`]: widget/panel/struct.Panel.html +/// [`Renderer`]: struct.Renderer.html +pub type Panel<'a, Message> = widget::Panel<'a, Message, Renderer>; + /// An [`Element`] using the built-in [`Renderer`]. /// /// [`Element`]: core/struct.Element.html @@ -229,7 +235,7 @@ pub trait UserInterface: Game { /// [`Game::interact`]: ../trait.Game.html#method.interact /// [`Game::Input`]: ../trait.Game.html#associatedtype.Input /// [`Message`]: #associatedtype.Message - fn react(&mut self, message: Self::Message); + fn react(&mut self, message: Self::Message, window: &mut Window); /// Produces the layout of the user interface. /// @@ -361,7 +367,7 @@ pub trait UserInterface: Game { } for message in messages.drain(..) { - game.react(message); + game.react(message, window); } debug.ui_finished(); diff --git a/src/ui/renderer.rs b/src/ui/renderer.rs index 6dc3ecf..cdd9ea1 100644 --- a/src/ui/renderer.rs +++ b/src/ui/renderer.rs @@ -1,6 +1,7 @@ mod button; mod checkbox; mod image; +mod panel; mod progress_bar; mod radio; mod slider; diff --git a/src/ui/renderer/panel.rs b/src/ui/renderer/panel.rs index bcd7642..11da6c2 100644 --- a/src/ui/renderer/panel.rs +++ b/src/ui/renderer/panel.rs @@ -1,5 +1,5 @@ use crate::graphics::{Point, Rectangle, Sprite}; -use crate::ui::core::widget::panel; +use crate::ui::widget::panel; use crate::ui::Renderer; const PANEL_WIDTH: u16 = 28; diff --git a/src/ui/widget.rs b/src/ui/widget.rs index 45489b7..df956d9 100644 --- a/src/ui/widget.rs +++ b/src/ui/widget.rs @@ -9,7 +9,7 @@ //! ``` //! //! However, if you want to use a custom renderer, you will need to work with -//! the definitions of [`Row`] and [`Column`] found in this module. +//! the definitions of [`Row`], [`Column`], and [`Panel`] found in this module. //! //! # Customization //! Every drawable widget has its own module with a `Renderer` trait that must @@ -21,6 +21,7 @@ //! [`ui` module]: ../index.html //! [`Row`]: struct.Row.html //! [`Column`]: struct.Column.html +//! [`Panel`]: struct.Panel.html //! [`Renderer`]: ../struct.Renderer.html mod column; mod row; @@ -28,15 +29,17 @@ mod row; pub mod button; pub mod checkbox; pub mod image; +pub mod panel; pub mod progress_bar; pub mod radio; pub mod slider; pub mod text; +pub use self::image::Image; pub use button::Button; pub use checkbox::Checkbox; pub use column::Column; -pub use self::image::Image; +pub use panel::Panel; pub use progress_bar::ProgressBar; pub use radio::Radio; pub use row::Row; diff --git a/src/ui/widget/panel.rs b/src/ui/widget/panel.rs index 28e5989..507d9b3 100644 --- a/src/ui/widget/panel.rs +++ b/src/ui/widget/panel.rs @@ -1,28 +1,74 @@ +//! Wrap your widgets in a box. use std::hash::Hash; use crate::graphics::{Point, Rectangle}; use crate::ui::core::{ - Event, Hasher, Layout, MouseCursor, Node, Style, Widget, + Element, Event, Hasher, Layout, MouseCursor, Node, Style, Widget, }; +/// A box that can wrap a widget. +/// +/// It implements [`Widget`] when the [`core::Renderer`] implements the +/// [`panel::Renderer`] trait. +/// +/// [`Widget`]: ../../core/trait.Widget.html +/// [`core::Renderer`]: ../../core/trait.Renderer.html +/// [`panel::Renderer`]: trait.Renderer.html +/// +/// # Example +/// +/// ``` +/// use coffee::ui::{Panel, Text}; +/// use coffee::graphics::HorizontalAlignment; +/// +/// pub enum Message { /* ... */ } +/// +/// Panel::::new( +/// Text::new("I'm in a box!") +/// .horizontal_alignment(HorizontalAlignment::Center) +/// ) +/// .width(500); +/// ``` pub struct Panel<'a, Message, Renderer> { style: Style, - content: Box + 'a>, + content: Element<'a, Message, Renderer>, +} + +impl<'a, Message, Renderer> std::fmt::Debug for Panel<'a, Message, Renderer> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Panel") + .field("style", &self.style) + .field("content", &self.content) + .finish() + } } impl<'a, Message, Renderer> Panel<'a, Message, Renderer> { - pub fn new(content: impl Widget + 'a) -> Self { + /// Creates new [`Panel`] containing the given [`Widget`]. + /// + /// [`Panel`]: struct.Panel.html + /// [`Widget`]: ../../core/trait.Widget.html + pub fn new(content: E) -> Self + where + E: 'a + Into>, + { Panel { style: Style::default().padding(20), - content: Box::new(content), + content: content.into(), } } + /// Sets the width of the [`Panel`] in pixels. + /// + /// [`Panel`]: struct.Panel.html pub fn width(mut self, width: u32) -> Self { self.style = self.style.width(width); self } + /// Sets the maximum width of the [`Panel`] in pixels. + /// + /// [`Panel`]: struct.Panel.html pub fn max_width(mut self, max_width: u32) -> Self { self.style = self.style.max_width(max_width); self @@ -35,13 +81,16 @@ where Renderer: self::Renderer, { fn node(&self, renderer: &Renderer) -> Node { - Node::with_children(self.style, vec![self.content.node(renderer)]) + Node::with_children( + self.style, + vec![self.content.widget.node(renderer)], + ) } fn on_event( &mut self, event: Event, - layout: Layout, + layout: Layout<'_>, cursor_position: Point, messages: &mut Vec, ) { @@ -49,14 +98,16 @@ where .iter_mut() .zip(layout.children()) .for_each(|(child, layout)| { - child.on_event(event, layout, cursor_position, messages) + child + .widget + .on_event(event, layout, cursor_position, messages) }); } fn draw( &self, renderer: &mut Renderer, - layout: Layout, + layout: Layout<'_>, cursor_position: Point, ) -> MouseCursor { let bounds = layout.bounds(); @@ -65,7 +116,8 @@ where [&self.content].iter().zip(layout.children()).for_each( |(child, layout)| { - let new_cursor = child.draw(renderer, layout, cursor_position); + let new_cursor = + child.widget.draw(renderer, layout, cursor_position); if new_cursor != MouseCursor::OutOfBounds { cursor = new_cursor; @@ -89,6 +141,31 @@ where } } +/// The renderer of a [`Panel`]. +/// +/// Your [`core::Renderer`] will need to implement this trait before being +/// able to use a [`Panel`] in your user interface. +/// +/// [`Panel`]: struct.Panel.html +/// [`core::Renderer`]: ../../core/trait.Renderer.html pub trait Renderer { + /// Draws a [`Panel`]. + /// + /// It receives the bounds of the [`Panel`]. + /// + /// [`Panel`]: struct.Panel.html fn draw(&mut self, bounds: Rectangle); } + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: 'static + self::Renderer, + Message: 'static, +{ + fn from( + panel: Panel<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(panel) + } +} diff --git a/tests/_graphics/mod.rs b/tests/_graphics/mod.rs new file mode 100644 index 0000000..2c8f9c9 --- /dev/null +++ b/tests/_graphics/mod.rs @@ -0,0 +1,3 @@ +pub mod test; + +pub use test::Test; diff --git a/tests/_graphics/test.rs b/tests/_graphics/test.rs new file mode 100644 index 0000000..5adf236 --- /dev/null +++ b/tests/_graphics/test.rs @@ -0,0 +1,178 @@ +use coffee::graphics::{Canvas, Gpu, Image}; + +mod mesh; + +use mesh::Mesh; + +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Test { + Mesh, +} + +impl Test { + pub fn all() -> Vec { + vec![Test::Mesh] + } + + pub fn run(&self, gpu: &mut Gpu) -> Drawing { + let draw = match self { + Test::Mesh => Mesh::draw(), + }; + + Drawing { + test: *self, + canvas: draw + .run(gpu) + .expect(&format!("Run test \"{}\"", self.to_string())), + } + } +} + +impl std::string::ToString for Test { + fn to_string(&self) -> String { + let name = match self { + Test::Mesh => "mesh", + }; + + String::from(name) + } +} + +pub struct Drawing { + test: Test, + canvas: Canvas, +} + +impl Drawing { + pub fn test(&self) -> Test { + self.test + } + + pub fn canvas(&self) -> &Canvas { + &self.canvas + } + + pub fn save_as_model(&self, gpu: &mut Gpu) { + let model_path = self.model_path(); + let model_directory = model_path.parent().expect("Get model directory"); + let image = self.canvas.read_pixels(gpu); + + std::fs::create_dir_all(model_directory) + .expect("Create model directory"); + + image + .to_rgba() + .save(self.model_path()) + .expect(&format!("Save \"{:?}\" drawing", self.test)); + } + + pub fn differences( + &self, + gpu: &mut Gpu, + ) -> Result, Error> { + let model = { + let mut buf = Vec::new(); + let mut reader = File::open(self.model_path())?; + let _ = reader.read_to_end(&mut buf)?; + image::load_from_memory(&buf)? + }; + + let image = self.canvas.read_pixels(gpu); + + let model_rgba = model.to_rgba(); + let image_rgba = image.to_rgba(); + + if model_rgba + .pixels() + .zip(image_rgba.pixels()) + .all(|(a, b)| a == b) + { + Ok(None) + } else { + let differences: Vec = model_rgba + .pixels() + .zip(image_rgba.pixels()) + .flat_map(|(a, b)| { + if a == b { + &[0, 0, 0, 0] + } else { + &[255, 0, 0, 255] + } + }) + .cloned() + .collect(); + + let image = image::RgbaImage::from_raw( + self.canvas.width() as u32, + self.canvas.height() as u32, + differences, + ) + .expect("Create diff image"); + + let image = + Image::from_image(gpu, image::DynamicImage::ImageRgba8(image)) + .expect("Upload diff image"); + + Ok(Some(Differences { + test: self.test, + canvas: self.canvas.clone(), + image, + })) + } + } + + fn model_path(&self) -> PathBuf { + let mut path = PathBuf::new(); + + path.push("tests"); + path.push("_graphics"); + path.push("models"); + path.push(self.test.to_string()); + path.set_extension("png"); + + path + } +} + +#[derive(Debug)] +pub struct Differences { + test: Test, + canvas: Canvas, + image: Image, +} + +impl Differences { + pub fn test(&self) -> Test { + self.test + } + + pub fn canvas(&self) -> &Canvas { + &self.canvas + } + + pub fn image(&self) -> &Image { + &self.image + } +} + +#[derive(Debug)] +pub enum Error { + ModelImageNotFound(std::io::Error), + ModelImageIsInvalid(image::ImageError), +} + +impl From for Error { + fn from(error: std::io::Error) -> Error { + Error::ModelImageNotFound(error) + } +} + +impl From for Error { + fn from(error: image::ImageError) -> Error { + Error::ModelImageIsInvalid(error) + } +} diff --git a/tests/_graphics/test/mesh.rs b/tests/_graphics/test/mesh.rs new file mode 100644 index 0000000..1d53944 --- /dev/null +++ b/tests/_graphics/test/mesh.rs @@ -0,0 +1,46 @@ +use coffee::graphics::{self, Canvas, Color, Point, Shape}; +use coffee::load::Task; + +pub struct Mesh {} + +impl Mesh { + pub fn draw() -> Task { + Task::using_gpu(|gpu| { + let mut canvas = + Canvas::new(gpu, 300, 300).expect("Canvas creation"); + + let mut mesh = graphics::Mesh::new(); + + mesh.stroke( + Shape::Circle { + center: Point::new(150.0, 150.0), + radius: 40.0, + }, + Color::RED, + 1.0, + ); + + mesh.stroke( + Shape::Circle { + center: Point::new(150.0, 150.0), + radius: 80.0, + }, + Color::GREEN, + 2.0, + ); + + mesh.stroke( + Shape::Circle { + center: Point::new(150.0, 150.0), + radius: 120.0, + }, + Color::BLUE, + 3.0, + ); + + mesh.draw(&mut canvas.as_target(gpu)); + + Ok(canvas) + }) + } +} diff --git a/tests/graphics.rs b/tests/graphics.rs new file mode 100644 index 0000000..3b21501 --- /dev/null +++ b/tests/graphics.rs @@ -0,0 +1,393 @@ +#![cfg(not(target_os = "windows"))] +use coffee::graphics::{ + Color, Frame, Gpu, Point, Quad, Window, WindowSettings, +}; +use coffee::load::Task; +use coffee::ui::{ + button, Button, Checkbox, Column, Element, Justify, Panel, Renderer, Row, + Text, UserInterface, +}; +use coffee::{Game, Result, Timer}; + +mod _graphics; +use _graphics::{test, Test}; + +#[test] +#[ignore] +fn graphics() -> Result<()> { + env_logger::init(); + + ::run(WindowSettings { + title: String::from("Graphics integration tests - Coffee"), + size: (1280, 1024), + resizable: false, + fullscreen: false, + }) +} + +struct Runner { + state: State, +} + +pub enum State { + Pending { + tests: Vec, + }, + Running { + remaining: Vec, + current: test::Drawing, + }, + AskingToSaveModelImage { + remaining: Vec, + current: test::Drawing, + save_button: button::State, + fail_button: button::State, + saved: bool, + }, + ReportingDifferences { + remaining: Vec, + current: test::Differences, + show: bool, + quit_button: button::State, + }, + Finished, +} + +struct Progress { + remaining: usize, + current: Option, +} + +impl State { + fn progress(&self) -> Option { + match self { + State::Pending { tests } => Some(Progress { + remaining: tests.len(), + current: None, + }), + State::Running { current, remaining } => Some(Progress { + remaining: remaining.len(), + current: Some(current.test()), + }), + State::ReportingDifferences { + current, remaining, .. + } => Some(Progress { + remaining: remaining.len(), + current: Some(current.test()), + }), + State::AskingToSaveModelImage { + current, remaining, .. + } => Some(Progress { + remaining: remaining.len(), + current: Some(current.test()), + }), + State::Finished { .. } => None, + } + } +} + +impl Runner { + fn run_next(&mut self, gpu: &mut Gpu) { + // We need to own the current state to avoid awkward copies. + // TODO: Not sure if there is a better way to do this. + // Something like `replace` but taking a closure would be nice. + let state = std::mem::replace(&mut self.state, State::Finished); + + let next = |mut remaining: Vec, gpu: &mut Gpu| { + if let Some(next) = remaining.pop() { + State::Running { + remaining, + current: next.run(gpu), + } + } else { + State::Finished + } + }; + + self.state = match state { + State::Pending { mut tests } => { + if let Some(first) = tests.pop() { + State::Running { + remaining: tests, + current: first.run(gpu), + } + } else { + State::Finished + } + } + State::Running { remaining, current } => { + let differences = current.differences(gpu); + + match differences { + Ok(None) => next(remaining, gpu), + Ok(Some(differences)) => State::ReportingDifferences { + remaining, + current: differences, + show: false, + quit_button: button::State::new(), + }, + Err(test::Error::ModelImageNotFound(_)) => { + State::AskingToSaveModelImage { + remaining, + current, + save_button: button::State::new(), + fail_button: button::State::new(), + saved: false, + } + } + Err(error) => panic!("Something went wrong: {:?}", error), + } + } + State::ReportingDifferences { remaining, .. } => { + next(remaining, gpu) + } + State::AskingToSaveModelImage { remaining, .. } => { + next(remaining, gpu) + } + State::Finished { .. } => state, + } + } +} + +impl Game for Runner { + type LoadingScreen = (); + type Input = (); + + fn load(_window: &Window) -> Task { + Task::succeed(|| Runner { + state: State::Pending { tests: Test::all() }, + }) + } + + fn interact(&mut self, _input: &mut (), window: &mut Window) { + match self.state { + State::Pending { .. } => self.run_next(window.gpu()), + State::Running { .. } => self.run_next(window.gpu()), + State::AskingToSaveModelImage { saved, .. } if saved => { + self.run_next(window.gpu()) + } + _ => {} + } + } + + fn draw(&mut self, frame: &mut Frame, _timer: &Timer) { + frame.clear(Color { + r: 0.3, + g: 0.3, + b: 0.6, + a: 1.0, + }); + + let canvas = match &self.state { + State::Running { current, .. } => Some(current.canvas()), + State::ReportingDifferences { current, .. } => { + Some(current.canvas()) + } + State::AskingToSaveModelImage { current, .. } => { + Some(current.canvas()) + } + _ => None, + }; + + if let Some(canvas) = canvas { + canvas.draw( + Quad { + position: Point::new( + frame.width() * 0.5 - canvas.width() as f32 * 0.5, + frame.height() * 0.5 - canvas.height() as f32 * 0.5, + ), + size: (canvas.width() as f32, canvas.height() as f32), + ..Quad::default() + }, + &mut frame.as_target(), + ); + } + + match &self.state { + State::ReportingDifferences { current, show, .. } if *show => { + let image = current.image(); + + image.draw( + Quad { + position: Point::new( + frame.width() * 0.5 - image.width() as f32 * 0.5, + frame.height() * 0.5 - image.height() as f32 * 0.5, + ), + size: (image.width() as f32, image.height() as f32), + ..Quad::default() + }, + &mut frame.as_target(), + ); + } + _ => {} + } + } + + fn is_finished(&self) -> bool { + match self.state { + State::Finished => true, + _ => false, + } + } + + fn on_close_request(&mut self) -> bool { + panic!("Exited before completion") + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Message { + CreateModelImage, + FailTest, + ToggleDifferences(bool), +} + +impl UserInterface for Runner { + type Message = Message; + type Renderer = Renderer; + + fn react(&mut self, msg: Message, window: &mut Window) { + match msg { + Message::CreateModelImage => match &mut self.state { + State::AskingToSaveModelImage { saved, current, .. } + if !*saved => + { + current.save_as_model(window.gpu()); + + *saved = true; + } + _ => {} + }, + Message::FailTest => { + if let Some(progress) = self.state.progress() { + match progress.current { + Some(test) => panic!("\"{:?}\" test failed", test), + None => {} + } + } + } + Message::ToggleDifferences(value) => { + if let State::ReportingDifferences { show, .. } = + &mut self.state + { + *show = value; + } + } + } + } + + fn layout(&mut self, window: &Window) -> Element { + let progress: Element<_> = match self.state.progress() { + Some(progress) => status_line(progress), + None => Column::new().into(), + }; + + let dialog: Element = match &mut self.state { + State::AskingToSaveModelImage { + current, + save_button, + fail_button, + .. + } => Row::new() + .justify_content(Justify::Center) + .push(save_model_image_dialog( + current.test(), + save_button, + fail_button, + )) + .into(), + State::ReportingDifferences { + current, + show, + quit_button, + .. + } => Row::new() + .justify_content(Justify::Center) + .push(differences_dialog(current.test(), *show, quit_button)) + .into(), + _ => Column::new().into(), + }; + + Column::new() + .width(window.width() as u32) + .height(window.height() as u32) + .padding(20) + .spacing(20) + .justify_content(Justify::SpaceBetween) + .push(progress) + .push(dialog) + .into() + } +} + +// UI elements +fn status_line<'a>(progress: Progress) -> Element<'a, Message> { + Row::new() + .justify_content(Justify::SpaceBetween) + .push(Text::new(&match progress.current { + Some(test) => format!("Testing {:?}...", test), + None => String::from("Pending..."), + })) + .push(Text::new(&format!( + "{} tests remaining", + progress.remaining + ))) + .into() +} + +fn save_model_image_dialog<'a>( + test: Test, + save_button: &'a mut button::State, + fail_button: &'a mut button::State, +) -> Element<'a, Message> { + let message = Text::new(&format!( + "No model image exists for the \"{:?}\" test. \ + Create one from the current drawing?", + test + )); + + let options = Row::new() + .spacing(10) + .push( + Button::new(fail_button, "No, fail the test.") + .class(button::Class::Secondary) + .fill_width() + .on_press(Message::FailTest), + ) + .push( + Button::new(save_button, "Yes, create it.") + .class(button::Class::Positive) + .fill_width() + .on_press(Message::CreateModelImage), + ); + + Panel::new(Column::new().spacing(20).push(message).push(options)).into() +} + +fn differences_dialog<'a>( + test: Test, + show: bool, + quit_button: &'a mut button::State, +) -> Element<'a, Message> { + let message = + Text::new(&format!("Differences found for the \"{:?}\" test.", test)); + + let show_checkbox = + Checkbox::new(show, "Overlay differences", Message::ToggleDifferences); + + let options = Row::new().spacing(10).push( + Button::new(quit_button, "Quit") + .class(button::Class::Secondary) + .fill_width() + .on_press(Message::FailTest), + ); + + Panel::new( + Column::new() + .spacing(20) + .push(message) + .push(show_checkbox) + .push(options), + ) + .into() +}