From 6ff766846f233a34fc8df7de91dccbf3965c1026 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 4 Jan 2024 15:54:58 -0800 Subject: [PATCH] Added CushyWindow While working on the changelog, I realized I didn't provide a type that allowed a third party developer to provide a PlatformWindowImplementation. This type now completes it. --- CHANGELOG.md | 51 ++++++- src/widget.rs | 6 +- src/window.rs | 371 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 310 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c1b6c..337f7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes +- All context types no longer accept a `'window` lifetime. For most end-user + code, it means removing one elided lifetime from these types: + - `WidgetContext` + - `EventContext` + - `LayoutContext` + - `GraphicsContext` +- `WidgetContext`'s `Deref` target is now `&mut dyn PlatformWindow`. This change + ensures all widgets utilize a shared interface between any host architecture. +- All `DeviceId` parameters have been changed to a `DeviceId` type provided by + Cushy. This allows for creating arbitrary input device IDs when creating an + integration with other frameworks or driving simulated input in a + `VirtualWindow`. - `WidgetRef` is now a `struct` instead of an enum. This refactor changes the mounted state to be stored in a `WindowLocal`, ensuring `WidgetRef`s work properly when used in a `WidgetInstance` shared between multiple windows. @@ -60,14 +72,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `WidgetCacheKey` now includes the `KludgineId` of the context it was created from. This ensures if a `WidgetInstance` moves or is shared between windows, the cache is invalidated. -- All `Dynamic` mapping functions now utilize weak references, and clean up as - necessary if a value is not able to be upgraded. +- All `Dynamic` mapping functions now utilize weak references, and the + `CallbackHandle` now contains a strong reference to the originating dynamic. + This should have no visible impact on end-user code. - `ForEach`/`MapEach`'s implementations for tuples are now defined using `Source` and `DynamicRead`. This allows combinations of `Dynamic`s and `DynamicReader`s to be used in for_each/map_each expressions. ### Added +- Cushy now supports being embedded in any wgpu application. Here are the API + highlights: + + - `CushyWindow` is a type that contains the state of a standalone window. It + defines an API designed to enable full control with winit integration into + any wgpu application. This type's design is inspired by wpgu's + "Encapsulating Graphics Work" article. Each of its functions require being + passed a type that implements `PlatformWindowImplementation`, which exposes + all APIs Cushy needs to be fully functional. + - `VirtualWindow` is a type that makes it easy to render a Cushy interface in + any wgpu application where no winit integration is desired. It utilizes + `VirtualState` as its `PlatformWindowImplementation`. This type also exposes + a design inspired by wpgu's "Encapsulating Graphics Work" article. + - `WindowDynamicState` is a set of dynamics that can be updated through + external threads and tasks. + - is a new trait that allows + customizing the behavior that Cushy widgets need to be rendered. +- Cushy now supports easily rendering a virtual window: `VirtualRecorder`. This + type utilizes a `VirtualWindow` and provides easy access to captured images. + This type has the ability to capture animated PNGs as well as still images. - `figures` is now directly re-exported at this crate's root. Kludgine still also provides this export, so existing references through kludgine will continue to work. This was added as an attempt to fix links on docs.rs (see @@ -115,6 +148,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 clone `self` before calling the `into_` function. This has only been done in situations where it is known or likely that the clone being performed is cheap. +- `CallbackHandle` now has `weak()` and `forget_owners()`. These functions allow + a `CallbackHandle` to release its strong references to the `Dynamic` that the + callback is installed on. This enables forming weak callback graphs that clean + up independent of one another. +- `Source::weak_clone` returns a `Dynamic` with a clone of each value + stored in the original source. The returned dynamic holds no strong references + to the original source. +- `Point`, `Size`, and `Rect` now implement `LinearInterpolate`. +- `MakeWidget::build_virtual_window()` returns a builder for a `VirtualWindow`. +- `MakeWidget::build_recorder()` returns a builder for a `VirtualRecorder`. +- `Space::dynamic()` returns a space that dynamically colors itself using + component provided. This allows the spacer to use values from the theme at + runtime. +- `Space::primary()` returns a space that contains the primary color. [99]: https://github.com/khonsulabs/cushy/issues/99 [120]: https://github.com/khonsulabs/cushy/issues/120 diff --git a/src/widget.rs b/src/widget.rs index bcc556e..25857bf 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -45,7 +45,7 @@ use crate::widgets::{ }; use crate::window::sealed::WindowCommand; use crate::window::{ - DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, VirtualWindowBuilder, Window, + CushyWindowBuilder, DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, Window, WindowBehavior, WindowHandle, WindowLocal, }; use crate::ConstraintLimit; @@ -917,8 +917,8 @@ pub trait MakeWidget: Sized { } /// Returns a builder for a [`VirtualWindow`](crate::window::VirtualWindow). - fn build_virtual_window(self) -> VirtualWindowBuilder { - VirtualWindowBuilder::new(self) + fn build_virtual_window(self) -> CushyWindowBuilder { + CushyWindowBuilder::new(self) } /// Returns a builder for a [`VirtualRecorder`](crate::window::VirtualRecorder) diff --git a/src/window.rs b/src/window.rs index 223b078..8756d44 100644 --- a/src/window.rs +++ b/src/window.rs @@ -652,10 +652,7 @@ where App: Application + ?Sized, { let cushy = app.cushy().clone(); - // let Some(app) = app.as_app().as_kludgine() else { - // return Ok(None); - // }; - let handle = CushyWindow::::open_with( + let handle = OpenWindow::::open_with( app, sealed::Context { user: self.context, @@ -730,7 +727,7 @@ pub trait WindowBehavior: Sized + 'static { } #[allow(clippy::struct_excessive_bools)] -struct CushyWindow { +struct OpenWindow { behavior: T, tree: Tree, root: MountedWidget, @@ -757,7 +754,7 @@ struct CushyWindow { on_closed: Option, } -impl CushyWindow +impl OpenWindow where T: WindowBehavior, { @@ -1623,7 +1620,7 @@ enum RootMode { Align, } -impl kludgine::app::WindowBehavior for CushyWindow +impl kludgine::app::WindowBehavior for OpenWindow where T: WindowBehavior, { @@ -1871,7 +1868,7 @@ where } } -impl Drop for CushyWindow { +impl Drop for OpenWindow { fn drop(&mut self) { if let Some(on_closed) = self.on_closed.take() { on_closed.invoke(()); @@ -2369,7 +2366,7 @@ impl PlatformWindowImplementation for &mut VirtualState { } /// A builder for a [`VirtualWindow`]. -pub struct VirtualWindowBuilder { +pub struct CushyWindowBuilder { widget: WidgetInstance, multisample_count: u32, initial_size: Size, @@ -2377,8 +2374,8 @@ pub struct VirtualWindowBuilder { transparent: bool, } -impl VirtualWindowBuilder { - /// Returns a new builder for a virtual window that contains `contents`. +impl CushyWindowBuilder { + /// Returns a new builder for a Cushy window that contains `contents`. #[must_use] pub fn new(contents: impl MakeWidget) -> Self { Self { @@ -2390,7 +2387,7 @@ impl VirtualWindowBuilder { } } - /// Sets this virtual window's multi-sample count. + /// Sets this window's multi-sample count. /// /// By default, 4 samples are taken. When 1 sample is used, multisampling is /// fully disabled. @@ -2400,7 +2397,7 @@ impl VirtualWindowBuilder { self } - /// Sets the size of the virtual window. + /// Sets the size of the window. #[must_use] pub fn size(mut self, size: Size) -> Self where @@ -2410,7 +2407,7 @@ impl VirtualWindowBuilder { self } - /// Sets the DPI scaling factor of the virtual window. + /// Sets the DPI scaling factor of the window. #[must_use] pub fn scale(mut self, scale: f32) -> Self { self.scale = scale; @@ -2424,58 +2421,26 @@ impl VirtualWindowBuilder { self } - /// Returns the initialized virtual window. + /// Returns the initialized window. #[must_use] - pub fn finish(self, device: &wgpu::Device, queue: &wgpu::Queue) -> VirtualWindow { - VirtualWindow::new( - self.widget, - self.multisample_count, - self.initial_size, - self.scale, - self.transparent, - device, - queue, - ) - } -} - -/// A virtual Cushy window. -/// -/// This type allows rendering Cushy applications directly into any wgpu -/// application. -pub struct VirtualWindow { - window: CushyWindow, - kludgine: Kludgine, - last_rendered_at: Option, - state: VirtualState, -} - -impl VirtualWindow { - /// Returns a new virtual window with the provided specifications. - fn new( - widget: WidgetInstance, - multisample_count: u32, - initial_size: Size, - scale: f32, - transparent: bool, - device: &wgpu::Device, - queue: &wgpu::Queue, - ) -> Self { + pub fn finish(self, window: W, device: &wgpu::Device, queue: &wgpu::Queue) -> CushyWindow + where + W: PlatformWindowImplementation, + { let mut kludgine = Kludgine::new( device, queue, wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::MultisampleState { - count: multisample_count, + count: self.multisample_count, ..Default::default() }, - initial_size, - scale, + self.initial_size, + self.scale, ); - let mut state = VirtualState::new(); - let window = CushyWindow::::new( - widget.make_widget(), - &mut state, + let window = OpenWindow::::new( + self.widget, + window, &mut kludgine::Graphics::new(&mut kludgine, device, queue), sealed::WindowSettings { cushy: Cushy::new(), @@ -2487,7 +2452,7 @@ impl VirtualWindow { inner_size: Dynamic::default(), theme: None, theme_mode: None, - transparent, + transparent: self.transparent, serif_font_family: FontFamilyList::default(), sans_serif_font_family: FontFamilyList::default(), fantasy_font_family: FontFamilyList::default(), @@ -2498,25 +2463,41 @@ impl VirtualWindow { }, ); - Self { - window, - kludgine, - last_rendered_at: None, - state, - } + CushyWindow { window, kludgine } } + /// Returns an initialized [`VirtualWindow`]. + #[must_use] + pub fn finish_virtual(self, device: &wgpu::Device, queue: &wgpu::Queue) -> VirtualWindow { + let mut state = VirtualState::new(); + let cushy = self.finish(&mut state, device, queue); + + VirtualWindow { + cushy, + state, + last_rendered_at: None, + } + } +} + +/// A standalone Cushy window. +/// +/// This type allows rendering Cushy applications directly into any wgpu +/// application. +pub struct CushyWindow { + window: OpenWindow, + kludgine: Kludgine, +} + +impl CushyWindow { /// Prepares all necessary resources and operations necessary to render the /// next frame. - pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { - let now = Instant::now(); - self.state.elapsed = self - .last_rendered_at - .map(|i| now.duration_since(i)) - .unwrap_or_default(); - self.last_rendered_at = Some(now); + pub fn prepare(&mut self, window: W, device: &wgpu::Device, queue: &wgpu::Queue) + where + W: PlatformWindowImplementation, + { self.window.prepare( - &mut self.state, + window, &mut kludgine::Graphics::new(&mut self.kludgine, device, queue), ); } @@ -2545,7 +2526,6 @@ impl VirtualWindow { queue: &wgpu::Queue, additional_drawing: Option<&Drawing>, ) -> Option { - self.state.dynamic.redraw_target.set(RedrawTarget::Never); let mut frame = self.kludgine.next_frame(); let mut gfx = frame.render(pass, device, queue); self.window.contents.render(1., &mut gfx); @@ -2564,7 +2544,6 @@ impl VirtualWindow { device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { - self.state.dynamic.redraw_target.set(RedrawTarget::Never); let mut frame = self.kludgine.next_frame(); let mut gfx = frame.render_into(texture, load_op, device, queue); self.window.contents.render(1., &mut gfx); @@ -2582,14 +2561,194 @@ impl VirtualWindow { kludgine::Graphics::new(&mut self.kludgine, device, queue) } + /// Requests that the window close. + /// + /// Returns true if the request should be honored. + pub fn request_close(&mut self, window: W) -> bool + where + W: PlatformWindowImplementation, + { + self.window.close_requested(window, &mut self.kludgine) + } + + /// Returns the current size of the window. + pub const fn size(&self) -> Size { + self.kludgine.size() + } + + /// Returns the current DPI scale of the window. + pub const fn scale(&self) -> Fraction { + self.kludgine.scale() + } + + /// Updates the dimensions and DPI scaling of the window. + pub fn resize(&mut self, new_size: Size, new_scale: impl Into, queue: &wgpu::Queue) { + self.kludgine.resize(new_size, new_scale.into(), queue); + self.window.resized(new_size); + } + + /// Provide keyboard input to this virtual window. + /// + /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. + pub fn keyboard_input( + &mut self, + window: W, + device_id: DeviceId, + input: KeyEvent, + is_synthetic: bool, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + self.window + .keyboard_input(window, &mut self.kludgine, device_id, input, is_synthetic) + } + + /// Provides mouse wheel input to this window. + /// + /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. + pub fn mouse_wheel( + &mut self, + window: W, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + self.window + .mouse_wheel(window, &mut self.kludgine, device_id, delta, phase) + } + + /// Provides input manager events to this window. + /// + /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. + pub fn ime(&mut self, window: W, ime: &Ime) -> EventHandling + where + W: PlatformWindowImplementation, + { + self.window.ime(window, &mut self.kludgine, ime) + } + + /// Provides cursor movement events to this window. + pub fn cursor_moved( + &mut self, + window: W, + device_id: DeviceId, + position: impl Into>, + ) where + W: PlatformWindowImplementation, + { + self.window + .cursor_moved(window, &mut self.kludgine, device_id, position); + } + + /// Notifies the window that the cursor is no longer within the window. + pub fn cursor_left(&mut self, window: W) + where + W: PlatformWindowImplementation, + { + self.window.cursor_left(window, &mut self.kludgine); + } + + /// Provides mouse input events to tihs window. + /// + /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. + pub fn mouse_input( + &mut self, + window: W, + device_id: DeviceId, + state: ElementState, + button: MouseButton, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + self.window + .mouse_input(window, &mut self.kludgine, device_id, state, button) + } +} + +/// A virtual Cushy window. +/// +/// This type allows rendering Cushy applications directly into any wgpu +/// application. +pub struct VirtualWindow { + cushy: CushyWindow, + state: VirtualState, + last_rendered_at: Option, +} + +impl VirtualWindow { + /// Prepares all necessary resources and operations necessary to render the + /// next frame. + pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { + let now = Instant::now(); + self.state.elapsed = self + .last_rendered_at + .map(|i| now.duration_since(i)) + .unwrap_or_default(); + self.last_rendered_at = Some(now); + self.cushy.prepare(&mut self.state, device, queue); + } + + /// Renders this window in a wgpu render pass created from `pass`. + /// + /// Returns the submission index of the last command submission, if any + /// commands were submitted. + pub fn render( + &mut self, + pass: &wgpu::RenderPassDescriptor<'_, '_>, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> Option { + self.render_with(pass, device, queue, None) + } + + /// Renders this window in a wgpu render pass created from `pass`. + /// + /// Returns the submission index of the last command submission, if any + /// commands were submitted. + pub fn render_with( + &mut self, + pass: &wgpu::RenderPassDescriptor<'_, '_>, + device: &wgpu::Device, + queue: &wgpu::Queue, + additional_drawing: Option<&Drawing>, + ) -> Option { + self.state.dynamic.redraw_target.set(RedrawTarget::Never); + self.cushy + .render_with(pass, device, queue, additional_drawing) + } + + /// Renders this window into `texture` after performing `load_op`. + pub fn render_into( + &mut self, + texture: &kludgine::Texture, + load_op: wgpu::LoadOp, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> Option { + self.state.dynamic.redraw_target.set(RedrawTarget::Never); + self.cushy.render_into(texture, load_op, device, queue) + } + + /// Returns a new [`kludgine::Graphics`] context for this window. + #[must_use] + pub fn graphics<'gfx>( + &'gfx mut self, + device: &'gfx wgpu::Device, + queue: &'gfx wgpu::Queue, + ) -> kludgine::Graphics<'gfx> { + self.cushy.graphics(device, queue) + } + /// Requests that the window close. /// /// Returns true if the request should be honored. pub fn request_close(&mut self) -> bool { - if self - .window - .close_requested(&mut self.state, &mut self.kludgine) - { + if self.cushy.request_close(&mut self.state) { self.state.closed = true; true } else { @@ -2612,18 +2771,17 @@ impl VirtualWindow { /// Returns the current size of the window. pub const fn size(&self) -> Size { - self.kludgine.size() + self.cushy.size() } /// Returns the current DPI scale of the window. pub const fn scale(&self) -> Fraction { - self.kludgine.scale() + self.cushy.scale() } /// Updates the dimensions and DPI scaling of the window. pub fn resize(&mut self, new_size: Size, new_scale: impl Into, queue: &wgpu::Queue) { - self.kludgine.resize(new_size, new_scale.into(), queue); - self.window.resized(new_size); + self.cushy.resize(new_size, new_scale.into(), queue); } /// Provide keyboard input to this virtual window. @@ -2635,13 +2793,8 @@ impl VirtualWindow { input: KeyEvent, is_synthetic: bool, ) -> EventHandling { - self.window.keyboard_input( - &mut self.state, - &mut self.kludgine, - device_id, - input, - is_synthetic, - ) + self.cushy + .keyboard_input(&mut self.state, device_id, input, is_synthetic) } /// Provides mouse wheel input to this window. @@ -2653,26 +2806,26 @@ impl VirtualWindow { delta: MouseScrollDelta, phase: TouchPhase, ) -> EventHandling { - self.window - .mouse_wheel(&mut self.state, &mut self.kludgine, device_id, delta, phase) + self.cushy + .mouse_wheel(&mut self.state, device_id, delta, phase) } /// Provides input manager events to this window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn ime(&mut self, ime: &Ime) -> EventHandling { - self.window.ime(&mut self.state, &mut self.kludgine, ime) + self.cushy.ime(&mut self.state, ime) } /// Provides cursor movement events to this window. pub fn cursor_moved(&mut self, device_id: DeviceId, position: impl Into>) { - self.window - .cursor_moved(&mut self.state, &mut self.kludgine, device_id, position); + self.cushy + .cursor_moved(&mut self.state, device_id, position); } /// Notifies the window that the cursor is no longer within the window. pub fn cursor_left(&mut self) { - self.window.cursor_left(&mut self.state, &mut self.kludgine); + self.cushy.cursor_left(&mut self.state); } /// Provides mouse input events to tihs window. @@ -2684,13 +2837,8 @@ impl VirtualWindow { state: ElementState, button: MouseButton, ) -> EventHandling { - self.window.mouse_input( - &mut self.state, - &mut self.kludgine, - device_id, - state, - button, - ) + self.cushy + .mouse_input(&mut self.state, device_id, state, button) } } @@ -2962,15 +3110,12 @@ where None, ))?; - let window = VirtualWindow::new( - contents.make_widget(), - 4, - size, - scale, - Format::HAS_ALPHA, - &device, - &queue, - ); + let window = contents + .build_virtual_window() + .size(size) + .scale(scale) + .transparent() + .finish_virtual(&device, &queue); let mut recorder = Self { window, @@ -2984,7 +3129,7 @@ where data_size: Size::ZERO, format: PhantomData, }; - recorder.window.window.resize_to_fit = resize_to_fit; + recorder.window.cushy.window.resize_to_fit = resize_to_fit; recorder.refresh()?; if resize_to_fit && recorder.window.state.size != recorder.window.size() { @@ -3086,7 +3231,7 @@ where } fn redraw(&mut self) { - let mut render_size = self.window.kludgine.size().ceil(); + let mut render_size = self.window.size().ceil(); if self.window.state.size != render_size { let current_scale = self.window.scale(); self.window