From be0399279c63e457d2d6a5fcf4ae4bc7483f64f9 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 11:35:43 -0800 Subject: [PATCH 01/10] Initial implementation of offscreen rendering --- .gitignore | 3 +- Cargo.lock | 39 +- Cargo.toml | 3 +- examples/custom-widgets.rs | 8 +- examples/offscreen.rs | 46 ++ src/app.rs | 23 +- src/context.rs | 142 ++--- src/debug.rs | 4 +- src/styles.rs | 35 +- src/styles/components.rs | 2 +- src/tick.rs | 2 +- src/value.rs | 12 +- src/widget.rs | 193 +++--- src/widgets/align.rs | 6 +- src/widgets/button.rs | 46 +- src/widgets/canvas.rs | 17 +- src/widgets/checkbox.rs | 4 +- src/widgets/collapse.rs | 4 +- src/widgets/color.rs | 10 +- src/widgets/container.rs | 16 +- src/widgets/custom.rs | 182 +++--- src/widgets/disclose.rs | 30 +- src/widgets/expand.rs | 4 +- src/widgets/grid.rs | 8 +- src/widgets/image.rs | 4 +- src/widgets/input.rs | 63 +- src/widgets/label.rs | 8 +- src/widgets/layers.rs | 38 +- src/widgets/mode_switch.rs | 2 +- src/widgets/progress.rs | 6 +- src/widgets/radio.rs | 4 +- src/widgets/resize.rs | 4 +- src/widgets/scroll.rs | 24 +- src/widgets/slider.rs | 32 +- src/widgets/space.rs | 4 +- src/widgets/stack.rs | 6 +- src/widgets/style.rs | 2 +- src/widgets/switcher.rs | 2 +- src/widgets/themed.rs | 2 +- src/widgets/tilemap.rs | 24 +- src/widgets/validated.rs | 5 +- src/widgets/wrap.rs | 4 +- src/window.rs | 1182 +++++++++++++++++++++++++++++++++--- 43 files changed, 1617 insertions(+), 638 deletions(-) create mode 100644 examples/offscreen.rs diff --git a/.gitignore b/.gitignore index 9f97022..97c1151 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +examples/offscreen*.png \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a9326a4..bca7823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,7 @@ dependencies = [ "ahash", "alot", "arboard", + "bytemuck", "cushy-macros", "figures", "image", @@ -1179,7 +1180,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#0bda2a6dc273aa49338f23ea5190aefdf037d740" +source = "git+https://github.com/khonsulabs/kludgine#9999ff4a323a6dec1deebb9a0a1825d561559478" dependencies = [ "ahash", "alot", @@ -1827,9 +1828,9 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", "syn", @@ -1858,9 +1859,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] @@ -1891,9 +1892,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2173,18 +2174,18 @@ checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", @@ -2331,9 +2332,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -2360,18 +2361,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", @@ -3185,9 +3186,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winit" -version = "0.29.7" +version = "0.29.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd430cd4560ee9c48885a4ef473b609a56796e37b1e18222abee146143f7457" +checksum = "0dc1a7ae1076890701c7dd71ea35b2aebaf9aeb7b8868ac2d33b1c7e8ef93c00" dependencies = [ "ahash", "android-activity", diff --git a/Cargo.toml b/Cargo.toml index c8f633c..00cc8ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,8 @@ cushy-macros = { version = "0.2.0", path = "cushy-macros" } arboard = "3.2.1" zeroize = "1.6.1" unicode-segmentation = "1.10.1" +pollster = "0.3.0" +bytemuck = "1.14.0" # [patch.crates-io] @@ -57,7 +59,6 @@ unicode-segmentation = "1.10.1" opt-level = 2 [dev-dependencies] -pollster = "0.3.0" rand = "0.8.5" image = { version = "0.24.7", features = ["png"] } diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 1e68df8..9891f40 100644 --- a/examples/custom-widgets.rs +++ b/examples/custom-widgets.rs @@ -90,14 +90,14 @@ impl Default for Toggle { } impl Widget for Toggle { - fn redraw(&mut self, context: &mut cushy::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut cushy::context::GraphicsContext<'_, '_, '_, '_>) { context.fill(self.color.get_tracking_redraw(context)); } fn layout( &mut self, available_space: Size, - context: &mut cushy::context::LayoutContext<'_, '_, '_, '_, '_>, + context: &mut cushy::context::LayoutContext<'_, '_, '_, '_>, ) -> Size { Size::new( available_space.width.min(), @@ -108,7 +108,7 @@ impl Widget for Toggle { fn hit_test( &mut self, _location: figures::Point, - _context: &mut cushy::context::EventContext<'_, '_>, + _context: &mut cushy::context::EventContext<'_>, ) -> bool { true } @@ -118,7 +118,7 @@ impl Widget for Toggle { _location: figures::Point, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - _context: &mut cushy::context::EventContext<'_, '_>, + _context: &mut cushy::context::EventContext<'_>, ) -> cushy::widget::EventHandling { self.value.toggle(); diff --git a/examples/offscreen.rs b/examples/offscreen.rs new file mode 100644 index 0000000..dca0bd9 --- /dev/null +++ b/examples/offscreen.rs @@ -0,0 +1,46 @@ +use cushy::widget::MakeWidget; +use figures::Size; + +fn ui() -> impl MakeWidget { + "Hello World".into_button().centered() +} + +fn main() { + // The default recorder generated solid, rgb images. + let recorder = ui() + .build_recorder() + .size(Size::new(320, 240)) + .finish() + .unwrap(); + image::save_buffer_with_format( + "examples/offscreen.png", + recorder.bytes(), + recorder.size().width.get(), + recorder.size().height.get(), + image::ColorType::Rgb8, + image::ImageFormat::Png, + ) + .unwrap(); + + // Creating a recorder with alpha makes the virtual window transparent. + let recorder = ui() + .build_recorder() + .with_alpha() + .size(Size::new(320, 240)) + .finish() + .unwrap(); + image::save_buffer_with_format( + "examples/offscreen-transparent.png", + recorder.bytes(), + recorder.size().width.get(), + recorder.size().height.get(), + image::ColorType::Rgba8, + image::ImageFormat::Png, + ) + .unwrap(); +} + +#[test] +fn runs() { + main(); +} diff --git a/src/app.rs b/src/app.rs index 4b8742b..ab5c063 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,11 +30,7 @@ impl Default for PendingApp { fn default() -> Self { Self { app: kludgine::app::PendingApp::default(), - cushy: Cushy { - clipboard: Clipboard::new() - .ok() - .map(|clipboard| Arc::new(Mutex::new(clipboard))), - }, + cushy: Cushy::new(), } } } @@ -52,6 +48,14 @@ pub struct Cushy { } impl Cushy { + pub(crate) fn new() -> Self { + Self { + clipboard: Clipboard::new() + .ok() + .map(|clipboard| Arc::new(Mutex::new(clipboard))), + } + } + /// Returns a locked mutex guard to the OS's clipboard, if one was able to be /// initialized when the window opened. #[must_use] @@ -77,7 +81,7 @@ impl Application for PendingApp { fn as_app(&self) -> App { App { - app: self.app.as_app(), + app: Some(self.app.as_app()), cushy: self.cushy.clone(), } } @@ -86,7 +90,7 @@ impl Application for PendingApp { /// A handle to a Cushy application. #[derive(Clone)] pub struct App { - app: kludgine::app::App, + app: Option>, cushy: Cushy, } @@ -102,7 +106,10 @@ impl Application for App { impl AsApplication> for App { fn as_application(&self) -> &dyn kludgine::app::Application> { - self.app.as_application() + self.app + .as_ref() + .map(AsApplication::as_application) + .expect("no app") } } diff --git a/src/context.rs b/src/context.rs index 1ceda6c..417d3b0 100644 --- a/src/context.rs +++ b/src/context.rs @@ -21,16 +21,16 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; use crate::tree::Tree; use crate::value::{IntoValue, Source, Value}; use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; -use crate::window::{CursorState, RunningWindow, ThemeMode}; +use crate::window::{CursorState, PlatformWindow, ThemeMode}; use crate::ConstraintLimit; /// A context to an event function. /// /// This type is a combination of a reference to the rendering library, /// [`Kludgine`], and a [`WidgetContext`]. -pub struct EventContext<'context, 'window> { +pub struct EventContext<'context> { /// The context for the widget receiving the event. - pub widget: WidgetContext<'context, 'window>, + pub widget: WidgetContext<'context>, /// The rendering library's state. /// /// This is useful for accessing the current [scale](Kludgine::scale) or @@ -38,13 +38,10 @@ pub struct EventContext<'context, 'window> { pub kludgine: &'context mut Kludgine, } -impl<'context, 'window> EventContext<'context, 'window> { +impl<'context> EventContext<'context> { const MAX_PENDING_CHANGE_CYCLES: u8 = 100; - pub(crate) fn new( - widget: WidgetContext<'context, 'window>, - kludgine: &'context mut Kludgine, - ) -> Self { + pub(crate) fn new(widget: WidgetContext<'context>, kludgine: &'context mut Kludgine) -> Self { Self { widget, kludgine } } @@ -58,10 +55,10 @@ impl<'context, 'window> EventContext<'context, 'window> { pub fn for_other<'child, Widget>( &'child mut self, widget: &Widget, - ) -> >>::Result + ) -> >>::Result where Widget: ManageWidget, - Widget::Managed: MapManagedWidget>, + Widget::Managed: MapManagedWidget>, { widget .manage(self) @@ -177,7 +174,8 @@ impl<'context, 'window> EventContext<'context, 'window> { cursor = widget_cursor; } } - self.winit().set_cursor_icon(cursor.unwrap_or_default()); + self.window_mut() + .set_cursor_icon(cursor.unwrap_or_default()); } pub(crate) fn clear_hover(&mut self) { @@ -189,7 +187,7 @@ impl<'context, 'window> EventContext<'context, 'window> { old_hover.lock().as_widget().unhover(&mut old_hover_context); } - self.winit().set_cursor_icon(CursorIcon::Default); + self.window_mut().set_cursor_icon(CursorIcon::Default); } fn apply_pending_activation(&mut self) { @@ -480,15 +478,15 @@ impl<'context, 'window> EventContext<'context, 'window> { } } -impl<'context, 'window> Deref for EventContext<'context, 'window> { - type Target = WidgetContext<'context, 'window>; +impl<'context> Deref for EventContext<'context> { + type Target = WidgetContext<'context>; fn deref(&self) -> &Self::Target { &self.widget } } -impl<'context, 'window> DerefMut for EventContext<'context, 'window> { +impl<'context> DerefMut for EventContext<'context> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.widget } @@ -523,18 +521,18 @@ impl DerefMut for Exclusive<'_, T> { } /// A context to a function that is rendering a widget. -pub struct GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { +pub struct GraphicsContext<'context, 'clip, 'gfx, 'pass> { /// The context of the widget being rendered. - pub widget: WidgetContext<'context, 'window>, + pub widget: WidgetContext<'context>, /// The graphics context clipped and offset to the area of the widget being /// rendered. Drawing at 0,0 will draw at the top-left pixel of the laid-out /// widget region. pub gfx: Exclusive<'context, Graphics<'clip, 'gfx, 'pass>>, } -impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { +impl<'context, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'clip, 'gfx, 'pass> { /// Returns a new instance that borrows from `self`. - pub fn borrowed(&mut self) -> GraphicsContext<'_, 'window, 'clip, 'gfx, 'pass> { + pub fn borrowed(&mut self) -> GraphicsContext<'_, 'clip, 'gfx, 'pass> { GraphicsContext { widget: self.widget.borrowed(), gfx: Exclusive::Borrowed(&mut self.gfx), @@ -546,12 +544,10 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' pub fn for_other<'child, Widget>( &'child mut self, widget: &Widget, - ) -> , - >>::Result + ) -> >>::Result where Widget: ManageWidget, - Widget::Managed: MapManagedWidget>, + Widget::Managed: MapManagedWidget>, { let opacity = self.get(&Opacity); widget.manage(self).map(|widget| { @@ -577,7 +573,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } /// Returns a new graphics context that renders to the `clip` rectangle. - pub fn clipped_to(&mut self, clip: Rect) -> GraphicsContext<'_, 'window, '_, 'gfx, 'pass> { + pub fn clipped_to(&mut self, clip: Rect) -> GraphicsContext<'_, '_, 'gfx, 'pass> { GraphicsContext { widget: self.widget.borrowed(), gfx: Exclusive::Owned(self.gfx.clipped_to(clip)), @@ -675,7 +671,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' } } -impl Drop for GraphicsContext<'_, '_, '_, '_, '_> { +impl Drop for GraphicsContext<'_, '_, '_, '_> { fn drop(&mut self) { if matches!(self.widget.pending_state, PendingState::Owned(_)) { self.as_event_context().apply_pending_state(); @@ -683,36 +679,30 @@ impl Drop for GraphicsContext<'_, '_, '_, '_, '_> { } } -impl<'context, 'window, 'clip, 'gfx, 'pass> Deref - for GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> -{ - type Target = WidgetContext<'context, 'window>; +impl<'context, 'clip, 'gfx, 'pass> Deref for GraphicsContext<'context, 'clip, 'gfx, 'pass> { + type Target = WidgetContext<'context>; fn deref(&self) -> &Self::Target { &self.widget } } -impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut - for GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> -{ +impl<'context, 'clip, 'gfx, 'pass> DerefMut for GraphicsContext<'context, 'clip, 'gfx, 'pass> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.widget } } /// A context to a function that is rendering a widget. -pub struct LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> { +pub struct LayoutContext<'context, 'clip, 'gfx, 'pass> { /// The graphics context that this layout operation is being performed /// within. - pub graphics: GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + pub graphics: GraphicsContext<'context, 'clip, 'gfx, 'pass>, persist_layout: bool, } -impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> { - pub(crate) fn new( - graphics: &'context mut GraphicsContext<'_, 'window, 'clip, 'gfx, 'pass>, - ) -> Self { +impl<'context, 'clip, 'gfx, 'pass> LayoutContext<'context, 'clip, 'gfx, 'pass> { + pub(crate) fn new(graphics: &'context mut GraphicsContext<'_, 'clip, 'gfx, 'pass>) -> Self { Self { graphics: graphics.borrowed(), persist_layout: true, @@ -736,10 +726,10 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl pub fn for_other<'child, Widget>( &'child mut self, widget: &Widget, - ) -> >>::Result + ) -> >>::Result where Widget: ManageWidget, - Widget::Managed: MapManagedWidget>, + Widget::Managed: MapManagedWidget>, { widget.manage(self).map(|widget| LayoutContext { graphics: self.graphics.for_other(&widget), @@ -782,36 +772,30 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl } } -impl<'context, 'window, 'clip, 'gfx, 'pass> AsEventContext<'window> - for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> -{ - fn as_event_context(&mut self) -> EventContext<'_, 'window> { +impl<'context, 'clip, 'gfx, 'pass> AsEventContext for LayoutContext<'context, 'clip, 'gfx, 'pass> { + fn as_event_context(&mut self) -> EventContext<'_> { self.graphics.as_event_context() } } -impl<'context, 'window, 'clip, 'gfx, 'pass> Deref - for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> -{ - type Target = GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>; +impl<'context, 'clip, 'gfx, 'pass> Deref for LayoutContext<'context, 'clip, 'gfx, 'pass> { + type Target = GraphicsContext<'context, 'clip, 'gfx, 'pass>; fn deref(&self) -> &Self::Target { &self.graphics } } -impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut - for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> -{ +impl<'context, 'clip, 'gfx, 'pass> DerefMut for LayoutContext<'context, 'clip, 'gfx, 'pass> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.graphics } } /// Converts from one context to an [`EventContext`]. -pub trait AsEventContext<'window> { +pub trait AsEventContext { /// Returns this context as an [`EventContext`]. - fn as_event_context(&mut self) -> EventContext<'_, 'window>; + fn as_event_context(&mut self) -> EventContext<'_>; /// Pushes a new child widget into the widget hierarchy beneathq the /// context's widget. @@ -837,14 +821,14 @@ pub trait AsEventContext<'window> { } } -impl<'window> AsEventContext<'window> for EventContext<'_, 'window> { - fn as_event_context(&mut self) -> EventContext<'_, 'window> { +impl AsEventContext for EventContext<'_> { + fn as_event_context(&mut self) -> EventContext<'_> { EventContext::new(self.widget.borrowed(), self.kludgine) } } -impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, '_> { - fn as_event_context(&mut self) -> EventContext<'_, 'window> { +impl AsEventContext for GraphicsContext<'_, '_, '_, '_> { + fn as_event_context(&mut self) -> EventContext<'_> { EventContext::new(self.widget.borrowed(), &mut self.gfx) } } @@ -853,10 +837,10 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, ' /// /// This type provides access to the widget hierarchy from the perspective of a /// specific widget. -pub struct WidgetContext<'context, 'window> { +pub struct WidgetContext<'context> { current_node: MountedWidget, pub(crate) tree: Tree, - window: &'context mut RunningWindow<'window>, + window: &'context mut dyn PlatformWindow, theme: Cow<'context, ThemePair>, cursor: &'context mut CursorState, pending_state: PendingState<'context>, @@ -864,11 +848,11 @@ pub struct WidgetContext<'context, 'window> { cache: WidgetCacheKey, } -impl<'context, 'window> WidgetContext<'context, 'window> { +impl<'context> WidgetContext<'context> { pub(crate) fn new( current_node: MountedWidget, theme: &'context ThemePair, - window: &'context mut RunningWindow<'window>, + window: &'context mut dyn PlatformWindow, theme_mode: ThemeMode, cursor: &'context mut CursorState, ) -> Self { @@ -899,7 +883,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } /// Returns a new instance that borrows from `self`. - pub fn borrowed(&mut self) -> WidgetContext<'_, 'window> { + pub fn borrowed(&mut self) -> WidgetContext<'_> { WidgetContext { tree: self.tree.clone(), current_node: self.current_node.clone(), @@ -916,10 +900,10 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub fn for_other<'child, Widget>( &'child mut self, widget: &Widget, - ) -> >>::Result + ) -> >>::Result where Widget: ManageWidget, - Widget::Managed: MapManagedWidget>, + Widget::Managed: MapManagedWidget>, { widget.manage(self).map(|current_node| { let (effective_styles, theme, theme_mode) = current_node.overidden_theme(); @@ -1159,13 +1143,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Returns the window containing this widget. #[must_use] - pub fn window(&self) -> &RunningWindow<'window> { + pub fn window(&self) -> &dyn PlatformWindow { self.window } /// Returns an exclusive reference to the window containing this widget. #[must_use] - pub fn window_mut(&mut self) -> &mut RunningWindow<'window> { + pub fn window_mut(&mut self) -> &mut dyn PlatformWindow { self.window } @@ -1201,9 +1185,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { } } -impl dyn AsEventContext<'_> {} - -impl Drop for EventContext<'_, '_> { +impl Drop for EventContext<'_> { fn drop(&mut self) { if matches!(self.widget.pending_state, PendingState::Owned(_)) { self.apply_pending_state(); @@ -1211,17 +1193,17 @@ impl Drop for EventContext<'_, '_> { } } -impl<'window> Deref for WidgetContext<'_, 'window> { - type Target = RunningWindow<'window>; +impl<'context> Deref for WidgetContext<'context> { + type Target = &'context mut dyn PlatformWindow; fn deref(&self) -> &Self::Target { - self.window + &self.window } } -impl<'window> DerefMut for WidgetContext<'_, 'window> { +impl DerefMut for WidgetContext<'_> { fn deref_mut(&mut self) -> &mut Self::Target { - self.window + &mut self.window } } @@ -1270,13 +1252,13 @@ pub trait ManageWidget { type Managed: MapManagedWidget; /// Resolve `self` into a [`MountedWidget`]. - fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed; + fn manage(&self, context: &WidgetContext<'_>) -> Self::Managed; } impl ManageWidget for WidgetId { type Managed = Option; - fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed { + fn manage(&self, context: &WidgetContext<'_>) -> Self::Managed { context.tree.widget(*self) } } @@ -1284,7 +1266,7 @@ impl ManageWidget for WidgetId { impl ManageWidget for WidgetInstance { type Managed = Option; - fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed { + fn manage(&self, context: &WidgetContext<'_>) -> Self::Managed { context.tree.widget(self.id()) } } @@ -1292,7 +1274,7 @@ impl ManageWidget for WidgetInstance { impl ManageWidget for MountedWidget { type Managed = Self; - fn manage(&self, _context: &WidgetContext<'_, '_>) -> Self::Managed { + fn manage(&self, _context: &WidgetContext<'_>) -> Self::Managed { self.clone() } } @@ -1348,7 +1330,7 @@ pub trait Trackable: sealed::Trackable { /// Marks the widget for redraw when this value is updated. /// /// This function has no effect if the value is constant. - fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) + fn redraw_when_changed(&self, context: &WidgetContext<'_>) where Self: Sized, { @@ -1358,7 +1340,7 @@ pub trait Trackable: sealed::Trackable { /// Marks the widget for redraw when this value is updated. /// /// This function has no effect if the value is constant. - fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) + fn invalidate_when_changed(&self, context: &WidgetContext<'_>) where Self: Sized, { diff --git a/src/debug.rs b/src/debug.rs index bcf98fa..95c3052 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -8,7 +8,7 @@ use crate::value::{Dynamic, DynamicReader, ForEach, Source, WeakDynamic}; use crate::widget::{Children, MakeWidget, WidgetInstance}; use crate::widgets::grid::{Grid, GridWidgets}; use crate::window::Window; -use crate::Open; +use crate::{Open, PendingApp}; /// A widget that can provide extra information when debugging. #[derive(Clone, Default)] @@ -122,7 +122,7 @@ impl Open for DebugContext { self.into_window().open(app) } - fn run_in(self, app: crate::PendingApp) -> crate::Result { + fn run_in(self, app: PendingApp) -> crate::Result { self.into_window().run_in(app) } } diff --git a/src/styles.rs b/src/styles.rs index 14929c6..9fc7b61 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -114,7 +114,7 @@ impl Styles { &self, component: &impl NamedComponent, fallback: &Fallback, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, ) -> Fallback::ComponentType where Fallback: ComponentDefinition + ?Sized, @@ -127,10 +127,7 @@ impl Styles { .unwrap_or_else(|| fallback.default_value(context)) } - fn resolve_component( - component: &Value, - context: &WidgetContext<'_, '_>, - ) -> Option + fn resolve_component(component: &Value, context: &WidgetContext<'_>) -> Option where T: ComponentType, { @@ -162,7 +159,7 @@ impl Styles { pub fn try_get( &self, component: &Named, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, ) -> Option where Named: ComponentDefinition + ?Sized, @@ -176,11 +173,7 @@ impl Styles { /// Returns the component associated with the given name, or if not found, /// returns the default value provided by the definition. #[must_use] - pub fn get( - &self, - component: &Named, - context: &WidgetContext<'_, '_>, - ) -> Named::ComponentType + pub fn get(&self, component: &Named, context: &WidgetContext<'_>) -> Named::ComponentType where Named: ComponentDefinition + ?Sized, { @@ -440,10 +433,8 @@ impl Component { #[must_use] pub fn dynamic(resolve: Func) -> Self where - Func: for<'a, 'context, 'widget> Fn(&'a WidgetContext<'context, 'widget>) -> Option - + Send - + Sync - + 'static, + Func: + for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option + Send + Sync + 'static, T: ComponentType, { Self::Dynamic(DynamicComponent::new(move |context| { @@ -1099,7 +1090,7 @@ pub trait ComponentDefinition: NamedComponent { type ComponentType: ComponentType; /// Returns the default value to use for this component. - fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType; + fn default_value(&self, context: &WidgetContext<'_>) -> Self::ComponentType; } /// Describes whether a type should invalidate a widget. @@ -2535,19 +2526,19 @@ impl PartialEq for DynamicComponent { /// A type that resolves to a [`Component`] at runtime. pub trait DynamicComponentResolver: Send + Sync + 'static { /// Returns the effective component, if one should be applied. - fn resolve_component(&self, context: &WidgetContext<'_, '_>) -> Option; + fn resolve_component(&self, context: &WidgetContext<'_>) -> Option; } struct DynamicFunctionWrapper(F); impl DynamicComponentResolver for DynamicFunctionWrapper where - T: for<'a, 'context, 'widget> Fn(&'a WidgetContext<'context, 'widget>) -> Option + T: for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option + Send + Sync + 'static, { - fn resolve_component(&self, context: &WidgetContext<'_, '_>) -> Option { + fn resolve_component(&self, context: &WidgetContext<'_>) -> Option { self.0(context) } } @@ -2556,7 +2547,7 @@ impl DynamicComponentResolver for T where T: ComponentDefinition + Clone + Send + Sync + 'static, { - fn resolve_component(&self, context: &WidgetContext<'_, '_>) -> Option { + fn resolve_component(&self, context: &WidgetContext<'_>) -> Option { Some(context.get(self).into_component()) } } @@ -2576,7 +2567,7 @@ impl DynamicComponent { #[must_use] pub fn new(resolve: Func) -> Self where - Func: for<'a, 'context, 'widget> Fn(&'a WidgetContext<'context, 'widget>) -> Option + Func: for<'a, 'context> Fn(&'a WidgetContext<'context>) -> Option + Send + Sync + 'static, @@ -2587,7 +2578,7 @@ impl DynamicComponent { /// Invokes the resolver function, optionally returning a resolved /// component. #[must_use] - pub fn resolve(&self, context: &WidgetContext<'_, '_>) -> Option { + pub fn resolve(&self, context: &WidgetContext<'_>) -> Option { self.0.resolve_component(context) } } diff --git a/src/styles/components.rs b/src/styles/components.rs index 58ee737..8cd92f2 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -69,7 +69,7 @@ macro_rules! define_components { define_components!($type, |context| context.theme().$($path)*); }; ($type:ty, |$context:ident| $($expr:tt)*) => { - fn default_value(&self, $context: &WidgetContext<'_, '_>) -> $type { + fn default_value(&self, $context: &WidgetContext<'_>) -> $type { $($expr)* } }; diff --git a/src/tick.rs b/src/tick.rs index daad98e..b1dfe70 100644 --- a/src/tick.rs +++ b/src/tick.rs @@ -25,7 +25,7 @@ pub struct Tick { impl Tick { /// Signals that this widget has been redrawn. - pub fn rendered(&self, context: &WidgetContext<'_, '_>) { + pub fn rendered(&self, context: &WidgetContext<'_>) { context.redraw_when_changed(&self.data.tick_number); self.data.sync.notify_one(); diff --git a/src/value.rs b/src/value.rs index 265d931..0f7ead2 100644 --- a/src/value.rs +++ b/src/value.rs @@ -103,7 +103,7 @@ pub trait Source { /// This function panics if this value is already locked by the current /// thread. #[must_use] - fn get_tracking_redraw(&self, context: &WidgetContext<'_, '_>) -> T + fn get_tracking_redraw(&self, context: &WidgetContext<'_>) -> T where T: Clone, Self: Trackable + Sized, @@ -121,7 +121,7 @@ pub trait Source { /// This function panics if this value is already locked by the current /// thread. #[must_use] - fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T + fn get_tracking_invalidate(&self, context: &WidgetContext<'_>) -> T where T: Clone, Self: Trackable + Sized, @@ -2508,7 +2508,7 @@ impl Value { /// updated. pub fn map_tracking_redraw( &self, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, map: impl FnOnce(&T) -> R, ) -> R { match self { @@ -2526,7 +2526,7 @@ impl Value { /// updated. pub fn map_tracking_invalidate( &self, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, map: impl FnOnce(&T) -> R, ) -> R { match self { @@ -2573,7 +2573,7 @@ impl Value { /// /// If `self` is a dynamic, `context` will be refreshed when the value is /// updated. - pub fn get_tracking_redraw(&self, context: &WidgetContext<'_, '_>) -> T + pub fn get_tracking_redraw(&self, context: &WidgetContext<'_>) -> T where T: Clone, { @@ -2584,7 +2584,7 @@ impl Value { /// /// If `self` is a dynamic, `context` will be invalidated when the value is /// updated. - pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T + pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_>) -> T where T: Clone, { diff --git a/src/widget.rs b/src/widget.rs index 927d9e5..b1e43f3 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -45,7 +45,11 @@ use crate::widgets::{ Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space, Stack, Style, Themed, ThemedMode, Validated, Wrap, }; -use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior, WindowHandle, WindowLocal}; +use crate::window::sealed::WindowCommand; +use crate::window::{ + Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, VirtualWindowBuilder, Window, + WindowBehavior, WindowHandle, WindowLocal, +}; use crate::ConstraintLimit; /// A type that makes up a graphical user interface. @@ -264,7 +268,7 @@ use crate::ConstraintLimit; /// [repo]: https://github.com/khonsulabs/cushy pub trait Widget: Send + Debug + 'static { /// Redraw the contents of this widget. - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>); /// Writes a summary of this widget into `fmt`. /// @@ -297,50 +301,46 @@ pub trait Widget: Send + Debug + 'static { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { available_space.map(ConstraintLimit::min) } /// The widget has been mounted into a parent widget. #[allow(unused_variables)] - fn mounted(&mut self, context: &mut EventContext<'_, '_>) {} + fn mounted(&mut self, context: &mut EventContext<'_>) {} /// The widget has been removed from its parent widget. #[allow(unused_variables)] - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {} + fn unmounted(&mut self, context: &mut EventContext<'_>) {} /// Returns true if this widget should respond to mouse input at `location`. #[allow(unused_variables)] - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { false } /// The widget is currently has a cursor hovering it at `location`. #[allow(unused_variables)] - fn hover( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, location: Point, context: &mut EventContext<'_>) -> Option { None } /// The widget is no longer being hovered. #[allow(unused_variables)] - fn unhover(&mut self, context: &mut EventContext<'_, '_>) {} + fn unhover(&mut self, context: &mut EventContext<'_>) {} /// This widget has been targeted to be focused. If this function returns /// true, the widget will be focused. If false, Cushy will continue /// searching for another focus target. #[allow(unused_variables)] - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { false } /// The widget has received focus for user input. #[allow(unused_variables)] - fn focus(&mut self, context: &mut EventContext<'_, '_>) {} + fn focus(&mut self, context: &mut EventContext<'_>) {} /// The widget should switch to the next focusable area within this widget, /// honoring `direction` in a consistent manner. Returning `HANDLED` will @@ -349,7 +349,7 @@ pub trait Widget: Send + Debug + 'static { fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -357,21 +357,21 @@ pub trait Widget: Send + Debug + 'static { /// The widget is about to lose focus. Returning true allows the focus to /// switch away from this widget. #[allow(unused_variables)] - fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn allow_blur(&mut self, context: &mut EventContext<'_>) -> bool { true } /// The widget is no longer focused for user input. #[allow(unused_variables)] - fn blur(&mut self, context: &mut EventContext<'_, '_>) {} + fn blur(&mut self, context: &mut EventContext<'_>) {} /// The widget has become the active widget. #[allow(unused_variables)] - fn activate(&mut self, context: &mut EventContext<'_, '_>) {} + fn activate(&mut self, context: &mut EventContext<'_>) {} /// The widget is no longer active. #[allow(unused_variables)] - fn deactivate(&mut self, context: &mut EventContext<'_, '_>) {} + fn deactivate(&mut self, context: &mut EventContext<'_>) {} /// A mouse button event has occurred at `location`. Returns whether the /// event has been handled or not. @@ -384,7 +384,7 @@ pub trait Widget: Send + Debug + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -397,7 +397,7 @@ pub trait Widget: Send + Debug + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -408,7 +408,7 @@ pub trait Widget: Send + Debug + 'static { location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -420,7 +420,7 @@ pub trait Widget: Send + Debug + 'static { device_id: DeviceId, input: KeyEvent, is_synthetic: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -428,7 +428,7 @@ pub trait Widget: Send + Debug + 'static { /// An input manager event has been sent to this widget. Returns whether the /// event has been handled or not. #[allow(unused_variables)] - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling { IGNORED } @@ -440,7 +440,7 @@ pub trait Widget: Send + Debug + 'static { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -451,7 +451,7 @@ pub trait Widget: Send + Debug + 'static { #[allow(unused_variables)] fn root_behavior( &mut self, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option<(RootBehavior, WidgetInstance)> { None } @@ -544,7 +544,7 @@ pub trait WrapperWidget: Debug + Send + 'static { /// Returns the behavior this widget should apply when positioned at the /// root of the window. #[allow(unused_variables)] - fn root_behavior(&mut self, context: &mut EventContext<'_, '_>) -> Option { + fn root_behavior(&mut self, context: &mut EventContext<'_>) -> Option { None } @@ -552,13 +552,13 @@ pub trait WrapperWidget: Debug + Send + 'static { /// /// This is invoked before the wrapped widget is drawn. #[allow(unused_variables)] - fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {} + fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {} /// Draws the foreground of the widget. /// /// This is invoked after the wrapped widget is drawn. #[allow(unused_variables)] - fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {} + fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {} /// Returns the rectangle that the child widget should occupy given /// `available_space`. @@ -566,7 +566,7 @@ pub trait WrapperWidget: Debug + Send + 'static { fn layout_child( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let adjusted_space = self.adjust_child_constraints(available_space, context); let child = self.child_mut().mounted(&mut context.as_event_context()); @@ -584,7 +584,7 @@ pub trait WrapperWidget: Debug + Send + 'static { fn adjust_child_constraints( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { available_space } @@ -596,7 +596,7 @@ pub trait WrapperWidget: Debug + Send + 'static { &mut self, size: Size, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { Size::new( available_space @@ -612,7 +612,7 @@ pub trait WrapperWidget: Debug + Send + 'static { /// Returns the background color to render behind the wrapped widget. #[allow(unused_variables)] #[must_use] - fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + fn background_color(&mut self, context: &WidgetContext<'_>) -> Option { // WidgetBackground is already filled, so we don't need to do anything // else by default. None @@ -620,39 +620,35 @@ pub trait WrapperWidget: Debug + Send + 'static { /// The widget has been mounted into a parent widget. #[allow(unused_variables)] - fn mounted(&mut self, context: &mut EventContext<'_, '_>) {} + fn mounted(&mut self, context: &mut EventContext<'_>) {} /// The widget has been removed from its parent widget. #[allow(unused_variables)] - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { self.child_mut().unmount_in(context); } /// Returns true if this widget should respond to mouse input at `location`. #[allow(unused_variables)] - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { false } /// The widget is currently has a cursor hovering it at `location`. #[allow(unused_variables)] - fn hover( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, location: Point, context: &mut EventContext<'_>) -> Option { None } /// The widget is no longer being hovered. #[allow(unused_variables)] - fn unhover(&mut self, context: &mut EventContext<'_, '_>) {} + fn unhover(&mut self, context: &mut EventContext<'_>) {} /// This widget has been targeted to be focused. If this function returns /// true, the widget will be focused. If false, Cushy will continue /// searching for another focus target. #[allow(unused_variables)] - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { false } @@ -663,33 +659,33 @@ pub trait WrapperWidget: Debug + Send + 'static { fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } /// The widget has received focus for user input. #[allow(unused_variables)] - fn focus(&mut self, context: &mut EventContext<'_, '_>) {} + fn focus(&mut self, context: &mut EventContext<'_>) {} /// The widget is about to lose focus. Returning true allows the focus to /// switch away from this widget. #[allow(unused_variables)] - fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn allow_blur(&mut self, context: &mut EventContext<'_>) -> bool { true } /// The widget is no longer focused for user input. #[allow(unused_variables)] - fn blur(&mut self, context: &mut EventContext<'_, '_>) {} + fn blur(&mut self, context: &mut EventContext<'_>) {} /// The widget has become the active widget. #[allow(unused_variables)] - fn activate(&mut self, context: &mut EventContext<'_, '_>) {} + fn activate(&mut self, context: &mut EventContext<'_>) {} /// The widget is no longer active. #[allow(unused_variables)] - fn deactivate(&mut self, context: &mut EventContext<'_, '_>) {} + fn deactivate(&mut self, context: &mut EventContext<'_>) {} /// A mouse button event has occurred at `location`. Returns whether the /// event has been handled or not. @@ -702,7 +698,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -715,7 +711,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -726,7 +722,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -738,7 +734,7 @@ pub trait WrapperWidget: Debug + Send + 'static { device_id: DeviceId, input: KeyEvent, is_synthetic: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -746,7 +742,7 @@ pub trait WrapperWidget: Debug + Send + 'static { /// An input manager event has been sent to this widget. Returns whether the /// event has been handled or not. #[allow(unused_variables)] - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling { IGNORED } @@ -758,7 +754,7 @@ pub trait WrapperWidget: Debug + Send + 'static { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -770,13 +766,13 @@ where { fn root_behavior( &mut self, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option<(RootBehavior, WidgetInstance)> { T::root_behavior(self, context) .map(|behavior| (behavior, T::child_mut(self).widget().clone())) } - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let background_color = self.background_color(context); if let Some(color) = background_color { context.fill(color); @@ -793,7 +789,7 @@ where fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let layout = self.layout_child(available_space, context); let child = self.child_mut().mounted(&mut context.as_event_context()); @@ -801,47 +797,43 @@ where layout.size } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { T::mounted(self, context); } - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { T::unmounted(self, context); } - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { T::hit_test(self, location, context) } - fn hover( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, location: Point, context: &mut EventContext<'_>) -> Option { T::hover(self, location, context) } - fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_>) { T::unhover(self, context); } - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { T::accept_focus(self, context) } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { T::focus(self, context); } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { T::blur(self, context); } - fn activate(&mut self, context: &mut EventContext<'_, '_>) { + fn activate(&mut self, context: &mut EventContext<'_>) { T::activate(self, context); } - fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { + fn deactivate(&mut self, context: &mut EventContext<'_>) { T::deactivate(self, context); } @@ -850,7 +842,7 @@ where location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { T::mouse_down(self, location, device_id, button, context) } @@ -860,7 +852,7 @@ where location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { T::mouse_drag(self, location, device_id, button, context); } @@ -870,7 +862,7 @@ where location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { T::mouse_up(self, location, device_id, button, context); } @@ -880,12 +872,12 @@ where device_id: DeviceId, input: KeyEvent, is_synthetic: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { T::keyboard_input(self, device_id, input, is_synthetic, context) } - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling { T::ime(self, ime, context) } @@ -894,7 +886,7 @@ where device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { T::mouse_wheel(self, device_id, delta, phase, context) } @@ -902,12 +894,12 @@ where fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { T::advance_focus(self, direction, context) } - fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn allow_blur(&mut self, context: &mut EventContext<'_>) -> bool { T::allow_blur(self, context) } @@ -926,6 +918,16 @@ pub trait MakeWidget: Sized { Window::new(self.make_widget()) } + /// Returns a builder for a [`VirtualWindow`](crate::window::VirtualWindow). + fn build_virtual_window(self) -> VirtualWindowBuilder { + VirtualWindowBuilder::new(self) + } + + /// Returns a builder for a [`VirtualRecorder`](crate::window::VirtualRecorder) + fn build_recorder(self) -> VirtualRecorderBuilder { + VirtualRecorderBuilder::new(self) + } + /// Associates `styles` with this widget. /// /// This is equivalent to `Style::new(styles, self)`. @@ -1589,7 +1591,10 @@ impl PartialEq for WidgetInstance { impl WindowBehavior for WidgetInstance { type Context = Self; - fn initialize(_window: &mut RunningWindow<'_>, context: Self::Context) -> Self { + fn initialize( + _window: &mut RunningWindow>, + context: Self::Context, + ) -> Self { context } @@ -2181,11 +2186,7 @@ where T: MountableChild, { /// Mounts and unmounts all children needed to be in sync with `children`. - pub fn synchronize_with( - &mut self, - children: &Value, - context: &mut EventContext<'_, '_>, - ) { + pub fn synchronize_with(&mut self, children: &Value, context: &mut EventContext<'_>) { let current_generation = children.generation(); if current_generation.map_or_else( || children.map(Children::len) != self.children.len(), @@ -2284,10 +2285,7 @@ impl WidgetRef { } /// Returns this child, mounting it in the process if necessary. - fn mounted_for_context<'window>( - &mut self, - context: &mut impl AsEventContext<'window>, - ) -> &MountedWidget { + fn mounted_for_context(&mut self, context: &mut impl AsEventContext) -> &MountedWidget { let mut context = context.as_event_context(); self.mounted .entry(&context) @@ -2295,21 +2293,18 @@ impl WidgetRef { } /// Returns this child, mounting it in the process if necessary. - pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) { + pub fn mount_if_needed(&mut self, context: &mut impl AsEventContext) { self.mounted_for_context(context); } /// Returns this child, mounting it in the process if necessary. - pub fn mounted<'window>( - &mut self, - context: &mut impl AsEventContext<'window>, - ) -> MountedWidget { + pub fn mounted(&mut self, context: &mut impl AsEventContext) -> MountedWidget { self.mounted_for_context(context).clone() } /// Returns this child, mounting it in the process if necessary. #[must_use] - pub fn as_mounted(&self, context: &WidgetContext<'_, '_>) -> Option<&MountedWidget> { + pub fn as_mounted(&self, context: &WidgetContext<'_>) -> Option<&MountedWidget> { self.mounted.get(context) } @@ -2320,7 +2315,7 @@ impl WidgetRef { } /// Unmounts this widget from the window belonging to `context`, if needed. - pub fn unmount_in<'window>(&mut self, context: &mut impl AsEventContext<'window>) { + pub fn unmount_in(&mut self, context: &mut impl AsEventContext) { let mut context = context.as_event_context(); if let Some(mounted) = self.mounted.clear_for(&context) { context.remove_child(&mounted); @@ -2351,7 +2346,7 @@ impl PartialEq for WidgetRef { impl ManageWidget for WidgetRef { type Managed = Option; - fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed { + fn manage(&self, context: &WidgetContext<'_>) -> Self::Managed { self.mounted .get(context) .cloned() @@ -2374,7 +2369,7 @@ impl WidgetId { /// Finds this widget mounted in this window, if present. #[must_use] - pub fn find_in(self, context: &WidgetContext<'_, '_>) -> Option { + pub fn find_in(self, context: &WidgetContext<'_>) -> Option { context.tree.widget(self) } } diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 9f2219c..34c51da 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -87,7 +87,7 @@ impl Align { fn measure( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Layout { let margin = self.edges.get(); let vertical = FrameInfo::new(context.gfx.scale(), margin.top, margin.bottom); @@ -179,14 +179,14 @@ impl WrapperWidget for Align { &mut self.child } - fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + fn root_behavior(&mut self, _context: &mut EventContext<'_>) -> Option { Some(RootBehavior::Align) } fn layout_child( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let layout = self.measure(available_space, context); diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 016d0d4..55d0c82 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -71,7 +71,7 @@ impl ButtonKind { pub fn colors_for_default( self, visual_state: VisualState, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, ) -> ButtonColors { match self { ButtonKind::Solid => match visual_state { @@ -171,7 +171,7 @@ impl Button { self } - fn invoke_on_click(&mut self, context: &WidgetContext<'_, '_>) { + fn invoke_on_click(&mut self, context: &WidgetContext<'_>) { if context.enabled() { if let Some(on_click) = self.on_click.as_mut() { on_click.invoke(()); @@ -179,7 +179,7 @@ impl Button { } } - fn visual_style(context: &WidgetContext<'_, '_>) -> VisualState { + fn visual_style(context: &WidgetContext<'_>) -> VisualState { if !context.enabled() { VisualState::Disabled } else if context.active() { @@ -195,7 +195,7 @@ impl Button { #[must_use] pub fn colors_for_transparent( visual_state: VisualState, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, ) -> ButtonColors { match visual_state { VisualState::Normal => ButtonColors { @@ -225,7 +225,7 @@ impl Button { } } - fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { + fn determine_stateful_colors(&mut self, context: &mut WidgetContext<'_>) -> ButtonColors { let kind = self.kind.get_tracking_redraw(context); let visual_state = Self::visual_style(context); @@ -247,7 +247,7 @@ impl Button { } } - fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) { + fn update_colors(&mut self, context: &mut WidgetContext<'_>, immediate: bool) { let new_style = self.determine_stateful_colors(context); let window_local = self.per_window.entry(context).or_default(); @@ -271,7 +271,7 @@ impl Button { } } - fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { + fn current_style(&mut self, context: &mut WidgetContext<'_>) -> ButtonColors { if self .per_window .entry(context) @@ -318,7 +318,7 @@ impl VisualState { /// Returns the colors to apply to a [`ButtonKind::Solid`] [`Button`] or /// button-like widget. #[must_use] - pub fn solid_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors { + pub fn solid_colors(self, context: &WidgetContext<'_>) -> ButtonColors { match self { VisualState::Normal => ButtonColors { background: context.get(&ButtonBackground), @@ -346,7 +346,7 @@ impl VisualState { /// Returns the colors to apply to a [`ButtonKind::Outline`] [`Button`] or /// button-like widget. #[must_use] - pub fn outline_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors { + pub fn outline_colors(self, context: &WidgetContext<'_>) -> ButtonColors { let solid = self.solid_colors(context); ButtonColors { background: solid.outline, @@ -364,7 +364,7 @@ impl Widget for Button { .finish() } - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { #![allow(clippy::similar_names)] let current_style = self.kind.get_tracking_redraw(context); @@ -417,11 +417,11 @@ impl Widget for Button { context.for_other(&content).redraw(); } - fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { true } - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { self.focusable && context.enabled() && context.get(&AutoFocusableControls).is_all() } @@ -430,7 +430,7 @@ impl Widget for Button { _location: Point, _device_id: DeviceId, _button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { self.per_window.entry(context).or_default().buttons_pressed += 1; context.activate(); @@ -442,7 +442,7 @@ impl Widget for Button { location: Point, _device_id: DeviceId, _button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { let changed = if Rect::from(context.last_layout().expect("must have been rendered").size) .contains(location) @@ -462,7 +462,7 @@ impl Widget for Button { location: Option>, _device_id: DeviceId, _button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { let window_local = self.per_window.entry(context).or_default(); window_local.buttons_pressed -= 1; @@ -484,7 +484,7 @@ impl Widget for Button { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let padding = context .get(&IntrinsicPadding) @@ -502,14 +502,14 @@ impl Widget for Button { size + double_padding } - fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_>) { self.update_colors(context, false); } fn hover( &mut self, _location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { self.update_colors(context, false); @@ -520,15 +520,15 @@ impl Widget for Button { } } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { context.set_needs_redraw(); } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { context.set_needs_redraw(); } - fn activate(&mut self, context: &mut EventContext<'_, '_>) { + fn activate(&mut self, context: &mut EventContext<'_>) { let window_local = self.per_window.entry(context).or_default(); // If we have no buttons pressed, the event should fire on activate not // on deactivate. @@ -538,11 +538,11 @@ impl Widget for Button { self.update_colors(context, true); } - fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { + fn deactivate(&mut self, context: &mut EventContext<'_>) { self.update_colors(context, false); } - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { self.content.unmount_in(context); } } diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index 0bf58c2..02e0c1d 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -20,8 +20,8 @@ impl Canvas { /// Returns a new canvas that draws its contents by invoking `render`. pub fn new(render: F) -> Self where - F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut( - &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + F: for<'clip, 'gfx, 'pass, 'context> FnMut( + &mut GraphicsContext<'context, 'clip, 'gfx, 'pass>, ) + Send + 'static, { @@ -40,7 +40,7 @@ impl Canvas { } impl Widget for Canvas { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { context.redraw_when_changed(&self.redraw); self.render.render(context); if let Some(tick) = &self.tick { @@ -51,7 +51,7 @@ impl Widget for Canvas { fn layout( &mut self, available_space: Size, - _context: &mut LayoutContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { available_space.map(ConstraintLimit::max) } @@ -64,17 +64,16 @@ impl Debug for Canvas { } trait RenderFunction: Send + 'static { - fn render(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); + fn render(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>); } impl RenderFunction for F where - F: for<'clip, 'gfx, 'pass, 'context, 'window> FnMut( - &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, - ) + Send + F: for<'clip, 'gfx, 'pass, 'context> FnMut(&mut GraphicsContext<'context, 'clip, 'gfx, 'pass>) + + Send + 'static, { - fn render(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn render(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self(context); } } diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index 31c09e4..8ed906c 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -175,7 +175,7 @@ struct CheckboxOrnament { } impl Widget for CheckboxOrnament { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let checkbox_size = context .gfx .region() @@ -244,7 +244,7 @@ impl Widget for CheckboxOrnament { fn layout( &mut self, _available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let checkbox_size = context.get(&CheckboxSize).into_upx(context.gfx.scale()); Size::squared(checkbox_size) diff --git a/src/widgets/collapse.rs b/src/widgets/collapse.rs index d89caac..57bd515 100644 --- a/src/widgets/collapse.rs +++ b/src/widgets/collapse.rs @@ -51,7 +51,7 @@ impl Collapse { } } - fn note_child_size(&mut self, size: Px, context: &mut LayoutContext<'_, '_, '_, '_, '_>) { + fn note_child_size(&mut self, size: Px, context: &mut LayoutContext<'_, '_, '_, '_>) { let (easing, target) = if self.collapse.get_tracking_invalidate(context) { (EasingFunction::from(EaseOutQuadradic), Px::ZERO) } else { @@ -90,7 +90,7 @@ impl WrapperWidget for Collapse { &mut self, size: Size, _available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let clip_size = self.size.get_tracking_invalidate(context); if self.vertical { diff --git a/src/widgets/color.rs b/src/widgets/color.rs index 624b5d6..86c59f6 100644 --- a/src/widgets/color.rs +++ b/src/widgets/color.rs @@ -69,7 +69,7 @@ impl ColorSourcePicker { } impl Widget for ColorSourcePicker { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let loupe_size = Lp::mm(3).into_px(context.gfx.scale()); let size = context.gfx.region().size; @@ -148,7 +148,7 @@ impl Widget for ColorSourcePicker { ); } - fn hit_test(&mut self, location: Point, _context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, _context: &mut EventContext<'_>) -> bool { self.visible_rect.contains(location) } @@ -157,7 +157,7 @@ impl Widget for ColorSourcePicker { location: Point, _device_id: DeviceId, _button: MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) -> EventHandling { self.update_from_mouse(location); HANDLED @@ -168,7 +168,7 @@ impl Widget for ColorSourcePicker { location: Point, _device_id: DeviceId, _button: MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { self.update_from_mouse(location); } @@ -180,7 +180,7 @@ fn draw_gradient_segment( height: Px, hue: Range, lightness: ZeroToOne, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, ) { let mid_left = ( Point::new(start.x, start.y + height / 2), diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 254503e..00c0e40 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -158,7 +158,7 @@ impl Container { self } - fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_, '_>) -> Edges { + fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_>) -> Edges { match &self.padding { Some(padding) => padding.get(), None => Edges::from(context.get(&IntrinsicPadding)), @@ -166,7 +166,7 @@ impl Container { .map(|dim| dim.into_px(context.gfx.scale()).round()) } - fn effective_background_color(&mut self, context: &WidgetContext<'_, '_>) -> kludgine::Color { + fn effective_background_color(&mut self, context: &WidgetContext<'_>) -> kludgine::Color { let background = match self.background.get() { ContainerBackground::Color(color) => EffectiveBackground::Color(color), ContainerBackground::Level(level) => EffectiveBackground::Level(level), @@ -206,7 +206,7 @@ impl Widget for Container { .finish() } - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { self.child.unmount_in(context); } @@ -215,7 +215,7 @@ impl Widget for Container { } #[allow(clippy::too_many_lines)] - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let opacity = context.get(&Opacity); let background = self.effective_background_color(context); @@ -251,7 +251,7 @@ impl Widget for Container { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let child = self.child.mounted(context); @@ -308,7 +308,7 @@ impl Widget for Container { fn root_behavior( &mut self, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option<(RootBehavior, WidgetInstance)> { let mut padding = self .padding @@ -345,7 +345,7 @@ fn render_shadow( mut corner_radii: CornerRadii, shadow: &ContainerShadow, background: Color, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, ) { let shadow_color = shadow.color.unwrap_or_else(|| context.theme_pair().shadow); let shadow_color = @@ -588,7 +588,7 @@ fn shadow_arc( solid_color: Color, transparent_color: Color, start_angle: Angle, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, ) { let full_radius = radius + gradient; let mut current_outer_arc = origin + Point::new(full_radius, Px::ZERO).rotate_by(start_angle); diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs index 2b16eba..1553c93 100644 --- a/src/widgets/custom.rs +++ b/src/widgets/custom.rs @@ -121,8 +121,8 @@ impl Custom { where Redraw: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( - &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + + for<'context, 'clip, 'gfx, 'pass> FnMut( + &mut GraphicsContext<'context, 'clip, 'gfx, 'pass>, ), { self.redraw_background = Some(Box::new(redraw)); @@ -143,8 +143,8 @@ impl Custom { where Redraw: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( - &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + + for<'context, 'clip, 'gfx, 'pass> FnMut( + &mut GraphicsContext<'context, 'clip, 'gfx, 'pass>, ), { self.redraw_foreground = Some(Box::new(redraw)); @@ -156,8 +156,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::mounted`]. pub fn on_mounted(mut self, mounted: Mounted) -> Self where - Mounted: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Mounted: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.mounted = Some(Box::new(mounted)); self @@ -169,8 +168,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::unmounted`]. pub fn on_unmounted(mut self, mounted: Mounted) -> Self where - Mounted: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Mounted: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.unmounted = Some(Box::new(mounted)); self @@ -181,8 +179,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::unhover`]. pub fn on_unhover(mut self, unhovered: Unhover) -> Self where - Unhover: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Unhover: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.unhover = Some(Box::new(unhovered)); self @@ -193,8 +190,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::focus`]. pub fn on_focus(mut self, focus: Focused) -> Self where - Focused: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Focused: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.focus = Some(Box::new(focus)); self @@ -205,7 +201,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::blur`]. pub fn on_blur(mut self, blur: Blur) -> Self where - Blur: Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Blur: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.blur = Some(Box::new(blur)); self @@ -216,8 +212,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::activate`]. pub fn on_activate(mut self, activated: Activated) -> Self where - Activated: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Activated: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.activate = Some(Box::new(activated)); self @@ -228,8 +223,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::deactivate`]. pub fn on_deactivate(mut self, deactivated: Deactivated) -> Self where - Deactivated: - Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>), + Deactivated: Send + 'static + for<'context> FnMut(&mut EventContext<'context>), { self.deactivate = Some(Box::new(deactivated)); self @@ -241,9 +235,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::accept_focus`]. pub fn on_accept_focus(mut self, accept: AcceptFocus) -> Self where - AcceptFocus: Send - + 'static - + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> bool, + AcceptFocus: Send + 'static + for<'context> FnMut(&mut EventContext<'context>) -> bool, { self.accept_focus = Some(Box::new(accept)); self @@ -256,9 +248,7 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::allow_blur`]. pub fn on_allow_blur(mut self, allow_blur: AllowBlur) -> Self where - AllowBlur: Send - + 'static - + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> bool, + AllowBlur: Send + 'static + for<'context> FnMut(&mut EventContext<'context>) -> bool, { self.allow_blur = Some(Box::new(allow_blur)); self @@ -274,10 +264,7 @@ impl Custom { where AdvanceFocus: Send + 'static - + for<'context, 'window> FnMut( - VisualOrder, - &mut EventContext<'context, 'window>, - ) -> EventHandling, + + for<'context> FnMut(VisualOrder, &mut EventContext<'context>) -> EventHandling, { self.advance_focus = Some(Box::new(advance_focus)); self @@ -295,9 +282,9 @@ impl Custom { where AdjustChildConstraints: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + + for<'context, 'clip, 'gfx, 'pass> FnMut( Size, - &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> Size, { self.adjust_child = Some(Box::new(adjust_child_constraints)); @@ -311,10 +298,10 @@ impl Custom { where PositionChild: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + + for<'context, 'clip, 'gfx, 'pass> FnMut( Size, Size, - &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> WrappedLayout, { self.position_child = Some(Box::new(position_child)); @@ -327,9 +314,8 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::hit_test`]. pub fn on_hit_test(mut self, hit_test: HitTest) -> Self where - HitTest: Send - + 'static - + for<'context, 'window> FnMut(Point, &mut EventContext<'context, 'window>) -> bool, + HitTest: + Send + 'static + for<'context> FnMut(Point, &mut EventContext<'context>) -> bool, { self.hit_test = Some(Box::new(hit_test)); self @@ -342,10 +328,7 @@ impl Custom { where Hover: Send + 'static - + for<'context, 'window> FnMut( - Point, - &mut EventContext<'context, 'window>, - ) -> Option, + + for<'context> FnMut(Point, &mut EventContext<'context>) -> Option, { self.hover = Some(Box::new(hover)); self @@ -364,11 +347,11 @@ impl Custom { where MouseDown: Send + 'static - + for<'context, 'window> FnMut( + + for<'context> FnMut( Point, DeviceId, MouseButton, - &mut EventContext<'context, 'window>, + &mut EventContext<'context>, ) -> EventHandling, { self.mouse_down = Some(Box::new(mouse_down)); @@ -383,12 +366,7 @@ impl Custom { where MouseDrag: Send + 'static - + for<'context, 'window> FnMut( - Point, - DeviceId, - MouseButton, - &mut EventContext<'context, 'window>, - ), + + for<'context> FnMut(Point, DeviceId, MouseButton, &mut EventContext<'context>), { self.mouse_drag = Some(Box::new(mouse_drag)); self @@ -401,11 +379,11 @@ impl Custom { where MouseUp: Send + 'static - + for<'context, 'window> FnMut( + + for<'context> FnMut( Option>, DeviceId, MouseButton, - &mut EventContext<'context, 'window>, + &mut EventContext<'context>, ), { self.mouse_up = Some(Box::new(mouse_up)); @@ -417,9 +395,8 @@ impl Custom { /// This callback corresponds to [`WrapperWidget::ime`]. pub fn on_ime(mut self, ime: OnIme) -> Self where - OnIme: Send - + 'static - + for<'context, 'window> FnMut(Ime, &mut EventContext<'context, 'window>) -> EventHandling, + OnIme: + Send + 'static + for<'context> FnMut(Ime, &mut EventContext<'context>) -> EventHandling, { self.ime = Some(Box::new(ime)); self @@ -432,11 +409,11 @@ impl Custom { where KeyboardInput: Send + 'static - + for<'context, 'window> FnMut( + + for<'context> FnMut( DeviceId, KeyEvent, bool, - &mut EventContext<'context, 'window>, + &mut EventContext<'context>, ) -> EventHandling, { self.keyboard_input = Some(Box::new(keyboard_input)); @@ -450,11 +427,11 @@ impl Custom { where MouseWheel: Send + 'static - + for<'context, 'window> FnMut( + + for<'context> FnMut( DeviceId, MouseScrollDelta, TouchPhase, - &mut EventContext<'context, 'window>, + &mut EventContext<'context>, ) -> EventHandling, { self.mouse_wheel = Some(Box::new(mouse_wheel)); @@ -467,13 +444,13 @@ impl WrapperWidget for Custom { &mut self.child } - fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { if let Some(redraw) = &mut self.redraw_background { redraw.invoke(context); } } - fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { if let Some(redraw) = &mut self.redraw_foreground { redraw.invoke(context); } @@ -482,7 +459,7 @@ impl WrapperWidget for Custom { fn adjust_child_constraints( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { if let Some(adjust_child) = &mut self.adjust_child { adjust_child.invoke(available_space, context) @@ -495,7 +472,7 @@ impl WrapperWidget for Custom { &mut self, size: Size, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { if let Some(position_child) = &mut self.position_child { position_child.invoke(size, available_space, context) @@ -512,19 +489,19 @@ impl WrapperWidget for Custom { } } - fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + fn background_color(&mut self, context: &WidgetContext<'_>) -> Option { self.background .as_ref() .map(|bg| bg.get_tracking_redraw(context)) } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { if let Some(mounted) = &mut self.mounted { mounted.invoke(context); } } - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { if let Some(unmounted) = &mut self.unmounted { unmounted.invoke(context); } else { @@ -532,7 +509,7 @@ impl WrapperWidget for Custom { } } - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { if let Some(hit_test) = &mut self.hit_test { hit_test.invoke(location, context) } else { @@ -540,22 +517,18 @@ impl WrapperWidget for Custom { } } - fn hover( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, location: Point, context: &mut EventContext<'_>) -> Option { let hover = self.hover.as_mut()?; hover.invoke(location, context) } - fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_>) { if let Some(unhover) = &mut self.unhover { unhover.invoke(context); } } - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { if let Some(accept_focus) = &mut self.accept_focus { accept_focus.invoke(context) } else { @@ -563,25 +536,25 @@ impl WrapperWidget for Custom { } } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { if let Some(focus) = &mut self.focus { focus.invoke(context); } } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { if let Some(blur) = &mut self.blur { blur.invoke(context); } } - fn activate(&mut self, context: &mut EventContext<'_, '_>) { + fn activate(&mut self, context: &mut EventContext<'_>) { if let Some(activate) = &mut self.activate { activate.invoke(context); } } - fn deactivate(&mut self, context: &mut EventContext<'_, '_>) { + fn deactivate(&mut self, context: &mut EventContext<'_>) { if let Some(deactivate) = &mut self.deactivate { deactivate.invoke(context); } @@ -592,7 +565,7 @@ impl WrapperWidget for Custom { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(mouse_down) = &mut self.mouse_down { mouse_down.invoke(location, device_id, button, context) @@ -606,7 +579,7 @@ impl WrapperWidget for Custom { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { if let Some(mouse_drag) = &mut self.mouse_drag { mouse_drag.invoke(location, device_id, button, context); @@ -618,7 +591,7 @@ impl WrapperWidget for Custom { location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { if let Some(mouse_up) = &mut self.mouse_up { mouse_up.invoke(location, device_id, button, context); @@ -630,7 +603,7 @@ impl WrapperWidget for Custom { device_id: DeviceId, input: KeyEvent, is_synthetic: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(keyboard_input) = &mut self.keyboard_input { keyboard_input.invoke(device_id, input, is_synthetic, context) @@ -639,7 +612,7 @@ impl WrapperWidget for Custom { } } - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling { if let Some(f) = &mut self.ime { f.invoke(ime, context) } else { @@ -652,7 +625,7 @@ impl WrapperWidget for Custom { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(mouse_wheel) = &mut self.mouse_wheel { mouse_wheel.invoke(device_id, delta, phase, context) @@ -664,7 +637,7 @@ impl WrapperWidget for Custom { fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(advance_focus) = &mut self.advance_focus { advance_focus.invoke(direction, context) @@ -673,7 +646,7 @@ impl WrapperWidget for Custom { } } - fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn allow_blur(&mut self, context: &mut EventContext<'_>) -> bool { if let Some(allow_blur) = &mut self.allow_blur { allow_blur.invoke(context) } else { @@ -683,18 +656,16 @@ impl WrapperWidget for Custom { } trait RedrawFunc: Send { - fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); + fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>); } impl RedrawFunc for Func where Func: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( - &mut GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, - ), + + for<'context, 'clip, 'gfx, 'pass> FnMut(&mut GraphicsContext<'context, 'clip, 'gfx, 'pass>), { - fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn invoke(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self(context); } } @@ -703,7 +674,7 @@ trait AdjustChildConstraintsFunc: Send { fn invoke( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size; } @@ -711,15 +682,15 @@ impl AdjustChildConstraintsFunc for Func where Func: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + + for<'context, 'clip, 'gfx, 'pass> FnMut( Size, - &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> Size, { fn invoke( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self(available_space, context) } @@ -730,7 +701,7 @@ trait PositionChildFunc: Send { &mut self, size: Size, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout; } @@ -738,45 +709,44 @@ impl PositionChildFunc for Func where Func: Send + 'static - + for<'context, 'window, 'clip, 'gfx, 'pass> FnMut( + + for<'context, 'clip, 'gfx, 'pass> FnMut( Size, Size, - &mut LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>, + &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> WrappedLayout, { fn invoke( &mut self, size: Size, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { self(size, available_space, context) } } trait EventFunc: Send { - fn invoke(&mut self, context: &mut EventContext<'_, '_>) -> R; + fn invoke(&mut self, context: &mut EventContext<'_>) -> R; } impl EventFunc for Func where - Func: Send + 'static + for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> R, + Func: Send + 'static + for<'context> FnMut(&mut EventContext<'context>) -> R, { - fn invoke(&mut self, context: &mut EventContext<'_, '_>) -> R { + fn invoke(&mut self, context: &mut EventContext<'_>) -> R { self(context) } } trait OneParamEventFunc: Send { - fn invoke(&mut self, param: P, context: &mut EventContext<'_, '_>) -> R; + fn invoke(&mut self, param: P, context: &mut EventContext<'_>) -> R; } impl OneParamEventFunc for Func where - Func: - Send + 'static + for<'context, 'window> FnMut(P, &mut EventContext<'context, 'window>) -> R, + Func: Send + 'static + for<'context> FnMut(P, &mut EventContext<'context>) -> R, { - fn invoke(&mut self, location: P, context: &mut EventContext<'_, '_>) -> R { + fn invoke(&mut self, location: P, context: &mut EventContext<'_>) -> R { self(location, context) } } @@ -787,7 +757,7 @@ trait ThreeParamEventFunc: Send { location: P1, device_id: P2, button: P3, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> R; } @@ -795,16 +765,14 @@ type MouseUpFunc = dyn ThreeParamEventFunc>, DeviceId, MouseBut impl ThreeParamEventFunc for Func where - Func: Send - + 'static - + for<'context, 'window> FnMut(P1, P2, P3, &mut EventContext<'context, 'window>) -> R, + Func: Send + 'static + for<'context> FnMut(P1, P2, P3, &mut EventContext<'context>) -> R, { fn invoke( &mut self, location: P1, device_id: P2, button: P3, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> R { self(location, device_id, button, context) } diff --git a/src/widgets/disclose.rs b/src/widgets/disclose.rs index 321ca26..3c01365 100644 --- a/src/widgets/disclose.rs +++ b/src/widgets/disclose.rs @@ -122,7 +122,7 @@ impl DiscloseIndicator { fn effective_colors( &mut self, - context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>, ) -> (Color, Color) { let current_color = if context.active() { context.get(&ButtonActiveBackground) @@ -161,14 +161,14 @@ impl DiscloseIndicator { } impl Widget for DiscloseIndicator { - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { if let Some(label) = &mut self.label { label.unmount_in(context); } self.contents.unmount_in(context); } - fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { let angle = self.angle.get_tracking_redraw(context); let (color, stroke_color) = self.effective_colors(context); let size = context @@ -217,7 +217,7 @@ impl Widget for DiscloseIndicator { fn layout( &mut self, mut available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let indicator_size = context .get(&IndicatorSize) @@ -270,19 +270,19 @@ impl Widget for DiscloseIndicator { ) } - fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool { true } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { context.set_needs_redraw(); } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { context.set_needs_redraw(); } - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { let size = context .get(&IndicatorSize) .into_px(context.kludgine.scale()) @@ -296,11 +296,7 @@ impl Widget for DiscloseIndicator { } } - fn hover( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, location: Point, context: &mut EventContext<'_>) -> Option { let hovering = self.hit_test(location, context); if self.hovering_indicator != hovering { context.set_needs_redraw(); @@ -310,7 +306,7 @@ impl Widget for DiscloseIndicator { hovering.then_some(CursorIcon::Pointer) } - fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_>) { if self.hovering_indicator { self.hovering_indicator = false; context.set_needs_redraw(); @@ -322,7 +318,7 @@ impl Widget for DiscloseIndicator { location: Point, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if self.hit_test(location, context) { self.mouse_buttons_pressed += 1; @@ -339,7 +335,7 @@ impl Widget for DiscloseIndicator { _location: Option>, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { self.mouse_buttons_pressed -= 1; if self.mouse_buttons_pressed == 0 { @@ -348,7 +344,7 @@ impl Widget for DiscloseIndicator { } } - fn activate(&mut self, _context: &mut EventContext<'_, '_>) { + fn activate(&mut self, _context: &mut EventContext<'_>) { if self.mouse_buttons_pressed == 0 { self.collapsed.toggle(); } diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 60a7745..819235d 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -98,14 +98,14 @@ impl WrapperWidget for Expand { &mut self.child } - fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + fn root_behavior(&mut self, _context: &mut EventContext<'_>) -> Option { Some(RootBehavior::Expand) } fn layout_child( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let available_space = available_space.map(|lim| ConstraintLimit::Fill(lim.max())); let child = self.child.mounted(&mut context.as_event_context()); diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index cc6bcce..d5f94be 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -67,7 +67,7 @@ impl Grid { self } - fn synchronize_specs(&mut self, context: &mut EventContext<'_, '_>) { + fn synchronize_specs(&mut self, context: &mut EventContext<'_>) { let current_generation = self.columns.generation(); let count_changed = self.layout.children.len() != ELEMENTS; if count_changed @@ -84,7 +84,7 @@ impl Grid { } } - fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { + fn synchronize_children(&mut self, context: &mut EventContext<'_>) { self.synchronize_specs(context); let current_generation = self.rows.generation(); self.rows.invalidate_when_changed(context); @@ -132,7 +132,7 @@ impl Grid { } impl Widget for Grid { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { for (row, widgets) in self.live_rows.iter_mut().enumerate() { if self.layout.others[row] > 0 { for (column, cell) in widgets.iter().enumerate() { @@ -147,7 +147,7 @@ impl Widget for Grid { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.synchronize_children(&mut context.as_event_context()); diff --git a/src/widgets/image.rs b/src/widgets/image.rs index eeaaa9d..5353769 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -112,7 +112,7 @@ impl Image { } impl Widget for Image { - fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { self.contents.map(|texture| { let size = texture.size().into_signed(); let rect = match self.scaling.get() { @@ -147,7 +147,7 @@ impl Widget for Image { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { match self.scaling.get_tracking_invalidate(context) { ImageScaling::Aspect { .. } | ImageScaling::Stretch => { diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 7dbca9b..1dddd11 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -166,7 +166,7 @@ where }); } - fn forward_delete(&mut self, context: &mut EventContext<'_, '_>) { + fn forward_delete(&mut self, context: &mut EventContext<'_>) { if !context.enabled() { return; } @@ -200,7 +200,7 @@ where }); } - fn delete(&mut self, context: &mut EventContext<'_, '_>) { + fn delete(&mut self, context: &mut EventContext<'_>) { if !context.enabled() { return; } @@ -231,7 +231,7 @@ where &mut self, direction: Affinity, mode: CursorNavigationMode, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { if !matches!(mode, CursorNavigationMode::Line) { self.line_navigation_x_target = None; @@ -315,11 +315,7 @@ where } } - fn move_cursor_by_line_extent( - &mut self, - affinity: Affinity, - context: &mut EventContext<'_, '_>, - ) { + fn move_cursor_by_line_extent(&mut self, affinity: Affinity, context: &mut EventContext<'_>) { let Some(cache) = self.cache.as_ref() else { return; }; @@ -339,7 +335,7 @@ where self.selection.cursor = self.cursor_from_point(position, context); } - fn move_cursor_by_line(&mut self, affinity: Affinity, context: &mut EventContext<'_, '_>) { + fn move_cursor_by_line(&mut self, affinity: Affinity, context: &mut EventContext<'_>) { let Some(cache) = self.cache.as_ref() else { return; }; @@ -406,13 +402,13 @@ where self.mask_symbol.map(|mask| !mask.is_empty()) } - fn copy_selection_to_clipboard(&mut self, context: &mut EventContext<'_, '_>) { + fn copy_selection_to_clipboard(&mut self, context: &mut EventContext<'_>) { if self.is_masked() { return; } self.map_selected_text(|text| { - if let Some(mut clipboard) = context.clipboard_guard() { + if let Some(mut clipboard) = context.cushy().clipboard_guard() { match clipboard.set_text(text) { Ok(()) => {} Err(err) => tracing::error!("error copying to clipboard: {err}"), @@ -421,7 +417,7 @@ where }); } - fn replace_selection(&mut self, new_text: &str, context: &mut EventContext<'_, '_>) { + fn replace_selection(&mut self, new_text: &str, context: &mut EventContext<'_>) { if !context.enabled() { return; } @@ -444,12 +440,13 @@ where }; } - fn paste_from_clipboard(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn paste_from_clipboard(&mut self, context: &mut EventContext<'_>) -> bool { if !context.enabled() { return false; } match context + .cushy() .clipboard_guard() .map(|mut clipboard| clipboard.get_text()) { @@ -465,7 +462,7 @@ where } } - fn handle_key(&mut self, input: KeyEvent, context: &mut EventContext<'_, '_>) -> EventHandling { + fn handle_key(&mut self, input: KeyEvent, context: &mut EventContext<'_>) -> EventHandling { match (input.state, input.logical_key, input.text.as_deref()) { (ElementState::Pressed, Key::Named(key @ (NamedKey::Backspace| NamedKey::Delete)), _) => { match key { @@ -547,11 +544,7 @@ where } } - fn layout_text( - &mut self, - width: Option, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, - ) { + fn layout_text(&mut self, width: Option, context: &mut GraphicsContext<'_, '_, '_, '_>) { context.invalidate_when_changed(&self.value); let mut key = { @@ -808,11 +801,7 @@ where } } - fn cursor_from_point( - &mut self, - location: Point, - context: &mut EventContext<'_, '_>, - ) -> Cursor { + fn cursor_from_point(&mut self, location: Point, context: &mut EventContext<'_>) -> Cursor { let mut cursor = self.cached_cursor_from_point(location, context); if let Some(symbol) = self.mask.graphemes(true).next() { let grapheme_offset = cursor.offset / symbol.len(); @@ -831,7 +820,7 @@ where fn cached_cursor_from_point( &mut self, location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Cursor { let Some(cache) = &self.cache else { return Cursor::default(); @@ -973,11 +962,11 @@ impl Widget for Input where Storage: InputStorage + Debug, { - fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { true } - fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool { true } @@ -986,7 +975,7 @@ where location: Point, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { self.mouse_buttons_down += 1; context.focus(); @@ -1000,7 +989,7 @@ where fn hover( &mut self, _location: Point, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) -> Option { Some(CursorIcon::Text) } @@ -1010,7 +999,7 @@ where location: Point, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { let cursor_location = self.cursor_from_point(location, context); if self.selection.cursor != cursor_location { @@ -1025,13 +1014,13 @@ where _location: Option>, _device_id: kludgine::app::winit::event::DeviceId, _button: kludgine::app::winit::event::MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { self.mouse_buttons_down -= 1; } #[allow(clippy::too_many_lines)] - fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { if self.needs_to_select_all { self.needs_to_select_all = false; self.select_all(); @@ -1177,7 +1166,7 @@ where fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let padding = context .get(&IntrinsicPadding) @@ -1202,7 +1191,7 @@ where _device_id: kludgine::app::winit::event::DeviceId, input: kludgine::app::winit::event::KeyEvent, _is_synthetic: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(on_key) = &mut self.on_key { on_key.invoke(input.clone())?; @@ -1219,7 +1208,7 @@ where handled } - fn ime(&mut self, ime: Ime, context: &mut EventContext<'_, '_>) -> EventHandling { + fn ime(&mut self, ime: Ime, context: &mut EventContext<'_>) -> EventHandling { match ime { Ime::Enabled | Ime::Disabled => {} Ime::Preedit(text, cursor) => { @@ -1234,7 +1223,7 @@ where HANDLED } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { if self.mouse_buttons_down == 0 { self.needs_to_select_all = true; } @@ -1248,7 +1237,7 @@ where context.set_needs_redraw(); } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { context.set_ime_allowed(false); context.set_needs_redraw(); } diff --git a/src/widgets/label.rs b/src/widgets/label.rs index d9bc7c8..c6b1534 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -38,7 +38,7 @@ where fn prepared_text( &mut self, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, color: Color, width: Px, ) -> &MeasuredText { @@ -76,7 +76,7 @@ impl Widget for Label where T: std::fmt::Debug + std::fmt::Display + Send + 'static, { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.display.invalidate_when_changed(context); let size = context.gfx.region().size; @@ -94,7 +94,7 @@ where fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let color = context.get(&TextColor); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); @@ -107,7 +107,7 @@ where fmt.debug_tuple("Label").field(&self.display).finish() } - fn unmounted(&mut self, context: &mut crate::context::EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut crate::context::EventContext<'_>) { self.prepared_text.clear_for(context); } } diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 689c8b2..800b772 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -39,14 +39,14 @@ impl Layers { } } - fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { + fn synchronize_children(&mut self, context: &mut EventContext<'_>) { self.children.invalidate_when_changed(context); self.mounted.synchronize_with(&self.children, context); } } impl Widget for Layers { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.synchronize_children(&mut context.as_event_context()); for mounted in self.mounted.children() { @@ -68,7 +68,7 @@ impl Widget for Layers { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.synchronize_children(&mut context.as_event_context()); @@ -104,11 +104,11 @@ impl Widget for Layers { size } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { self.synchronize_children(context); } - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { for child in self.mounted.drain() { context.remove_child(&child); } @@ -116,7 +116,7 @@ impl Widget for Layers { fn root_behavior( &mut self, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option<(RootBehavior, WidgetInstance)> { self.synchronize_children(context); @@ -174,7 +174,7 @@ impl OverlayLayer { } impl Widget for OverlayLayer { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let state = self.state.lock(); for child in &state.overlays { @@ -192,7 +192,7 @@ impl Widget for OverlayLayer { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let mut state = self.state.lock(); state.prevent_notifications(); @@ -233,7 +233,7 @@ impl Widget for OverlayLayer { Size::ZERO } - fn hit_test(&mut self, location: Point, context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { let state = self.state.lock(); state.test_point(location, false, context).is_some() } @@ -241,7 +241,7 @@ impl Widget for OverlayLayer { fn hover( &mut self, location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { let mut state = self.state.lock(); @@ -259,7 +259,7 @@ impl Widget for OverlayLayer { None } - fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, _context: &mut EventContext<'_>) { let mut state = self.state.lock(); state.hovering = None; @@ -301,7 +301,7 @@ impl OverlayState { &self, location: Point, check_original_relative: bool, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { for (index, overlay) in self.overlays.iter().enumerate() { if overlay.requires_hover @@ -326,7 +326,7 @@ impl OverlayState { fn point_is_in_root_relative( &self, location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> bool { if let Some(relative_to) = self .overlays @@ -342,7 +342,7 @@ impl OverlayState { false } - fn process_new_overlays(&mut self, context: &mut EventContext<'_, '_>) { + fn process_new_overlays(&mut self, context: &mut EventContext<'_>) { while self.new_overlays > 0 { let new_index = self.overlays.len() - self.new_overlays; self.new_overlays -= 1; @@ -378,7 +378,7 @@ impl OverlayState { index: usize, widget: &MountedWidget, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, relative_to: WidgetId, ) -> Option> { let direction = self.overlays[index].direction; @@ -465,7 +465,7 @@ impl OverlayState { &self, checking_index: usize, layout: &Rect, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> bool { for index in (0..self.overlays.len()).filter(|&i| i != checking_index) { if self.overlays[index] @@ -497,7 +497,7 @@ impl OverlayState { index: usize, widget: &MountedWidget, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Option> { if let Some(relative_to) = self.overlays[index].relative_to { self.layout_overlay_relative(index, widget, available_space, context, relative_to) @@ -747,7 +747,7 @@ impl WrapperWidget for Tooltipped { fn hover( &mut self, _location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { let background_color = context.theme().surface.highest_container; @@ -779,7 +779,7 @@ impl WrapperWidget for Tooltipped { None } - fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, _context: &mut EventContext<'_>) { self.show_animation = None; self.data.shown_tooltip.set(None); } diff --git a/src/widgets/mode_switch.rs b/src/widgets/mode_switch.rs index 2f2f61e..1cd344d 100644 --- a/src/widgets/mode_switch.rs +++ b/src/widgets/mode_switch.rs @@ -25,7 +25,7 @@ impl WrapperWidget for ThemedMode { &mut self.child } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { context.attach_theme_mode(self.mode.clone()); } } diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index 62cca88..a857d72 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -318,7 +318,7 @@ impl Spinner { start: ZeroToOne, sweep: ZeroToOne, color: Color, - context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>, ) { if sweep > 0. { context.gfx.draw_shape( @@ -335,7 +335,7 @@ impl Spinner { } impl Widget for Spinner { - fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { let track_size = context.get(&TrackSize).into_px(context.gfx.scale()); let start = self.start.get_tracking_redraw(context); let end = self.end.get_tracking_redraw(context); @@ -384,7 +384,7 @@ impl Widget for Spinner { fn layout( &mut self, available_space: figures::Size, - context: &mut crate::context::LayoutContext<'_, '_, '_, '_, '_>, + context: &mut crate::context::LayoutContext<'_, '_, '_, '_>, ) -> figures::Size { let track_size = context.get(&TrackSize).into_px(context.gfx.scale()); let minimum_size = track_size * 4; diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index 48b64b5..b53e27f 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -80,7 +80,7 @@ impl Widget for RadioOrnament where T: Debug + Eq + Send + 'static, { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let radio_size = context .gfx .region() @@ -118,7 +118,7 @@ where fn layout( &mut self, _available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { let radio_size = context.get(&RadioSize).into_upx(context.gfx.scale()); Size::squared(radio_size) diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index bb9a271..ad0c96f 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -89,14 +89,14 @@ impl WrapperWidget for Resize { &mut self.child } - fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + fn root_behavior(&mut self, _context: &mut EventContext<'_>) -> Option { Some(RootBehavior::Resize(Size::new(self.width, self.height))) } fn layout_child( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let child = self.child.mounted(&mut context.as_event_context()); let size = if let (Some(width), Some(height)) = diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 153cad8..208e0bc 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -97,7 +97,7 @@ impl Scroll { (clamped, max_scroll) } - fn show_scrollbars(&mut self, context: &mut EventContext<'_, '_>) { + fn show_scrollbars(&mut self, context: &mut EventContext<'_>) { let should_hide = self.drag.mouse_buttons_down == 0; if should_hide != self.scrollbar_opacity_animation.will_hide || self.scrollbar_opacity_animation.handle.is_complete() @@ -130,7 +130,7 @@ impl Scroll { } } - fn hide_scrollbars(&mut self, context: &mut EventContext<'_, '_>) { + fn hide_scrollbars(&mut self, context: &mut EventContext<'_>) { if self.drag.mouse_buttons_down == 0 && !self.scrollbar_opacity_animation.will_hide { self.scrollbar_opacity_animation.will_hide = true; self.scrollbar_opacity_animation.handle = self @@ -144,29 +144,29 @@ impl Scroll { } impl Widget for Scroll { - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + fn unmounted(&mut self, context: &mut EventContext<'_>) { self.contents.unmount_in(context); } - fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { true } fn hover( &mut self, _location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { self.show_scrollbars(context); None } - fn unhover(&mut self, context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, context: &mut EventContext<'_>) { self.hide_scrollbars(context); } - fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { context.redraw_when_changed(&self.scrollbar_opacity); let managed = self.contents.mounted(&mut context.as_event_context()); @@ -198,7 +198,7 @@ impl Widget for Scroll { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.bar_width = context .get(&ScrollBarThickness) @@ -302,7 +302,7 @@ impl Widget for Scroll { _device_id: DeviceId, delta: MouseScrollDelta, _phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let amount = match delta { MouseScrollDelta::LineDelta(x, y) => Point::new(x, y) * self.line_height.into_float(), @@ -330,7 +330,7 @@ impl Widget for Scroll { location: Point, _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let relative_x = (self.control_size.width - location.x).max(Px::ZERO); let in_vertical_area = self.enabled.y && relative_x <= self.bar_width; @@ -380,7 +380,7 @@ impl Widget for Scroll { location: Point, _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { self.drag.update( location, @@ -397,7 +397,7 @@ impl Widget for Scroll { location: Option>, _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { self.drag.mouse_buttons_down -= 1; diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index b720620..390400a 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -144,7 +144,7 @@ where self } - fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_>) { if self.horizontal { self.rendered_size = spec.size.width; } else { @@ -238,7 +238,7 @@ where focus: Option, focus_ring_width: Px, spec: &TrackSpec, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, ) { let (a, a_is_focused, b) = match (start_knob, focus) { (Some(start_knob), Some(Knob::Start)) => (end_knob, false, Some((start_knob, true))), @@ -257,7 +257,7 @@ where is_focused: bool, focus_ring_width: Px, spec: &TrackSpec, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut GraphicsContext<'_, '_, '_, '_>, ) { context.gfx.draw_shape( Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center) @@ -417,7 +417,7 @@ impl Widget for Slider where T: SliderValue, { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let (track_color, inactive_track_color, knob_color) = if context.enabled() { ( context.get(&TrackColor), @@ -500,7 +500,7 @@ where fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.knob_size = if self.knob_visible { context.get(&KnobSize).into_upx(context.gfx.scale()) @@ -552,14 +552,14 @@ where } } - fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_, '_>) -> bool { + fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { self.interactive } fn hover( &mut self, _location: Point, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> Option { (self.interactive && self.knob_visible).then_some({ if context.enabled() { @@ -574,14 +574,14 @@ where }) } - fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool { context.enabled() && self.interactive && self.knob_visible && context.get(&AutoFocusableControls).is_all() } - fn focus(&mut self, context: &mut EventContext<'_, '_>) { + fn focus(&mut self, context: &mut EventContext<'_>) { if self.mouse_buttons_down == 0 { self.focused_knob = Some(if T::RANGED && !context.focus_is_advancing() { Knob::End @@ -595,7 +595,7 @@ where fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let (true, Some(focused)) = (T::RANGED, self.focused_knob) else { return IGNORED; @@ -619,7 +619,7 @@ where HANDLED } - fn blur(&mut self, context: &mut EventContext<'_, '_>) { + fn blur(&mut self, context: &mut EventContext<'_>) { self.previous_focus = self.focused_knob.take(); context.set_needs_redraw(); } @@ -629,7 +629,7 @@ where location: Point, _device_id: DeviceId, _button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let true = self.interactive else { return IGNORED; @@ -652,7 +652,7 @@ where location: Point, _device_id: DeviceId, _button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { if context.enabled() { self.update_from_click(location, None); @@ -664,7 +664,7 @@ where _location: Option>, _device_id: DeviceId, _button: MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { self.mouse_buttons_down -= 1; } @@ -674,7 +674,7 @@ where _device_id: DeviceId, input: kludgine::app::winit::event::KeyEvent, _is_synthetic: bool, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) -> EventHandling { let true = self.interactive else { return IGNORED; @@ -699,7 +699,7 @@ where _device_id: DeviceId, delta: MouseScrollDelta, _phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let true = self.interactive else { return IGNORED; diff --git a/src/widgets/space.rs b/src/widgets/space.rs index 93dd8aa..9bb7283 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -38,7 +38,7 @@ impl Space { } impl Widget for Space { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let color = self.color.get_tracking_redraw(context); context.fill(color); } @@ -46,7 +46,7 @@ impl Widget for Space { fn layout( &mut self, _available_space: Size, - _context: &mut LayoutContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { Size::default() } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index d99fd3e..3da1907 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -56,7 +56,7 @@ impl Stack { self } - fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { + fn synchronize_children(&mut self, context: &mut EventContext<'_>) { let current_generation = self.children.generation(); self.children.invalidate_when_changed(context); if current_generation.map_or_else( @@ -118,7 +118,7 @@ impl Stack { } impl Widget for Stack { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { for (layout, child) in self.layout.iter().zip(&self.synced_children) { if layout.size > 0 { context.for_other(child).redraw(); @@ -129,7 +129,7 @@ impl Widget for Stack { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { self.synchronize_children(&mut context.as_event_context()); diff --git a/src/widgets/style.rs b/src/widgets/style.rs index e423d0a..e6e3487 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -206,7 +206,7 @@ impl WrapperWidget for Style { &mut self.child } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { context.attach_styles(self.styles.clone()); } } diff --git a/src/widgets/switcher.rs b/src/widgets/switcher.rs index 0d9bc31..daaaf06 100644 --- a/src/widgets/switcher.rs +++ b/src/widgets/switcher.rs @@ -52,7 +52,7 @@ impl WrapperWidget for Switcher { fn adjust_child_constraints( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { if self.source.has_updated() { self.child = WidgetRef::new(self.source.get()); diff --git a/src/widgets/themed.rs b/src/widgets/themed.rs index 5a23fa8..1a38ec4 100644 --- a/src/widgets/themed.rs +++ b/src/widgets/themed.rs @@ -25,7 +25,7 @@ impl WrapperWidget for Themed { &mut self.child } - fn mounted(&mut self, context: &mut EventContext<'_, '_>) { + fn mounted(&mut self, context: &mut EventContext<'_>) { context.attach_theme(self.theme.clone()); } } diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index 9a3b3c4..521470b 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -64,7 +64,7 @@ impl Widget for TileMap where Layers: tilemap::Layers, { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let focus = self.focus.get(); // TODO this needs to be updated to support being placed in side of a scroll view. let redraw_after = match &mut self.layers { @@ -103,14 +103,14 @@ where } } - fn accept_focus(&mut self, _context: &mut EventContext<'_, '_>) -> bool { + fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool { true } fn hit_test( &mut self, _location: figures::Point, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) -> bool { true } @@ -118,7 +118,7 @@ where fn layout( &mut self, available_space: Size, - _context: &mut LayoutContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { Size::new(available_space.width.max(), available_space.height.max()) } @@ -128,7 +128,7 @@ where _device_id: DeviceId, delta: MouseScrollDelta, _phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { let amount = match delta { MouseScrollDelta::LineDelta(_, lines) => lines, @@ -141,11 +141,7 @@ where HANDLED } - fn hover( - &mut self, - local: Point, - context: &mut EventContext<'_, '_>, - ) -> Option { + fn hover(&mut self, local: Point, context: &mut EventContext<'_>) -> Option { if let Some(tick) = &self.tick { let Some(size) = context.last_layout().map(|rect| rect.size) else { return None; @@ -163,7 +159,7 @@ where None } - fn unhover(&mut self, _context: &mut EventContext<'_, '_>) { + fn unhover(&mut self, _context: &mut EventContext<'_>) { if let Some(tick) = &self.tick { tick.set_cursor_position(None); } @@ -174,7 +170,7 @@ where _device_id: DeviceId, input: KeyEvent, _is_synthetic: bool, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) -> EventHandling { if let Some(tick) = &self.tick { tick.key_input(&input)?; @@ -188,7 +184,7 @@ where _location: Point, _device_id: DeviceId, button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if let Some(tick) = &self.tick { tick.mouse_button(button, ElementState::Pressed); @@ -204,7 +200,7 @@ where _location: Option>, _device_id: DeviceId, button: kludgine::app::winit::event::MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { if let Some(tick) = &self.tick { tick.mouse_button(button, ElementState::Released); diff --git a/src/widgets/validated.rs b/src/widgets/validated.rs index e589a74..a04eee7 100644 --- a/src/widgets/validated.rs +++ b/src/widgets/validated.rs @@ -102,10 +102,7 @@ impl WrapperWidget for ValidatedWidget { &mut self.contents } - fn redraw_background( - &mut self, - context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, - ) { + fn redraw_background(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { self.error_color.set(context.get(&InvalidTextColor)); self.default_color.set(context.get(&HintTextColor)); } diff --git a/src/widgets/wrap.rs b/src/widgets/wrap.rs index 111067a..dce6103 100644 --- a/src/widgets/wrap.rs +++ b/src/widgets/wrap.rs @@ -97,7 +97,7 @@ impl Wrap { } impl Widget for Wrap { - fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { for child in self.mounted.children() { context.for_other(child).redraw(); } @@ -107,7 +107,7 @@ impl Widget for Wrap { fn layout( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { struct RowChild { index: usize, diff --git a/src/window.rs b/src/window.rs index 97626cb..ae29088 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,27 +4,31 @@ use std::cell::RefCell; use std::collections::hash_map; use std::ffi::OsStr; use std::hash::Hash; +use std::marker::PhantomData; +use std::num::TryFromIntError; use std::ops::{Deref, DerefMut, Not}; use std::path::Path; use std::string::ToString; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock}; +use std::time::{Duration, Instant}; use ahash::AHashMap; use alot::LotId; use arboard::Clipboard; use figures::units::{Px, UPx}; -use figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size, Zero}; +use figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, Round, ScreenScale, Size, Zero}; +use intentional::{Assert, Cast}; use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ - DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, + DeviceId, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::keyboard::{Key, NamedKey}; -use kludgine::app::winit::window; -use kludgine::app::WindowBehavior as _; +use kludgine::app::winit::window::{self, CursorIcon}; +use kludgine::app::{winit, WindowBehavior as _}; use kludgine::cosmic_text::{fontdb, Family, FamilyOwned}; use kludgine::render::Drawing; -use kludgine::wgpu::CompositeAlphaMode; -use kludgine::{Kludgine, KludgineId}; +use kludgine::wgpu::{self, CompositeAlphaMode, COPY_BYTES_PER_ROW_ALIGNMENT}; +use kludgine::{Color, Kludgine, KludgineId, Texture}; use tracing::Level; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; @@ -48,9 +52,183 @@ use crate::widget::{ use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit}; +/// A platform-dependent window implementation. +pub trait PlatformWindowImplementation { + /// Marks the window to close as soon as possible. + fn close(&mut self); + /// Returns the underlying `winit` window, if one exists. + fn winit(&self) -> Option<&winit::window::Window>; + /// Sets the window to redraw as soon as possible. + fn set_needs_redraw(&mut self); + /// Sets the window to redraw after a `duration`. + fn redraw_in(&mut self, duration: Duration); + /// Sets the window to redraw at a specified instant. + fn redraw_at(&mut self, moment: Instant); + /// Returns the current keyboard modifiers. + fn modifiers(&self) -> Modifiers; + /// Returns the amount of time that has elapsed since the last redraw. + fn elapsed(&self) -> Duration; + /// Sets the current cursor icon to `cursor`. + fn set_cursor_icon(&mut self, cursor: CursorIcon); + /// Returns a handle for the window. + fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle; + /// Returns the current inner size of the window. + fn inner_size(&self) -> Size; + + /// Returns true if the window can have its size changed. + /// + /// The provided implementation returns + /// [`winit::window::Window::is_resizable`], or true if this window has no + /// winit window. + fn is_resizable(&self) -> bool { + self.winit() + .map_or(true, winit::window::Window::is_resizable) + } + + /// Returns true if the window can have its size changed. + /// + /// The provided implementation returns [`winit::window::Window::theme`], or + /// dark if this window has no winit window. + fn theme(&self) -> winit::window::Theme { + self.winit() + .and_then(winit::window::Window::theme) + .unwrap_or(winit::window::Theme::Dark) + } + + /// Requests that the window change its inner size. + /// + /// The provided implementation forwards the request onto the winit window, + /// if present. + fn request_inner_size(&mut self, inner_size: Size) { + self.winit() + .map(|winit| winit.request_inner_size(PhysicalSize::from(inner_size))); + } + + /// Sets whether [`Ime`] events should be enabled. + /// + /// The provided implementation forwards the request onto the winit window, + /// if present. + fn set_ime_allowed(&self, allowed: bool) { + if let Some(winit) = self.winit() { + winit.set_ime_allowed(allowed); + } + } + + /// Sets the current [`Ime`] purpose. + /// + /// The provided implementation forwards the request onto the winit window, + /// if present. + fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) { + if let Some(winit) = self.winit() { + winit.set_ime_purpose(purpose); + } + } + + /// Sets the window's minimum inner size. + fn set_min_inner_size(&self, min_size: Option>) { + if let Some(winit) = self.winit() { + winit.set_min_inner_size::>(min_size.map(Into::into)); + } + } + + /// Sets the window's maximum inner size. + fn set_max_inner_size(&self, max_size: Option>) { + if let Some(winit) = self.winit() { + winit.set_max_inner_size::>(max_size.map(Into::into)); + } + } +} + +impl PlatformWindowImplementation for kludgine::app::Window<'_, WindowCommand> { + fn set_cursor_icon(&mut self, cursor: CursorIcon) { + self.winit().set_cursor_icon(cursor); + } + + fn inner_size(&self) -> Size { + self.winit().inner_size().into() + } + + fn close(&mut self) { + self.close(); + } + + fn winit(&self) -> Option<&winit::window::Window> { + Some(self.winit()) + } + + fn set_needs_redraw(&mut self) { + self.set_needs_redraw(); + } + + fn redraw_in(&mut self, duration: Duration) { + self.redraw_in(duration); + } + + fn redraw_at(&mut self, moment: Instant) { + self.redraw_at(moment); + } + + fn modifiers(&self) -> Modifiers { + self.modifiers() + } + + fn elapsed(&self) -> Duration { + self.elapsed() + } + + fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle { + WindowHandle::new(self.handle(), redraw_status) + } +} + +/// A platform-dependent window. +pub trait PlatformWindow { + /// Marks the window to close as soon as possible. + fn close(&mut self); + /// Returns the underlying `winit` window, if one exists. + fn winit(&self) -> Option<&winit::window::Window>; + /// Returns a handle for the window. + fn handle(&self) -> WindowHandle; + /// Returns the unique id of the [`Kludgine`] instance used by this window. + fn kludgine_id(&self) -> KludgineId; + /// Returns the dynamic that is synchrnoized with the window's focus. + fn focused(&self) -> &Dynamic; + /// Returns the dynamic that is synchronized with the window's occlusion + /// status. + fn occluded(&self) -> &Dynamic; + /// Returns the current inner size of the window. + fn inner_size(&self) -> &Dynamic>; + /// Returns the shared application resources. + fn cushy(&self) -> &Cushy; + /// Sets the window to redraw as soon as possible. + fn set_needs_redraw(&mut self); + /// Sets the window to redraw after a `duration`. + fn redraw_in(&mut self, duration: Duration); + /// Sets the window to redraw at a specified instant. + fn redraw_at(&mut self, moment: Instant); + /// Returns the current keyboard modifiers. + fn modifiers(&self) -> Modifiers; + /// Returns the amount of time that has elapsed since the last redraw. + fn elapsed(&self) -> Duration; + /// Sets the current cursor icon to `cursor`. + fn set_cursor_icon(&mut self, cursor: CursorIcon); + + /// Sets whether [`Ime`] events should be enabled. + fn set_ime_allowed(&self, allowed: bool); + /// Sets the current [`Ime`] purpose. + fn set_ime_purpose(&self, purpose: winit::window::ImePurpose); + + /// Requests that the window change its inner size. + fn request_inner_size(&mut self, inner_size: Size); + /// Sets the window's minimum inner size. + fn set_min_inner_size(&self, min_size: Option>); + /// Sets the window's maximum inner size. + fn set_max_inner_size(&self, max_size: Option>); +} + /// A currently running Cushy window. -pub struct RunningWindow<'window> { - window: kludgine::app::Window<'window, WindowCommand>, +pub struct RunningWindow { + window: W, kludgine_id: KludgineId, invalidation_status: InvalidationStatus, cushy: Cushy, @@ -59,9 +237,12 @@ pub struct RunningWindow<'window> { inner_size: Dynamic>, } -impl<'window> RunningWindow<'window> { +impl RunningWindow +where + W: PlatformWindowImplementation, +{ pub(crate) fn new( - window: kludgine::app::Window<'window, WindowCommand>, + window: W, kludgine_id: KludgineId, invalidation_status: &InvalidationStatus, cushy: &Cushy, @@ -113,7 +294,7 @@ impl<'window> RunningWindow<'window> { /// Returns a handle to this window. #[must_use] pub fn handle(&self) -> WindowHandle { - WindowHandle::new(self.window.handle(), self.invalidation_status.clone()) + self.window.handle(self.invalidation_status.clone()) } /// Returns a dynamic that is synchronized with this window's inner size. @@ -135,20 +316,107 @@ impl<'window> RunningWindow<'window> { } } -impl<'window> Deref for RunningWindow<'window> { - type Target = kludgine::app::Window<'window, WindowCommand>; +impl Deref for RunningWindow +where + W: PlatformWindowImplementation + 'static, +{ + type Target = dyn PlatformWindowImplementation; fn deref(&self) -> &Self::Target { &self.window } } -impl<'window> DerefMut for RunningWindow<'window> { +impl DerefMut for RunningWindow +where + W: PlatformWindowImplementation + 'static, +{ fn deref_mut(&mut self) -> &mut Self::Target { &mut self.window } } +impl PlatformWindow for RunningWindow +where + W: PlatformWindowImplementation, +{ + fn close(&mut self) { + self.window.close(); + } + + fn winit(&self) -> Option<&winit::window::Window> { + self.window.winit() + } + + fn handle(&self) -> WindowHandle { + self.window.handle(self.invalidation_status.clone()) + } + + fn kludgine_id(&self) -> KludgineId { + self.kludgine_id + } + + fn focused(&self) -> &Dynamic { + &self.focused + } + + fn occluded(&self) -> &Dynamic { + &self.occluded + } + + fn inner_size(&self) -> &Dynamic> { + &self.inner_size + } + + fn cushy(&self) -> &Cushy { + &self.cushy + } + + fn set_needs_redraw(&mut self) { + self.window.set_needs_redraw(); + } + + fn redraw_in(&mut self, duration: Duration) { + self.window.redraw_in(duration); + } + + fn redraw_at(&mut self, moment: Instant) { + self.window.redraw_at(moment); + } + + fn modifiers(&self) -> Modifiers { + self.window.modifiers() + } + + fn elapsed(&self) -> Duration { + self.window.elapsed() + } + + fn set_ime_allowed(&self, allowed: bool) { + self.window.set_ime_allowed(allowed); + } + + fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) { + self.window.set_ime_purpose(purpose); + } + + fn set_cursor_icon(&mut self, cursor: CursorIcon) { + self.window.set_cursor_icon(cursor); + } + + fn set_min_inner_size(&self, min_size: Option>) { + self.window.set_min_inner_size(min_size); + } + + fn set_max_inner_size(&self, max_size: Option>) { + self.window.set_max_inner_size(max_size); + } + + fn request_inner_size(&mut self, inner_size: Size) { + self.window.request_inner_size(inner_size); + } +} + /// The attributes of a Cushy window. pub type WindowAttributes = kludgine::app::WindowAttributes; @@ -377,7 +645,9 @@ 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( app, sealed::Context { @@ -389,9 +659,9 @@ where on_closed: self.on_closed, transparent: self.attributes.transparent, attributes: Some(self.attributes), - occluded: self.occluded, - focused: self.focused, - inner_size: self.inner_size, + occluded: self.occluded.unwrap_or_default(), + focused: self.focused.unwrap_or_default(), + inner_size: self.inner_size.unwrap_or_default(), theme: Some(self.theme), theme_mode: self.theme_mode, font_data_to_load: self.font_data_to_load, @@ -419,7 +689,10 @@ pub trait WindowBehavior: Sized + 'static { type Context: Send + 'static; /// Return a new instance of this behavior using `context`. - fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self; + fn initialize( + window: &mut RunningWindow>, + context: Self::Context, + ) -> Self; /// Create the window's root widget. This function is only invoked once. fn make_root(&mut self) -> WidgetInstance; @@ -428,7 +701,10 @@ pub trait WindowBehavior: Sized + 'static { /// the window will be closed. Returning false prevents the window from /// closing. #[allow(unused_variables)] - fn close_requested(&self, window: &mut RunningWindow<'_>) -> bool { + fn close_requested(&self, window: &mut RunningWindow) -> bool + where + W: PlatformWindowImplementation, + { true } @@ -479,7 +755,7 @@ where fn request_close( should_close: &mut bool, behavior: &mut T, - window: &mut RunningWindow<'_>, + window: &mut RunningWindow>, ) -> bool { *should_close |= behavior.close_requested(window); @@ -490,7 +766,7 @@ where &mut self, is_pressed: bool, widget: Option, - window: &mut RunningWindow<'_>, + window: &mut RunningWindow>, kludgine: &mut Kludgine, ) { if is_pressed { @@ -544,12 +820,15 @@ where } } - fn constrain_window_resizing( + fn constrain_window_resizing( &mut self, resizable: bool, - window: &mut RunningWindow<'_>, + window: &mut RunningWindow, graphics: &mut kludgine::Graphics<'_>, - ) -> RootMode { + ) -> RootMode + where + W: PlatformWindowImplementation, + { let mut root_or_child = self.root.widget.clone(); let mut root_mode = None; let mut padding = Edges::::default(); @@ -691,7 +970,7 @@ where fn handle_window_keyboard_input( &mut self, - window: &mut RunningWindow<'_>, + window: &mut RunningWindow>, kludgine: &mut Kludgine, input: KeyEvent, ) { @@ -782,40 +1061,31 @@ where } } } -} -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -enum RootMode { - Fit, - Expand, - Align, -} - -impl kludgine::app::WindowBehavior for CushyWindow -where - T: WindowBehavior, -{ - type Context = sealed::Context; - - fn initialize( - window: kludgine::app::Window<'_, WindowCommand>, + #[allow(clippy::needless_pass_by_value)] + fn new( + mut behavior: T, + window: W, graphics: &mut kludgine::Graphics<'_>, - context: Self::Context, - ) -> Self { - let mut settings = context.settings.borrow_mut(); + mut settings: sealed::WindowSettings, + ) -> Self + where + W: PlatformWindowImplementation, + { + let redraw_status = settings.redraw_status.clone(); if let Value::Dynamic(title) = &settings.title { - let handle = window.handle(); + let handle = window.handle(redraw_status.clone()); title .for_each_cloned(move |title| { - let _result = handle.send(WindowCommand::SetTitle(title)); + handle.inner.send(WindowCommand::SetTitle(title)); }) .persist(); } let cushy = settings.cushy.clone(); - let occluded = settings.occluded.take().unwrap_or_default(); - let focused = settings.focused.take().unwrap_or_default(); - let theme = settings.theme.take().expect("theme always present"); - let inner_size = settings.inner_size.take().unwrap_or_default(); + let occluded = settings.occluded.clone(); + let focused = settings.focused.clone(); + let theme = settings.theme.take().unwrap_or_default(); + let inner_size = settings.inner_size.clone(); let on_closed = settings.on_closed.take(); inner_size.set(window.inner_size()); @@ -831,19 +1101,7 @@ where None => Value::dynamic(window.theme().into()), }; let transparent = settings.transparent; - let redraw_status = settings.redraw_status.clone(); - let mut behavior = T::initialize( - &mut RunningWindow::new( - window, - graphics.id(), - &redraw_status, - &cushy, - &focused, - &occluded, - &inner_size, - ), - context.user, - ); + let tree = Tree::default(); let root = tree.push_boxed(behavior.make_root(), None); @@ -882,11 +1140,10 @@ where } } - fn prepare( - &mut self, - window: kludgine::app::Window<'_, WindowCommand>, - graphics: &mut kludgine::Graphics<'_>, - ) { + fn prepare(&mut self, window: W, graphics: &mut kludgine::Graphics<'_>) + where + W: PlatformWindowImplementation, + { if let Some(theme) = &mut self.theme { if theme.has_updated() { self.current_theme = theme.get(); @@ -899,7 +1156,7 @@ where self.tree .new_frame(self.redraw_status.invalidations().drain()); - let resizable = window.winit().is_resizable(); + let resizable = window.is_resizable(); let mut window = RunningWindow::new( window, graphics.id(), @@ -953,9 +1210,7 @@ where layout_context.redraw_when_changed(&self.inner_size); let inner_size_generation = self.inner_size.generation(); if self.inner_size_generation != inner_size_generation { - let _ = layout_context - .winit() - .request_inner_size(PhysicalSize::from(self.inner_size.get())); + layout_context.request_inner_size(self.inner_size.get()); self.inner_size_generation = inner_size_generation; } else if actual_size != window_size && !resizable { let mut new_size = actual_size; @@ -965,9 +1220,7 @@ where if let Some(max_size) = self.max_inner_size { new_size = new_size.min(max_size); } - let _ = layout_context - .winit() - .request_inner_size(PhysicalSize::from(new_size)); + layout_context.request_inner_size(new_size); } self.root.set_layout(Rect::from(render_size.into_signed())); @@ -990,6 +1243,74 @@ where } } + fn close_requested(&mut self, window: W, kludgine: &mut Kludgine) -> bool + where + W: PlatformWindowImplementation, + { + if self.behavior.close_requested(&mut RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + )) { + self.should_close = true; + true + } else { + false + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +enum RootMode { + Fit, + Expand, + Align, +} + +impl kludgine::app::WindowBehavior for CushyWindow +where + T: WindowBehavior, +{ + type Context = sealed::Context; + + fn initialize( + window: kludgine::app::Window<'_, WindowCommand>, + graphics: &mut kludgine::Graphics<'_>, + context: Self::Context, + ) -> Self { + let settings = context.settings.borrow_mut(); + let mut window = RunningWindow::new( + window, + graphics.id(), + &settings.redraw_status, + &settings.cushy, + &settings.focused, + &settings.occluded, + &settings.inner_size, + ); + drop(settings); + + let behavior = T::initialize(&mut window, context.user); + Self::new( + behavior, + window.window, + graphics, + context.settings.into_inner(), + ) + } + + fn prepare( + &mut self, + window: kludgine::app::Window<'_, WindowCommand>, + graphics: &mut kludgine::Graphics<'_>, + ) { + self.prepare(window, graphics); + } + fn focus_changed( &mut self, window: kludgine::app::Window<'_, WindowCommand>, @@ -1451,8 +1772,8 @@ impl Drop for CushyWindow { } fn recursively_handle_event( - context: &mut EventContext<'_, '_>, - mut each_widget: impl FnMut(&mut EventContext<'_, '_>) -> EventHandling, + context: &mut EventContext<'_>, + mut each_widget: impl FnMut(&mut EventContext<'_>) -> EventHandling, ) -> Option { match each_widget(context) { HANDLED => Some(context.widget().clone()), @@ -1491,9 +1812,9 @@ pub(crate) mod sealed { pub redraw_status: InvalidationStatus, pub title: Value, pub attributes: Option, - pub occluded: Option>, - pub focused: Option>, - pub inner_size: Option>>, + pub occluded: Dynamic, + pub focused: Dynamic, + pub inner_size: Dynamic>, pub theme: Option>, pub theme_mode: Option>, pub transparent: bool, @@ -1512,6 +1833,12 @@ pub(crate) mod sealed { RequestClose, SetTitle(String), } + + pub trait CaptureFormat { + const HAS_ALPHA: bool; + + fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32); + } } /// Controls whether the light or dark theme is applied. @@ -1678,6 +2005,7 @@ impl Hash for WindowHandle { enum InnerWindowHandle { Pending(Arc), Known(kludgine::app::WindowHandle), + Virtual(WindowDynamicState), } impl InnerWindowHandle { @@ -1697,6 +2025,11 @@ impl InnerWindowHandle { InnerWindowHandle::Known(handle) => { let _result = handle.send(message); } + InnerWindowHandle::Virtual(state) => match message { + WindowCommand::Redraw => state.redraw_target.set(RedrawTarget::Now), + WindowCommand::RequestClose => state.close_requested.set(true), + WindowCommand::SetTitle(title) => state.title.set(title), + }, }; } } @@ -1779,12 +2112,12 @@ impl WindowLocal { /// Looks up the entry for this window. /// /// Internally this API uses [`HashMap::entry`](hash_map::HashMap::entry). - pub fn entry(&mut self, context: &WidgetContext<'_, '_>) -> hash_map::Entry<'_, KludgineId, T> { + pub fn entry(&mut self, context: &WidgetContext<'_>) -> hash_map::Entry<'_, KludgineId, T> { self.by_window.entry(context.kludgine_id()) } /// Sets `value` as the local value for `context`'s window. - pub fn set(&mut self, context: &WidgetContext<'_, '_>, value: T) { + pub fn set(&mut self, context: &WidgetContext<'_>, value: T) { self.by_window.insert(context.kludgine_id(), value); } @@ -1792,12 +2125,12 @@ impl WindowLocal { /// /// Internally this API uses [`HashMap::get`](hash_map::HashMap::get). #[must_use] - pub fn get(&self, context: &WidgetContext<'_, '_>) -> Option<&T> { + pub fn get(&self, context: &WidgetContext<'_>) -> Option<&T> { self.by_window.get(&context.kludgine_id()) } /// Removes any stored value for this window. - pub fn clear_for(&mut self, context: &WidgetContext<'_, '_>) -> Option { + pub fn clear_for(&mut self, context: &WidgetContext<'_>) -> Option { self.by_window.remove(&context.kludgine_id()) } } @@ -1809,3 +2142,680 @@ impl Default for WindowLocal { } } } + +/// The state of a [`VirtualWindow`]. +pub struct VirtualState { + /// State that may be updated outside of the window's event callbacks. + pub dynamic: WindowDynamicState, + /// When true, this window should be closed. + pub closed: bool, + /// The current keyboard modifers. + pub modifiers: Modifiers, + /// The amount of time elapsed since the last redraw call. + pub elapsed: Duration, + /// The currently set cursor icon. + pub cursor: CursorIcon, + /// The inner size of the virtual window. + pub size: Size, +} + +impl VirtualState { + fn new() -> Self { + Self { + dynamic: WindowDynamicState::default(), + closed: false, + modifiers: Modifiers::default(), + elapsed: Duration::ZERO, + cursor: CursorIcon::default(), + size: Size::new(UPx::new(800), UPx::new(600)), + } + } +} + +/// Window state that is able to be updated outside of event handling, +/// potentially via other threads depending on the application. +#[derive(Clone, Debug, Default)] +pub struct WindowDynamicState { + /// The target of the next frame to draw. + pub redraw_target: Dynamic, + /// When true, the window has been asked to close. To ensure full Cushy + /// functionality, upon detecting this, [`VirtualWindow::request_close`] + /// should be invoked. + pub close_requested: Dynamic, + /// The current title of the window. + pub title: Dynamic, +} + +/// A target for the next redraw of a window. +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum RedrawTarget { + /// The window should not redraw. + #[default] + Never, + /// The window should redraw as soon as possible. + Now, + /// The window should try to redraw at the given instant. + At(Instant), +} + +impl PlatformWindowImplementation for &mut VirtualState { + fn close(&mut self) { + self.closed = true; + } + + fn winit(&self) -> Option<&winit::window::Window> { + None + } + + fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle { + WindowHandle { + inner: InnerWindowHandle::Virtual(self.dynamic.clone()), + redraw_status, + } + } + + fn set_needs_redraw(&mut self) { + self.dynamic.redraw_target.set(RedrawTarget::Now); + } + + fn redraw_in(&mut self, duration: Duration) { + self.redraw_at(Instant::now() + duration); + } + + fn redraw_at(&mut self, moment: Instant) { + self.dynamic.redraw_target.map_mut(|mut redraw_at| { + if match *redraw_at { + RedrawTarget::At(instant) => moment < instant, + RedrawTarget::Never => true, + RedrawTarget::Now => false, + } { + *redraw_at = RedrawTarget::At(moment); + } + }); + } + + fn modifiers(&self) -> Modifiers { + self.modifiers + } + + fn elapsed(&self) -> Duration { + self.elapsed + } + + fn set_cursor_icon(&mut self, cursor: CursorIcon) { + self.cursor = cursor; + } + + fn inner_size(&self) -> Size { + self.size + } + + fn request_inner_size(&mut self, inner_size: Size) { + self.size = inner_size; + self.set_needs_redraw(); + } +} + +/// A builder for a [`VirtualWindow`]. +pub struct VirtualWindowBuilder { + widget: WidgetInstance, + multisample_count: u32, + initial_size: Size, + scale: f32, + transparent: bool, +} + +impl VirtualWindowBuilder { + /// Returns a new builder for a virtual window that contains `contents`. + #[must_use] + pub fn new(contents: impl MakeWidget) -> Self { + Self { + widget: contents.make_widget(), + multisample_count: 4, + initial_size: Size::new(UPx::new(800), UPx::new(600)), + scale: 1., + transparent: false, + } + } + + /// Sets this virtual window's multi-sample count. + /// + /// By default, 4 samples are taken. When 1 sample is used, multisampling is + /// fully disabled. + #[must_use] + pub fn multisample_count(mut self, count: u32) -> Self { + self.multisample_count = count; + self + } + + /// Sets the size of the virtual window. + #[must_use] + pub fn size(mut self, size: Size) -> Self + where + Unit: Into, + { + self.initial_size = size.map(Into::into); + self + } + + /// Sets the DPI scaling factor of the virtual window. + #[must_use] + pub fn scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } + + /// Sets the window not fill its background before rendering its contents. + #[must_use] + pub fn transparent(mut self) -> Self { + self.transparent = true; + self + } + + /// Returns the initialized virtual 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 { + let mut kludgine = Kludgine::new( + device, + queue, + wgpu::TextureFormat::Rgba8UnormSrgb, + wgpu::MultisampleState { + count: multisample_count, + ..Default::default() + }, + initial_size, + scale, + ); + let mut state = VirtualState::new(); + let window = CushyWindow::::new( + widget.make_widget(), + &mut state, + &mut kludgine::Graphics::new(&mut kludgine, device, queue), + sealed::WindowSettings { + cushy: Cushy::new(), + redraw_status: InvalidationStatus::default(), + title: Value::default(), + attributes: None, + occluded: Dynamic::default(), + focused: Dynamic::default(), + inner_size: Dynamic::default(), + theme: None, + theme_mode: None, + transparent, + serif_font_family: FontFamilyList::default(), + sans_serif_font_family: FontFamilyList::default(), + fantasy_font_family: FontFamilyList::default(), + monospace_font_family: FontFamilyList::default(), + cursive_font_family: FontFamilyList::default(), + font_data_to_load: Vec::default(), + on_closed: None, + }, + ); + + Self { + window, + kludgine, + last_rendered_at: None, + state, + } + } + + /// 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.window.prepare( + &mut self.state, + &mut kludgine::Graphics::new(&mut self.kludgine, 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 { + let mut frame = self.kludgine.next_frame(); + let mut gfx = frame.render(pass, device, queue); + self.window.contents.render(1., &mut gfx); + drop(gfx); + frame.submit(queue) + } + + /// 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 { + 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); + drop(gfx); + frame.submit(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> { + 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) -> bool { + if self + .window + .close_requested(&mut self.state, &mut self.kludgine) + { + self.state.closed = true; + true + } else { + self.state.dynamic.close_requested.set(false); + false + } + } + + /// Returns true if this window should no longer be open. + #[must_use] + pub fn closed(&self) -> bool { + self.state.closed + } +} + +/// A color format containing 8-bit red, green, and blue channels. +pub struct Rgb8; + +/// A color format containing 8-bit red, green, blue, and alpha channels. +pub struct Rgba8; + +/// A format that can be captured in a [`VirtualRecorder`]. +pub trait CaptureFormat: sealed::CaptureFormat {} + +impl CaptureFormat for Rgb8 {} + +impl sealed::CaptureFormat for Rgb8 { + const HAS_ALPHA: bool = false; + + fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32) { + let packed_width = width * 4; + // Tightly pack the rgb data, discarding the alpha and extra padding.q + let mut index = 0; + data.retain(|_| { + let retain = index % bytes_per_row < packed_width && index % 4 < 3; + index += 1; + retain + }); + } +} + +impl CaptureFormat for Rgba8 {} + +impl sealed::CaptureFormat for Rgba8 { + const HAS_ALPHA: bool = true; + + fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32) { + let packed_width = width * 4; + if packed_width != bytes_per_row { + // Tightly pack the rgba data + let mut index = 0; + data.retain(|_| { + let retain = index % bytes_per_row < packed_width; + index += 1; + retain + }); + } + } +} + +/// A builder of a [`VirtualRecorder`]. +pub struct VirtualRecorderBuilder { + contents: WidgetInstance, + size: Size, + scale: f32, + format: PhantomData, +} + +impl VirtualRecorderBuilder { + /// Returns a builder of a [`VirtualRecorder`] that renders `contents`. + pub fn new(contents: impl MakeWidget) -> Self { + Self { + contents: contents.make_widget(), + size: Size::new(UPx::new(800), UPx::new(600)), + scale: 1.0, + format: PhantomData, + } + } + + /// Enables transparency support to render the contents without a background + /// color. + #[must_use] + pub fn with_alpha(self) -> VirtualRecorderBuilder { + VirtualRecorderBuilder { + contents: self.contents, + size: self.size, + scale: self.scale, + format: PhantomData, + } + } +} + +impl VirtualRecorderBuilder +where + Format: CaptureFormat, +{ + /// Sets the size of the virtual window. + #[must_use] + pub fn size(mut self, size: Size) -> Self + where + Unit: Into, + { + self.size = size.map(Into::into); + self + } + + /// Returns an initialized [`VirtualRecorder`]. + pub fn finish(self) -> Result, VirtualRecorderError> { + VirtualRecorder::new(self.size, self.scale, self.contents) + } +} + +struct Capture { + buffer: wgpu::Buffer, + texture: Texture, + multisample: Texture, +} + +/// A recorder of a [`VirtualWindow`]. +pub struct VirtualRecorder { + window: VirtualWindow, + device: wgpu::Device, + queue: wgpu::Queue, + capture: Option, + data: Vec, + format: PhantomData, +} + +impl VirtualRecorder +where + Format: CaptureFormat, +{ + /// Returns a new virtual recorder that renders `contents` into a graphic of + /// `size`. + /// + /// `scale` adjusts the default DPI scaling to perform. It does not affect + /// the `size`. + pub fn new( + size: Size, + scale: f32, + contents: impl MakeWidget, + ) -> Result { + let wgpu = wgpu::Instance::default(); + let adapter = + pollster::block_on(wgpu.request_adapter(&wgpu::RequestAdapterOptions::default())) + .ok_or(VirtualRecorderError::NoAdapter)?; + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: None, + features: Kludgine::REQURED_FEATURES, + limits: Kludgine::adjust_limits(wgpu::Limits::downlevel_webgl2_defaults()), + }, + None, + ))?; + + let mut recorder = Self { + window: VirtualWindow::new( + contents.make_widget(), + 4, + size, + scale, + Format::HAS_ALPHA, + &device, + &queue, + ), + device, + queue, + capture: None, + data: Vec::new(), + format: PhantomData, + }; + + recorder.refresh()?; + + Ok(recorder) + } + + /// Returns the tightly-packed captured bytes. + /// + /// The layout of this data is determined by the `Format` generic. + pub fn bytes(&self) -> &[u8] { + &self.data + } + + /// Returns the current size of the recorder. + pub const fn size(&self) -> Size { + self.window.kludgine.size() + } + + fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64) { + if self + .capture + .as_ref() + .map_or(true, |capture| capture.texture.size() != size) + { + let texture = Texture::new( + &self.window.graphics(&self.device, &self.queue), + size, + wgpu::TextureFormat::Rgba8UnormSrgb, + wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::TEXTURE_BINDING, + wgpu::FilterMode::Linear, + ); + let multisample = Texture::multisampled( + &self.window.graphics(&self.device, &self.queue), + 4, + size, + wgpu::TextureFormat::Rgba8UnormSrgb, + wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + wgpu::FilterMode::Linear, + ); + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: bytes, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + self.capture = Some(Capture { + buffer, + texture, + multisample, + }); + } + } + + /// Redraws the contents. + pub fn refresh(&mut self) -> Result<(), wgpu::BufferAsyncError> { + let render_size = self.window.kludgine.size().ceil(); + let bytes_per_row = copy_buffer_aligned_bytes_per_row(render_size.width.get() * 4); + let size = u64::from(bytes_per_row) * u64::from(render_size.height.get()); + self.recreate_buffers_if_needed(render_size, size); + + let capture = self.capture.as_ref().assert("always initialized above"); + + self.window.prepare(&self.device, &self.queue); + + self.window.render( + &wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: capture.multisample.view(), + resolve_target: Some(capture.texture.view()), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(Color::CLEAR_BLACK.into()), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + &self.device, + &self.queue, + ); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + capture.texture.copy_to_buffer( + wgpu::ImageCopyBuffer { + buffer: &capture.buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + }, + &mut encoder, + ); + self.queue.submit([encoder.finish()]); + + let map_result = Arc::new(Mutex::new(None)); + let condvar = Arc::new(Condvar::new()); + let slice = capture.buffer.slice(0..size); + + std::thread::scope(|scope| { + scope.spawn({ + let map_result = map_result.clone(); + let condvar = condvar.clone(); + move || { + condvar.notify_one(); + slice.map_async(wgpu::MapMode::Read, { + move |result| { + *map_result.lock().assert("thread panicked") = Some(result); + condvar.notify_one(); + } + }); + } + }); + + // Now that we've queued up the data mapping thread, let's make sure + // our vec is allocated. Since an allocation can take a moment, this + // is the perfect to do it. + self.data.clear(); + self.data.reserve(size.cast()); + + // Wait for the buffer to have been mapped. + loop { + self.device.poll(wgpu::Maintain::Poll); + + let mut result = map_result.lock().assert("thread panicked"); + if let Some(result) = result.take() { + result?; + break; + } + + let _guard = condvar + .wait_timeout(result, Duration::from_millis(1)) + .assert("thread panicked"); + } + + Ok(()) + })?; + + self.data + .extend_from_slice(bytemuck::cast_slice(&slice.get_mapped_range())); + + Format::convert_rgba(&mut self.data, render_size.width.get(), bytes_per_row); + + Ok(()) + } +} + +fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { + (width + COPY_BYTES_PER_ROW_ALIGNMENT - 1) / COPY_BYTES_PER_ROW_ALIGNMENT + * COPY_BYTES_PER_ROW_ALIGNMENT +} + +/// An error from a [`VirtualRecorder`]. +#[derive(Debug)] +pub enum VirtualRecorderError { + /// No compatible wgpu adapters could be found. + NoAdapter, + /// An error occurred requesting a device. + RequestDevice(wgpu::RequestDeviceError), + /// The capture texture dimensions are too large to fit in the current host + /// platform's memory. + TooLarge, + /// An error occurred trying to read a buffer. + MapBuffer(wgpu::BufferAsyncError), +} + +impl From for VirtualRecorderError { + fn from(value: wgpu::RequestDeviceError) -> Self { + Self::RequestDevice(value) + } +} + +impl From for VirtualRecorderError { + fn from(value: wgpu::BufferAsyncError) -> Self { + Self::MapBuffer(value) + } +} + +impl From for VirtualRecorderError { + fn from(_: TryFromIntError) -> Self { + Self::TooLarge + } +} From cdef6bcbc3434ed64ff01bf12f41472c3cba3d0f Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 11:41:26 -0800 Subject: [PATCH 02/10] Removed unnecessary bytemuck usage This was copy-pasted from a Buffer implementation that was removed from Kludgine, since Buffer isn't public. --- Cargo.lock | 1 - Cargo.toml | 1 - src/window.rs | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bca7823..f24cbea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,7 +574,6 @@ dependencies = [ "ahash", "alot", "arboard", - "bytemuck", "cushy-macros", "figures", "image", diff --git a/Cargo.toml b/Cargo.toml index 00cc8ed..f302ca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ arboard = "3.2.1" zeroize = "1.6.1" unicode-segmentation = "1.10.1" pollster = "0.3.0" -bytemuck = "1.14.0" # [patch.crates-io] diff --git a/src/window.rs b/src/window.rs index ae29088..fcbc53a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2774,8 +2774,7 @@ where Ok(()) })?; - self.data - .extend_from_slice(bytemuck::cast_slice(&slice.get_mapped_range())); + self.data.extend_from_slice(&slice.get_mapped_range()); Format::convert_rgba(&mut self.data, render_size.width.get(), bytes_per_row); From 5a56833cd07ee0a9ab9d36f859111939439adc1e Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 11:44:15 -0800 Subject: [PATCH 03/10] Switching text matrix to continue jobs --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d7ed051..8531508 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,6 +5,7 @@ on: [push] jobs: test: strategy: + fail-fast: false matrix: version: ["stable", "1.70.0"] os: ["ubuntu-latest", "windows-latest", "macos-latest"] From 2e2d45d281eac15aa2cbeba865152f3ec5423739 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 11:47:59 -0800 Subject: [PATCH 04/10] Switching wgpu dependencies This now matches the current CI config on wgpu's repository. --- .github/workflows/rust.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8531508..d586037 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,10 +17,15 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update -y -qq - sudo add-apt-repository ppa:oibaf/graphics-drivers -y + + # vulkan sdk + wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - + sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-jammy.list https://packages.lunarg.com/vulkan/lunarg-vulkan-jammy.list + + # install dependencies sudo apt-get update sudo apt-get install -y \ - libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + libegl-mesa0 libgl1-mesa-dri libxcb-xfixes0-dev vulkan-sdk mesa-vulkan-drivers - uses: dtolnay/rust-toolchain@stable with: @@ -31,6 +36,10 @@ jobs: run: | cargo clippy --all-features --all-targets - - name: Run default features unit tests + - name: Compile with all features + run: | + cargo build --all-features --all-targets + + - name: Run all features unit tests run: | cargo test --all-features --all-targets From bc52be440f19dc6479b191aee539e8be9f118f0f Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 17:45:11 -0800 Subject: [PATCH 05/10] Added animation recording + event wiring Not great, but it technically exists. --- .github/workflows/rust.yml | 3 + Cargo.lock | 15 +- Cargo.toml | 1 + examples/custom-widgets.rs | 3 +- examples/offscreen-apng.rs | 38 ++ examples/offscreen.rs | 8 +- src/animation.rs | 38 +- src/context.rs | 6 +- src/widget.rs | 6 +- src/widgets/button.rs | 4 +- src/widgets/color.rs | 3 +- src/widgets/custom.rs | 5 +- src/widgets/disclose.rs | 5 +- src/widgets/input.rs | 8 +- src/widgets/scroll.rs | 3 +- src/widgets/slider.rs | 3 +- src/widgets/tilemap.rs | 3 +- src/window.rs | 985 ++++++++++++++++++++++++++----------- 18 files changed, 807 insertions(+), 330 deletions(-) create mode 100644 examples/offscreen-apng.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d586037..f3d77bc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -41,5 +41,8 @@ jobs: cargo build --all-features --all-targets - name: Run all features unit tests + # for msrv, we only check build compatibility, as it's possible bugs are + # fixed purely by updating the rust version. + if: matrix.version == 'stable' run: | cargo test --all-features --all-targets diff --git a/Cargo.lock b/Cargo.lock index f24cbea..7a9ed23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,6 +582,7 @@ dependencies = [ "kempt", "kludgine", "palette", + "png", "pollster", "rand", "tracing", @@ -1179,7 +1180,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#9999ff4a323a6dec1deebb9a0a1825d561559478" +source = "git+https://github.com/khonsulabs/kludgine#6b0d5fa0477daaf79f75a6f7dad819377a45e645" dependencies = [ "ahash", "alot", @@ -1858,9 +1859,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" dependencies = [ "unicode-ident", ] @@ -2331,9 +2332,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" dependencies = [ "proc-macro2", "quote", @@ -3233,9 +3234,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.5.31" +version = "0.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" +checksum = "8434aeec7b290e8da5c3f0d628cb0eac6cabcb31d14bb74f779a08109a5914d6" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f302ca9..74fb601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ arboard = "3.2.1" zeroize = "1.6.1" unicode-segmentation = "1.10.1" pollster = "0.3.0" +png = "0.17.10" # [patch.crates-io] diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 9891f40..14173ef 100644 --- a/examples/custom-widgets.rs +++ b/examples/custom-widgets.rs @@ -4,6 +4,7 @@ use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED}; use cushy::widgets::Custom; +use cushy::window::DeviceId; use cushy::Run; use figures::units::{Lp, UPx}; use figures::{ScreenScale, Size}; @@ -116,7 +117,7 @@ impl Widget for Toggle { fn mouse_down( &mut self, _location: figures::Point, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, _context: &mut cushy::context::EventContext<'_>, ) -> cushy::widget::EventHandling { diff --git a/examples/offscreen-apng.rs b/examples/offscreen-apng.rs new file mode 100644 index 0000000..8fc6751 --- /dev/null +++ b/examples/offscreen-apng.rs @@ -0,0 +1,38 @@ +use std::time::Duration; + +use cushy::animation::easings::EaseInOutSine; +use cushy::widget::MakeWidget; +use figures::units::Px; +use figures::{Point, Size}; + +fn ui() -> impl MakeWidget { + "Hello World".into_button().centered() +} + +fn main() { + // The default recorder generated solid, rgb images. + let mut recorder = ui() + .build_recorder() + .size(Size::new(320, 240)) + .finish() + .unwrap(); + let initial_point = Point::new(Px::new(140), Px::new(150)); + recorder.set_cursor_position(initial_point); + recorder.refresh().unwrap(); + let mut animation = recorder.record_animated_png(60); + animation + .animate_cursor_to( + Point::new(Px::new(160), Px::new(120)), + Duration::from_millis(250), + EaseInOutSine, + ) + .unwrap(); + animation.wait_for(Duration::from_millis(500)).unwrap(); + animation + .animate_cursor_to(initial_point, Duration::from_millis(250), EaseInOutSine) + .unwrap(); + animation.wait_for(Duration::from_millis(500)).unwrap(); + animation + .write_to("examples/offscreen-animated.png") + .unwrap(); +} diff --git a/examples/offscreen.rs b/examples/offscreen.rs index dca0bd9..c1b43ed 100644 --- a/examples/offscreen.rs +++ b/examples/offscreen.rs @@ -15,8 +15,8 @@ fn main() { image::save_buffer_with_format( "examples/offscreen.png", recorder.bytes(), - recorder.size().width.get(), - recorder.size().height.get(), + recorder.window.size().width.get(), + recorder.window.size().height.get(), image::ColorType::Rgb8, image::ImageFormat::Png, ) @@ -32,8 +32,8 @@ fn main() { image::save_buffer_with_format( "examples/offscreen-transparent.png", recorder.bytes(), - recorder.size().width.get(), - recorder.size().height.get(), + recorder.window.size().width.get(), + recorder.window.size().height.get(), image::ColorType::Rgba8, image::ImageFormat::Png, ) diff --git a/src/animation.rs b/src/animation.rs index 256cdc6..f3594b2 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -49,7 +49,7 @@ use std::time::{Duration, Instant}; use alot::{LotId, Lots}; use figures::units::{Lp, Px, UPx}; -use figures::{Angle, Ranged, UnscaledUnit, Zero}; +use figures::{Angle, Point, Ranged, Rect, Size, UnscaledUnit, Zero}; use intentional::Cast; use kempt::Set; use kludgine::Color; @@ -929,6 +929,42 @@ impl_unscaled_lerp!(Px); impl_unscaled_lerp!(Lp); impl_unscaled_lerp!(UPx); +impl LinearInterpolate for Point +where + Unit: LinearInterpolate, +{ + fn lerp(&self, target: &Self, percent: f32) -> Self { + Self::new( + self.x.lerp(&target.x, percent), + self.y.lerp(&target.y, percent), + ) + } +} + +impl LinearInterpolate for Size +where + Unit: LinearInterpolate, +{ + fn lerp(&self, target: &Self, percent: f32) -> Self { + Self::new( + self.width.lerp(&target.width, percent), + self.height.lerp(&target.height, percent), + ) + } +} + +impl LinearInterpolate for Rect +where + Unit: LinearInterpolate, +{ + fn lerp(&self, target: &Self, percent: f32) -> Self { + Self::new( + self.origin.lerp(&target.origin, percent), + self.size.lerp(&target.size, percent), + ) + } +} + #[test] fn integer_lerps() { #[track_caller] diff --git a/src/context.rs b/src/context.rs index 417d3b0..56b215a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -4,9 +4,7 @@ use std::ops::{Deref, DerefMut}; use figures::units::{Lp, Px, UPx}; use figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size, Zero}; -use kludgine::app::winit::event::{ - DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, -}; +use kludgine::app::winit::event::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::{Color, Kludgine, KludgineId}; @@ -21,7 +19,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; use crate::tree::Tree; use crate::value::{IntoValue, Source, Value}; use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; -use crate::window::{CursorState, PlatformWindow, ThemeMode}; +use crate::window::{CursorState, DeviceId, PlatformWindow, ThemeMode}; use crate::ConstraintLimit; /// A context to an event function. diff --git a/src/widget.rs b/src/widget.rs index b1e43f3..bcc556e 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -12,9 +12,7 @@ use alot::LotId; use figures::units::{Px, UPx}; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use intentional::Assert; -use kludgine::app::winit::event::{ - DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, -}; +use kludgine::app::winit::event::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::Color; @@ -47,7 +45,7 @@ use crate::widgets::{ }; use crate::window::sealed::WindowCommand; use crate::window::{ - Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, VirtualWindowBuilder, Window, + DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, VirtualWindowBuilder, Window, WindowBehavior, WindowHandle, WindowLocal, }; use crate::ConstraintLimit; diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 55d0c82..b0d3a4a 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -3,7 +3,7 @@ use std::time::Duration; use figures::units::{Lp, Px, UPx}; use figures::{IntoSigned, Point, Rect, Round, ScreenScale, Size}; -use kludgine::app::winit::event::{DeviceId, MouseButton}; +use kludgine::app::winit::event::MouseButton; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::Color; @@ -21,7 +21,7 @@ use crate::styles::components::{ use crate::styles::{ColorExt, Styles}; use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED}; -use crate::window::WindowLocal; +use crate::window::{DeviceId, WindowLocal}; use crate::FitMeasuredSize; /// A clickable button. diff --git a/src/widgets/color.rs b/src/widgets/color.rs index 86c59f6..f026ed9 100644 --- a/src/widgets/color.rs +++ b/src/widgets/color.rs @@ -4,7 +4,7 @@ use std::ops::Range; use figures::units::{Lp, Px}; use figures::{FloatConversion, Point, Rect, Round, ScreenScale, Zero}; use intentional::Cast; -use kludgine::app::winit::event::{DeviceId, MouseButton}; +use kludgine::app::winit::event::MouseButton; use kludgine::shapes::{self, FillOptions, PathBuilder, Shape, StrokeOptions}; use kludgine::{Color, DrawableExt, Origin}; @@ -14,6 +14,7 @@ use crate::styles::components::{HighlightColor, OutlineColor, TextColor}; use crate::styles::{ColorExt, ColorSource}; use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{EventHandling, Widget, HANDLED}; +use crate::window::DeviceId; /// A widget that selects a [`ColorSource`]. #[derive(Debug)] diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs index 1553c93..8220346 100644 --- a/src/widgets/custom.rs +++ b/src/widgets/custom.rs @@ -2,9 +2,7 @@ use std::fmt::Debug; use figures::units::Px; use figures::{Point, Size}; -use kludgine::app::winit::event::{ - DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, -}; +use kludgine::app::winit::event::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::Color; @@ -13,6 +11,7 @@ use crate::styles::VisualOrder; use crate::value::{IntoValue, Value}; use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED}; use crate::widgets::Space; +use crate::window::DeviceId; use crate::ConstraintLimit; /// A callback-based custom widget. diff --git a/src/widgets/disclose.rs b/src/widgets/disclose.rs index 3c01365..870e311 100644 --- a/src/widgets/disclose.rs +++ b/src/widgets/disclose.rs @@ -18,6 +18,7 @@ use crate::widget::{ EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetRef, WidgetTag, HANDLED, IGNORED, }; +use crate::window::DeviceId; use crate::ConstraintLimit; /// A widget that hides and shows another widget. @@ -316,7 +317,7 @@ impl Widget for DiscloseIndicator { fn mouse_down( &mut self, location: Point, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_>, ) -> EventHandling { @@ -333,7 +334,7 @@ impl Widget for DiscloseIndicator { fn mouse_up( &mut self, _location: Option>, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: DeviceId, _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_>, ) { diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 1dddd11..515bdc0 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -973,7 +973,7 @@ where fn mouse_down( &mut self, location: Point, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: crate::window::DeviceId, _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_>, ) -> EventHandling { @@ -997,7 +997,7 @@ where fn mouse_drag( &mut self, location: Point, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: crate::window::DeviceId, _button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_>, ) { @@ -1012,7 +1012,7 @@ where fn mouse_up( &mut self, _location: Option>, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: crate::window::DeviceId, _button: kludgine::app::winit::event::MouseButton, _context: &mut EventContext<'_>, ) { @@ -1188,7 +1188,7 @@ where fn keyboard_input( &mut self, - _device_id: kludgine::app::winit::event::DeviceId, + _device_id: crate::window::DeviceId, input: kludgine::app::winit::event::KeyEvent, _is_synthetic: bool, context: &mut EventContext<'_>, diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 208e0bc..41bf7dd 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -4,7 +4,7 @@ use std::time::{Duration, Instant}; use figures::units::{Lp, Px, UPx}; use figures::{FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero}; use intentional::Cast; -use kludgine::app::winit::event::{DeviceId, MouseScrollDelta, TouchPhase}; +use kludgine::app::winit::event::{MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::Shape; use kludgine::Color; @@ -15,6 +15,7 @@ use crate::styles::components::{EasingIn, EasingOut, LineHeight}; use crate::styles::Dimension; use crate::value::{Destination, Dynamic, Source}; use crate::widget::{EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED}; +use crate::window::DeviceId; use crate::ConstraintLimit; /// A widget that supports scrolling its contents. diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 390400a..4987564 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -6,7 +6,7 @@ use std::ops::RangeInclusive; use figures::units::{Lp, Px, UPx}; use figures::{FloatConversion, IntoSigned, Point, Ranged, Rect, Round, ScreenScale, Size}; use intentional::{Assert, Cast as _}; -use kludgine::app::winit::event::{DeviceId, MouseButton, MouseScrollDelta, TouchPhase}; +use kludgine::app::winit::event::{MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::keyboard::{Key, NamedKey}; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::{Shape, StrokeOptions}; @@ -21,6 +21,7 @@ use crate::styles::components::{ use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder}; use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; +use crate::window::DeviceId; use crate::ConstraintLimit; /// A widget that allows sliding between two values. diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index 521470b..b976470 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use figures::units::{Px, UPx}; use figures::{Point, Size}; use intentional::Cast; -use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseScrollDelta, TouchPhase}; +use kludgine::app::winit::event::{ElementState, KeyEvent, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::tilemap; use kludgine::tilemap::TileMapFocus; @@ -12,6 +12,7 @@ use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::tick::Tick; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; +use crate::window::DeviceId; use crate::ConstraintLimit; /// A layered tile-based 2d game surface. diff --git a/src/window.rs b/src/window.rs index fcbc53a..394920a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -16,22 +16,27 @@ use ahash::AHashMap; use alot::LotId; use arboard::Clipboard; use figures::units::{Px, UPx}; -use figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, Round, ScreenScale, Size, Zero}; +use figures::{ + Fraction, IntoSigned, IntoUnsigned, Point, Ranged, Rect, Round, ScreenScale, Size, Zero, +}; use intentional::{Assert, Cast}; use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ - DeviceId, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::keyboard::{Key, NamedKey}; use kludgine::app::winit::window::{self, CursorIcon}; use kludgine::app::{winit, WindowBehavior as _}; use kludgine::cosmic_text::{fontdb, Family, FamilyOwned}; use kludgine::render::Drawing; +use kludgine::shapes::Shape; use kludgine::wgpu::{self, CompositeAlphaMode, COPY_BYTES_PER_ROW_ALIGNMENT}; -use kludgine::{Color, Kludgine, KludgineId, Texture}; +use kludgine::{Color, DrawableExt, Kludgine, KludgineId, Origin, Texture}; use tracing::Level; -use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; +use crate::animation::{ + AnimationTarget, Easing, LinearInterpolate, PercentBetween, Spawn, ZeroToOne, +}; use crate::app::{Application, Cushy, Open, PendingApp, Run}; use crate::context::sealed::InvalidationStatus; use crate::context::{ @@ -701,9 +706,9 @@ pub trait WindowBehavior: Sized + 'static { /// the window will be closed. Returning false prevents the window from /// closing. #[allow(unused_variables)] - fn close_requested(&self, window: &mut RunningWindow) -> bool + fn close_requested(&self, window: &mut W) -> bool where - W: PlatformWindowImplementation, + W: PlatformWindow, { true } @@ -762,13 +767,15 @@ where *should_close } - fn keyboard_activate_widget( + fn keyboard_activate_widget( &mut self, is_pressed: bool, widget: Option, - window: &mut RunningWindow>, + window: &mut W, kludgine: &mut Kludgine, - ) { + ) where + W: PlatformWindow, + { if is_pressed { if let Some(default) = widget.and_then(|id| self.tree.widget_from_node(id)) { if let Some(previously_active) = self @@ -968,19 +975,22 @@ where fonts } - fn handle_window_keyboard_input( + fn handle_window_keyboard_input( &mut self, - window: &mut RunningWindow>, + window: &mut W, kludgine: &mut Kludgine, input: KeyEvent, - ) { + ) -> EventHandling + where + W: PlatformWindow, + { match input.logical_key { Key::Character(ch) if ch == "w" && window.modifiers().primary() => { - if input.state.is_pressed() - && Self::request_close(&mut self.should_close, &mut self.behavior, window) - { + if input.state.is_pressed() && self.behavior.close_requested(window) { + self.should_close = true; window.set_needs_redraw(); } + HANDLED } Key::Named(NamedKey::Space) if !window.modifiers().possible_shortcut() => { let target = self.tree.focused_widget().unwrap_or(self.root.node_id); @@ -1008,6 +1018,7 @@ where target.deactivate(); } } + HANDLED } Key::Named(NamedKey::Tab) if !window.modifiers().possible_shortcut() => { @@ -1033,6 +1044,7 @@ where target.advance_focus(); } } + HANDLED } Key::Named(NamedKey::Enter) => { self.keyboard_activate_widget( @@ -1041,6 +1053,7 @@ where window, kludgine, ); + HANDLED } Key::Named(NamedKey::Escape) => { self.keyboard_activate_widget( @@ -1049,6 +1062,7 @@ where window, kludgine, ); + HANDLED } _ => { tracing::event!( @@ -1058,6 +1072,7 @@ where state = ?input.state, "Ignored Keyboard Input", ); + IGNORED } } } @@ -1262,6 +1277,336 @@ where false } } + + fn resized(&mut self, new_size: Size) { + self.inner_size.set(new_size); + // We want to prevent a resize request for this resized event. + self.inner_size_generation = self.inner_size.generation(); + self.root.invalidate(); + } + + pub fn keyboard_input( + &mut self, + window: W, + kludgine: &mut Kludgine, + device_id: DeviceId, + input: KeyEvent, + is_synthetic: bool, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + let target = self.tree.focused_widget().unwrap_or(self.root.node_id); + let Some(target) = self.tree.widget_from_node(target) else { + return IGNORED; + }; + let mut target = EventContext::new( + WidgetContext::new( + target, + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + + if recursively_handle_event(&mut target, |widget| { + widget.keyboard_input(device_id, input.clone(), is_synthetic) + }) + .is_some() + { + return HANDLED; + } + drop(target); + + self.handle_window_keyboard_input(&mut window, kludgine, input) + } + + pub fn mouse_wheel( + &mut self, + window: W, + kludgine: &mut Kludgine, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + let widget = self + .tree + .hovered_widget() + .and_then(|hovered| self.tree.widget_from_node(hovered)) + .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); + + let mut widget = EventContext::new( + WidgetContext::new( + widget, + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + if recursively_handle_event(&mut widget, |widget| { + widget.mouse_wheel(device_id, delta, phase) + }) + .is_some() + { + HANDLED + } else { + IGNORED + } + } + + fn ime(&mut self, window: W, kludgine: &mut Kludgine, ime: &Ime) -> EventHandling + where + W: PlatformWindowImplementation, + { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + let widget = self + .tree + .focused_widget() + .and_then(|hovered| self.tree.widget_from_node(hovered)) + .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); + let mut target = EventContext::new( + WidgetContext::new( + widget, + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + + if recursively_handle_event(&mut target, |widget| widget.ime(ime.clone())).is_some() { + HANDLED + } else { + IGNORED + } + } + + fn cursor_moved( + &mut self, + window: W, + kludgine: &mut Kludgine, + device_id: DeviceId, + position: impl Into>, + ) where + W: PlatformWindowImplementation, + { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + + let location = position.into(); + self.cursor.location = Some(location); + + EventContext::new( + WidgetContext::new( + self.root.clone(), + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ) + .update_hovered_widget(); + + if let Some(state) = self.mouse_buttons.get(&device_id) { + // Mouse Drag + for (button, handler) in state { + let Some(handler) = self.tree.widget(*handler) else { + continue; + }; + let mut context = EventContext::new( + WidgetContext::new( + handler.clone(), + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + let Some(last_rendered_at) = context.last_layout() else { + continue; + }; + context.mouse_drag(location - last_rendered_at.origin, device_id, *button); + } + } + } + + fn cursor_left(&mut self, window: W, kludgine: &mut Kludgine) + where + W: PlatformWindowImplementation, + { + if self.cursor.widget.take().is_some() { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + + let mut context = EventContext::new( + WidgetContext::new( + self.root.clone(), + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + context.clear_hover(); + } + } + + fn mouse_input( + &mut self, + window: W, + kludgine: &mut Kludgine, + device_id: DeviceId, + state: ElementState, + button: MouseButton, + ) -> EventHandling + where + W: PlatformWindowImplementation, + { + let mut window = RunningWindow::new( + window, + kludgine.id(), + &self.redraw_status, + &self.cushy, + &self.focused, + &self.occluded, + &self.inner_size, + ); + match state { + ElementState::Pressed => { + EventContext::new( + WidgetContext::new( + self.root.clone(), + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ) + .clear_focus(); + + if let (ElementState::Pressed, Some(location), Some(hovered)) = ( + state, + self.cursor.location, + self.cursor.widget.and_then(|id| self.tree.widget(id)), + ) { + if let Some(handler) = recursively_handle_event( + &mut EventContext::new( + WidgetContext::new( + hovered.clone(), + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ), + |context| { + let Some(layout) = context.last_layout() else { + return IGNORED; + }; + let relative = location - layout.origin; + context.mouse_down(relative, device_id, button) + }, + ) { + self.mouse_buttons + .entry(device_id) + .or_default() + .insert(button, handler.id()); + return HANDLED; + } + } + IGNORED + } + ElementState::Released => { + let Some(device_buttons) = self.mouse_buttons.get_mut(&device_id) else { + return IGNORED; + }; + let Some(handler) = device_buttons.remove(&button) else { + return IGNORED; + }; + if device_buttons.is_empty() { + self.mouse_buttons.remove(&device_id); + } + let Some(handler) = self.tree.widget(handler) else { + return IGNORED; + }; + let cursor_location = self.cursor.location; + let mut context = EventContext::new( + WidgetContext::new( + handler, + &self.current_theme, + &mut window, + self.theme_mode.get(), + &mut self.cursor, + ), + kludgine, + ); + + let relative = if let (Some(last_rendered), Some(location)) = + (context.last_layout(), cursor_location) + { + Some(location - last_rendered.origin) + } else { + None + }; + + context.mouse_up(relative, device_id, button); + HANDLED + } + } + } } #[derive(Clone, Copy, Eq, PartialEq, Debug)] @@ -1402,10 +1747,7 @@ where window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { - self.inner_size.set(window.inner_size()); - // We want to prevent a resize request for this resized event. - self.inner_size_generation = self.inner_size.generation(); - self.root.invalidate(); + self.resized(window.inner_size()); } // fn theme_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} @@ -1422,81 +1764,22 @@ where &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, - device_id: DeviceId, + device_id: winit::event::DeviceId, input: KeyEvent, is_synthetic: bool, ) { - let target = self.tree.focused_widget().unwrap_or(self.root.node_id); - let Some(target) = self.tree.widget_from_node(target) else { - return; - }; - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - let mut target = EventContext::new( - WidgetContext::new( - target, - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - - let handled = recursively_handle_event(&mut target, |widget| { - widget.keyboard_input(device_id, input.clone(), is_synthetic) - }) - .is_some(); - drop(target); - - if !handled { - self.handle_window_keyboard_input(&mut window, kludgine, input); - } + self.keyboard_input(window, kludgine, device_id.into(), input, is_synthetic); } fn mouse_wheel( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, - device_id: DeviceId, + device_id: winit::event::DeviceId, delta: MouseScrollDelta, phase: TouchPhase, ) { - let widget = self - .tree - .hovered_widget() - .and_then(|hovered| self.tree.widget_from_node(hovered)) - .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); - - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - let mut widget = EventContext::new( - WidgetContext::new( - widget, - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - recursively_handle_event(&mut widget, |widget| { - widget.mouse_wheel(device_id, delta, phase) - }); + self.mouse_wheel(window, kludgine, device_id.into(), delta, phase); } // fn modifiers_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} @@ -1507,219 +1790,37 @@ where kludgine: &mut Kludgine, ime: Ime, ) { - let widget = self - .tree - .focused_widget() - .and_then(|hovered| self.tree.widget_from_node(hovered)) - .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - let mut target = EventContext::new( - WidgetContext::new( - widget, - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - - let _handled = - recursively_handle_event(&mut target, |widget| widget.ime(ime.clone())).is_some(); + self.ime(window, kludgine, &ime); } fn cursor_moved( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, - device_id: DeviceId, + device_id: winit::event::DeviceId, position: PhysicalPosition, ) { - let location = Point::::from(position); - self.cursor.location = Some(location); - - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - - EventContext::new( - WidgetContext::new( - self.root.clone(), - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ) - .update_hovered_widget(); - - if let Some(state) = self.mouse_buttons.get(&device_id) { - // Mouse Drag - for (button, handler) in state { - let Some(handler) = self.tree.widget(*handler) else { - continue; - }; - let mut context = EventContext::new( - WidgetContext::new( - handler.clone(), - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - let Some(last_rendered_at) = context.last_layout() else { - continue; - }; - context.mouse_drag(location - last_rendered_at.origin, device_id, *button); - } - } + self.cursor_moved(window, kludgine, device_id.into(), position); } fn cursor_left( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, - _device_id: DeviceId, + _device_id: winit::event::DeviceId, ) { - if self.cursor.widget.take().is_some() { - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - let mut context = EventContext::new( - WidgetContext::new( - self.root.clone(), - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - context.clear_hover(); - } + self.cursor_left(window, kludgine); } fn mouse_input( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, - device_id: DeviceId, + device_id: winit::event::DeviceId, state: ElementState, button: MouseButton, ) { - let mut window = RunningWindow::new( - window, - kludgine.id(), - &self.redraw_status, - &self.cushy, - &self.focused, - &self.occluded, - &self.inner_size, - ); - match state { - ElementState::Pressed => { - EventContext::new( - WidgetContext::new( - self.root.clone(), - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ) - .clear_focus(); - - if let (ElementState::Pressed, Some(location), Some(hovered)) = ( - state, - self.cursor.location, - self.cursor.widget.and_then(|id| self.tree.widget(id)), - ) { - if let Some(handler) = recursively_handle_event( - &mut EventContext::new( - WidgetContext::new( - hovered.clone(), - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ), - |context| { - let Some(layout) = context.last_layout() else { - return IGNORED; - }; - let relative = location - layout.origin; - context.mouse_down(relative, device_id, button) - }, - ) { - self.mouse_buttons - .entry(device_id) - .or_default() - .insert(button, handler.id()); - } - } - } - ElementState::Released => { - let Some(device_buttons) = self.mouse_buttons.get_mut(&device_id) else { - return; - }; - let Some(handler) = device_buttons.remove(&button) else { - return; - }; - if device_buttons.is_empty() { - self.mouse_buttons.remove(&device_id); - } - let Some(handler) = self.tree.widget(handler) else { - return; - }; - let cursor_location = self.cursor.location; - let mut context = EventContext::new( - WidgetContext::new( - handler, - &self.current_theme, - &mut window, - self.theme_mode.get(), - &mut self.cursor, - ), - kludgine, - ); - - let relative = if let (Some(last_rendered), Some(location)) = - (context.last_layout(), cursor_location) - { - Some(location - last_rendered.origin) - } else { - None - }; - - context.mouse_up(relative, device_id, button); - } - } + self.mouse_input(window, kludgine, device_id.into(), state, button); } fn theme_changed( @@ -2419,9 +2520,27 @@ impl VirtualWindow { 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); let mut frame = self.kludgine.next_frame(); let mut gfx = frame.render(pass, device, queue); self.window.contents.render(1., &mut gfx); + if let Some(additional) = additional_drawing { + additional.render(1., &mut gfx); + } drop(gfx); frame.submit(queue) } @@ -2434,6 +2553,7 @@ 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); @@ -2472,6 +2592,95 @@ impl VirtualWindow { pub fn closed(&self) -> bool { self.state.closed } + + /// Returns a reference to the window's state. + #[must_use] + pub const fn state(&self) -> &VirtualState { + &self.state + } + + /// 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, + device_id: DeviceId, + input: KeyEvent, + is_synthetic: bool, + ) -> EventHandling { + self.window.keyboard_input( + &mut self.state, + &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, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + ) -> EventHandling { + self.window + .mouse_wheel(&mut self.state, &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, ime: &Ime) -> EventHandling { + self.window.ime(&mut self.state, &mut self.kludgine, 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); + } + + /// 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); + } + + /// Provides mouse input events to tihs window. + /// + /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. + pub fn mouse_input( + &mut self, + device_id: DeviceId, + state: ElementState, + button: MouseButton, + ) -> EventHandling { + self.window.mouse_input( + &mut self.state, + &mut self.kludgine, + device_id, + state, + button, + ) + } } /// A color format containing 8-bit red, green, and blue channels. @@ -2565,6 +2774,18 @@ where self } + /// Sets the DPI scaling to apply to this virtual window. + /// + /// When scale is 1.0, resolution-independent content will be rendered at + /// 96-ppi. + /// + /// This setting does not affect the image's pixel dimensions. + #[must_use] + pub fn scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } + /// Returns an initialized [`VirtualRecorder`]. pub fn finish(self) -> Result, VirtualRecorderError> { VirtualRecorder::new(self.size, self.scale, self.contents) @@ -2579,11 +2800,14 @@ struct Capture { /// A recorder of a [`VirtualWindow`]. pub struct VirtualRecorder { - window: VirtualWindow, + /// The virtual window being recorded. + pub window: VirtualWindow, device: wgpu::Device, queue: wgpu::Queue, capture: Option, data: Vec, + cursor: Dynamic>, + cursor_graphic: Drawing, format: PhantomData, } @@ -2614,26 +2838,26 @@ where None, ))?; - let mut recorder = Self { - window: VirtualWindow::new( - contents.make_widget(), - 4, - size, - scale, - Format::HAS_ALPHA, - &device, - &queue, - ), + let window = VirtualWindow::new( + contents.make_widget(), + 4, + size, + scale, + Format::HAS_ALPHA, + &device, + &queue, + ); + + Ok(Self { + window, device, queue, + cursor: Dynamic::default(), + cursor_graphic: Drawing::default(), capture: None, data: Vec::new(), format: PhantomData, - }; - - recorder.refresh()?; - - Ok(recorder) + }) } /// Returns the tightly-packed captured bytes. @@ -2643,11 +2867,6 @@ where &self.data } - /// Returns the current size of the recorder. - pub const fn size(&self) -> Size { - self.window.kludgine.size() - } - fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64) { if self .capture @@ -2694,9 +2913,17 @@ where let capture = self.capture.as_ref().assert("always initialized above"); + let mut gfx = self.window.graphics(&self.device, &self.queue); + let mut frame = self.cursor_graphic.new_frame(&mut gfx); + frame.draw_shape( + Shape::filled_circle(Px::new(4), Color::WHITE, Origin::Center) + .translate_by(self.cursor.get()), + ); + drop(frame); + self.window.prepare(&self.device, &self.queue); - self.window.render( + self.window.render_with( &wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -2713,6 +2940,7 @@ where }, &self.device, &self.queue, + Some(&self.cursor_graphic), ); let mut encoder = self @@ -2740,7 +2968,6 @@ where let map_result = map_result.clone(); let condvar = condvar.clone(); move || { - condvar.notify_one(); slice.map_async(wgpu::MapMode::Read, { move |result| { *map_result.lock().assert("thread panicked") = Some(result); @@ -2775,11 +3002,26 @@ where })?; self.data.extend_from_slice(&slice.get_mapped_range()); + capture.buffer.unmap(); Format::convert_rgba(&mut self.data, render_size.width.get(), bytes_per_row); Ok(()) } + + /// Sets the cursor position immediately. + pub fn set_cursor_position(&self, position: Point) { + self.cursor.set(position); + } + + /// Begins recording an animated png. + pub fn record_animated_png(&mut self, target_fps: u8) -> AnimationRecorder<'_, Format> { + AnimationRecorder { + recorder: self, + target_fps, + frames: Vec::new(), + } + } } fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { @@ -2787,6 +3029,146 @@ fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { * COPY_BYTES_PER_ROW_ALIGNMENT } +/// An animated PNG recorder. +pub struct AnimationRecorder<'a, Format> { + recorder: &'a mut VirtualRecorder, + target_fps: u8, + frames: Vec, +} + +impl AnimationRecorder<'_, Format> +where + Format: CaptureFormat, +{ + /// Animates the cursor to move from its current location to `location`. + pub fn animate_cursor_to( + &mut self, + location: Point, + over: Duration, + easing: impl Easing, + ) -> Result<(), VirtualRecorderError> { + self.recorder + .cursor + .transition_to(location) + .over(over) + .with_easing(easing) + .launch(); + self.wait_for(over) + } + + /// Waits for `duration`, rendering frames as needed. + pub fn wait_for(&mut self, duration: Duration) -> Result<(), VirtualRecorderError> { + self.wait_until(Instant::now() + duration) + } + + /// Waits until `time`, rendering frames as needed. + pub fn wait_until(&mut self, time: Instant) -> Result<(), VirtualRecorderError> { + let frame_duration = Duration::from_micros(1_000_000 / u64::from(self.target_fps)); + let mut last_frame = Instant::now(); + + loop { + let now = Instant::now(); + let final_frame = now > time; + + self.recorder + .window + .cursor_moved(DeviceId::Virtual(0), self.recorder.cursor.get()); + + let next_frame = match self.recorder.window.state.dynamic.redraw_target.get() { + RedrawTarget::Never => now + frame_duration, + RedrawTarget::Now => now, + RedrawTarget::At(instant) => now.min(instant), + }; + + if final_frame || next_frame == now { + let elapsed = now.saturating_duration_since(last_frame); + last_frame = now; + self.recorder.refresh()?; + match self.frames.last_mut() { + Some(frame) if frame.data == self.recorder.bytes() => { + frame.duration += elapsed; + } + _ => { + self.frames.push(Frame { + data: self.recorder.bytes().to_vec(), + duration: elapsed, + }); + } + } + } + + if now > time { + break; + } + + let render_duration = now.elapsed(); + std::thread::sleep(frame_duration.saturating_sub(render_duration)); + } + + Ok(()) + } + + /// Encodes the currently recorded frames into a new file at `path`. + pub fn write_to(&self, path: impl AsRef) -> Result<(), png::EncodingError> { + let mut file = std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(path)?; + let mut encoder = png::Encoder::new( + &mut file, + self.recorder.window.size().width.get(), + self.recorder.window.size().height.get(), + ); + encoder.set_color(png::ColorType::Rgb); + encoder.set_adaptive_filter(png::AdaptiveFilterType::Adaptive); + encoder.set_animated( + u32::try_from(self.frames.len()).assert("too many frames"), + 0, + )?; + encoder.set_compression(png::Compression::Best); + + let mut current_frame_delay = self + .frames + .first() + .assert("always at least one frame") + .duration; + encoder.set_frame_delay( + current_frame_delay + .as_millis() + // TODO should be checked + .cast(), + 1_000, + )?; + let mut writer = encoder.write_header()?; + for frame in &self.frames { + if current_frame_delay != frame.duration { + current_frame_delay = frame.duration; + writer.set_frame_delay( + current_frame_delay + .as_millis() + // TODO should be checked + .cast(), + 1_000, + )?; + } + + writer.write_image_data(&frame.data)?; + } + + writer.finish()?; + + file.sync_all()?; + + Ok(()) + } +} + +struct Frame { + data: Vec, + duration: Duration, +} + /// An error from a [`VirtualRecorder`]. #[derive(Debug)] pub enum VirtualRecorderError { @@ -2818,3 +3200,18 @@ impl From for VirtualRecorderError { Self::TooLarge } } + +/// A unique identifier of an input device. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum DeviceId { + /// A winit-supplied device id. + Winit(winit::event::DeviceId), + /// A simulated device. + Virtual(u64), +} + +impl From for DeviceId { + fn from(value: winit::event::DeviceId) -> Self { + Self::Winit(value) + } +} From 36b80e8f3437dfb68eca6d0619026e2614a83e64 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Wed, 3 Jan 2024 20:15:00 -0800 Subject: [PATCH 06/10] Moved frame mapping to its own thread This ensures frames render consistently. This change only affects the animation recorder. The regular virtual recorder interface blocks when refresh is called. --- examples/offscreen-apng.rs | 5 +- src/window.rs | 282 ++++++++++++++++++++++++------------- 2 files changed, 184 insertions(+), 103 deletions(-) diff --git a/examples/offscreen-apng.rs b/examples/offscreen-apng.rs index 8fc6751..b1e0452 100644 --- a/examples/offscreen-apng.rs +++ b/examples/offscreen-apng.rs @@ -10,7 +10,6 @@ fn ui() -> impl MakeWidget { } fn main() { - // The default recorder generated solid, rgb images. let mut recorder = ui() .build_recorder() .size(Size::new(320, 240)) @@ -32,7 +31,5 @@ fn main() { .animate_cursor_to(initial_point, Duration::from_millis(250), EaseInOutSine) .unwrap(); animation.wait_for(Duration::from_millis(500)).unwrap(); - animation - .write_to("examples/offscreen-animated.png") - .unwrap(); + animation.write_to("examples/offscreen-apng.png").unwrap(); } diff --git a/src/window.rs b/src/window.rs index 394920a..ee3afa5 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,12 +4,13 @@ use std::cell::RefCell; use std::collections::hash_map; use std::ffi::OsStr; use std::hash::Hash; +use std::io; use std::marker::PhantomData; use std::num::TryFromIntError; use std::ops::{Deref, DerefMut, Not}; use std::path::Path; use std::string::ToString; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock}; +use std::sync::{mpsc, Arc, Mutex, MutexGuard, OnceLock}; use std::time::{Duration, Instant}; use ahash::AHashMap; @@ -2793,18 +2794,75 @@ where } struct Capture { + bytes: u64, + bytes_per_row: u32, buffer: wgpu::Buffer, texture: Texture, multisample: Texture, } +impl Capture { + fn map_into( + &self, + buffer: &mut Vec, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> Result<(), wgpu::BufferAsyncError> + where + Format: CaptureFormat, + { + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + self.texture.copy_to_buffer( + wgpu::ImageCopyBuffer { + buffer: &self.buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(self.bytes_per_row), + rows_per_image: None, + }, + }, + &mut encoder, + ); + queue.submit([encoder.finish()]); + + let map_result = Arc::new(Mutex::new(None)); + let slice = self.buffer.slice(0..self.bytes); + + slice.map_async(wgpu::MapMode::Read, { + let map_result = map_result.clone(); + move |result| { + *map_result.lock().assert("thread panicked") = Some(result); + } + }); + + buffer.clear(); + buffer.reserve(self.bytes.cast()); + + loop { + device.poll(wgpu::Maintain::Poll); + let mut result = map_result.lock().assert("thread panicked"); + if let Some(result) = result.take() { + result?; + break; + } + } + + buffer.extend_from_slice(&slice.get_mapped_range()); + self.buffer.unmap(); + + Format::convert_rgba(buffer, self.texture.size().width.get(), self.bytes_per_row); + + Ok(()) + } +} + /// A recorder of a [`VirtualWindow`]. pub struct VirtualRecorder { /// The virtual window being recorded. pub window: VirtualWindow, - device: wgpu::Device, - queue: wgpu::Queue, - capture: Option, + device: Arc, + queue: Arc, + capture: Option>, data: Vec, cursor: Dynamic>, cursor_graphic: Drawing, @@ -2848,16 +2906,18 @@ where &queue, ); - Ok(Self { + let mut recorder = Self { window, - device, - queue, + device: Arc::new(device), + queue: Arc::new(queue), cursor: Dynamic::default(), cursor_graphic: Drawing::default(), capture: None, data: Vec::new(), format: PhantomData, - }) + }; + recorder.refresh()?; + Ok(recorder) } /// Returns the tightly-packed captured bytes. @@ -2867,7 +2927,7 @@ where &self.data } - fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64) { + fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64, bytes_per_row: u32) { if self .capture .as_ref() @@ -2896,20 +2956,21 @@ where usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); - self.capture = Some(Capture { + self.capture = Some(Box::new(Capture { + bytes, + bytes_per_row, buffer, texture, multisample, - }); + })); } } - /// Redraws the contents. - pub fn refresh(&mut self) -> Result<(), wgpu::BufferAsyncError> { + fn redraw(&mut self) { let render_size = self.window.kludgine.size().ceil(); let bytes_per_row = copy_buffer_aligned_bytes_per_row(render_size.width.get() * 4); let size = u64::from(bytes_per_row) * u64::from(render_size.height.get()); - self.recreate_buffers_if_needed(render_size, size); + self.recreate_buffers_if_needed(render_size, size, bytes_per_row); let capture = self.capture.as_ref().assert("always initialized above"); @@ -2942,69 +3003,15 @@ where &self.queue, Some(&self.cursor_graphic), ); + } - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); - capture.texture.copy_to_buffer( - wgpu::ImageCopyBuffer { - buffer: &capture.buffer, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row), - rows_per_image: None, - }, - }, - &mut encoder, - ); - self.queue.submit([encoder.finish()]); + /// Redraws the contents. + pub fn refresh(&mut self) -> Result<(), wgpu::BufferAsyncError> { + self.redraw(); - let map_result = Arc::new(Mutex::new(None)); - let condvar = Arc::new(Condvar::new()); - let slice = capture.buffer.slice(0..size); + let capture = self.capture.as_ref().assert("always initialized above"); - std::thread::scope(|scope| { - scope.spawn({ - let map_result = map_result.clone(); - let condvar = condvar.clone(); - move || { - slice.map_async(wgpu::MapMode::Read, { - move |result| { - *map_result.lock().assert("thread panicked") = Some(result); - condvar.notify_one(); - } - }); - } - }); - - // Now that we've queued up the data mapping thread, let's make sure - // our vec is allocated. Since an allocation can take a moment, this - // is the perfect to do it. - self.data.clear(); - self.data.reserve(size.cast()); - - // Wait for the buffer to have been mapped. - loop { - self.device.poll(wgpu::Maintain::Poll); - - let mut result = map_result.lock().assert("thread panicked"); - if let Some(result) = result.take() { - result?; - break; - } - - let _guard = condvar - .wait_timeout(result, Duration::from_millis(1)) - .assert("thread panicked"); - } - - Ok(()) - })?; - - self.data.extend_from_slice(&slice.get_mapped_range()); - capture.buffer.unmap(); - - Format::convert_rgba(&mut self.data, render_size.width.get(), bytes_per_row); + capture.map_into::(&mut self.data, &self.device, &self.queue)?; Ok(()) } @@ -3017,9 +3024,9 @@ where /// Begins recording an animated png. pub fn record_animated_png(&mut self, target_fps: u8) -> AnimationRecorder<'_, Format> { AnimationRecorder { - recorder: self, target_fps, - frames: Vec::new(), + assembler: FrameAssembler::spawn::(self.device.clone(), self.queue.clone()), + recorder: self, } } } @@ -3033,7 +3040,7 @@ fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { pub struct AnimationRecorder<'a, Format> { recorder: &'a mut VirtualRecorder, target_fps: u8, - frames: Vec, + assembler: FrameAssembler, } impl AnimationRecorder<'_, Format> @@ -3081,19 +3088,17 @@ where }; if final_frame || next_frame == now { + // Try to reuse an existing capture instead of forcing an + // allocation. + if let Ok(capture) = self.assembler.resuable_captures.try_recv() { + self.recorder.capture = Some(capture); + } let elapsed = now.saturating_duration_since(last_frame); last_frame = now; - self.recorder.refresh()?; - match self.frames.last_mut() { - Some(frame) if frame.data == self.recorder.bytes() => { - frame.duration += elapsed; - } - _ => { - self.frames.push(Frame { - data: self.recorder.bytes().to_vec(), - duration: elapsed, - }); - } + self.recorder.redraw(); + let capture = self.recorder.capture.take().assert("always present"); + if self.assembler.sender.send((capture, elapsed)).is_err() { + break; } } @@ -3109,7 +3114,8 @@ where } /// Encodes the currently recorded frames into a new file at `path`. - pub fn write_to(&self, path: impl AsRef) -> Result<(), png::EncodingError> { + pub fn write_to(self, path: impl AsRef) -> Result<(), VirtualRecorderError> { + let frames = self.assembler.finish()?; let mut file = std::fs::OpenOptions::new() .create(true) .truncate(true) @@ -3122,17 +3128,10 @@ where ); encoder.set_color(png::ColorType::Rgb); encoder.set_adaptive_filter(png::AdaptiveFilterType::Adaptive); - encoder.set_animated( - u32::try_from(self.frames.len()).assert("too many frames"), - 0, - )?; + encoder.set_animated(u32::try_from(frames.len()).assert("too many frames"), 0)?; encoder.set_compression(png::Compression::Best); - let mut current_frame_delay = self - .frames - .first() - .assert("always at least one frame") - .duration; + let mut current_frame_delay = frames.first().assert("always at least one frame").duration; encoder.set_frame_delay( current_frame_delay .as_millis() @@ -3141,7 +3140,7 @@ where 1_000, )?; let mut writer = encoder.write_header()?; - for frame in &self.frames { + for frame in &frames { if current_frame_delay != frame.duration { current_frame_delay = frame.duration; writer.set_frame_delay( @@ -3181,6 +3180,14 @@ pub enum VirtualRecorderError { TooLarge, /// An error occurred trying to read a buffer. MapBuffer(wgpu::BufferAsyncError), + /// An error occurred encoding a png image. + PngEncode(png::EncodingError), +} + +impl From for VirtualRecorderError { + fn from(value: png::EncodingError) -> Self { + Self::PngEncode(value) + } } impl From for VirtualRecorderError { @@ -3201,6 +3208,12 @@ impl From for VirtualRecorderError { } } +impl From for VirtualRecorderError { + fn from(value: io::Error) -> Self { + Self::PngEncode(value.into()) + } +} + /// A unique identifier of an input device. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum DeviceId { @@ -3215,3 +3228,74 @@ impl From for DeviceId { Self::Winit(value) } } + +struct FrameAssembler { + sender: mpsc::SyncSender<(Box, Duration)>, + result: mpsc::Receiver, VirtualRecorderError>>, + resuable_captures: mpsc::Receiver>, +} + +impl FrameAssembler { + fn spawn(device: Arc, queue: Arc) -> Self + where + Format: CaptureFormat, + { + let (frame_sender, frame_receiver) = mpsc::sync_channel(1000); + let (finished_frame_sender, finished_frame_receiver) = mpsc::sync_channel(600); + let (result_sender, result_receiver) = mpsc::sync_channel(1); + + std::thread::spawn(move || { + Self::assembler_thread::( + &frame_receiver, + &result_sender, + &finished_frame_sender, + &device, + &queue, + ); + }); + + Self { + sender: frame_sender, + result: result_receiver, + resuable_captures: finished_frame_receiver, + } + } + + fn finish(self) -> Result, VirtualRecorderError> { + drop(self.sender); + self.result.recv().assert("thread panicked") + } + + fn assembler_thread( + frames: &mpsc::Receiver<(Box, Duration)>, + result: &mpsc::SyncSender, VirtualRecorderError>>, + reusable: &mpsc::SyncSender>, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) where + Format: CaptureFormat, + { + let mut assembled = Vec::::new(); + let mut buffer = Vec::new(); + while let Ok((capture, elapsed)) = frames.recv() { + if let Err(err) = capture.map_into::(&mut buffer, device, queue) { + let _result = result.send(Err(err.into())); + return; + } + match assembled.last_mut() { + Some(frame) if frame.data == buffer => { + frame.duration += elapsed; + } + _ => { + assembled.push(Frame { + data: std::mem::take(&mut buffer), + duration: elapsed, + }); + } + } + let _result = reusable.try_send(capture); + } + + let _result = result.send(Ok(assembled)); + } +} From eb2013311606c18df16b27e18fd0ec66b624231a Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 4 Jan 2024 13:54:30 -0800 Subject: [PATCH 07/10] Reinstating weak_clone and non-weak callbacks Somehow I missed that my changes for weak callbacks broke the theme editor. I thought I had it working with the try_get changes, but I discovered several flaws in this approach. In the end, ownership has been transferred to the CallbackHandle, and a CallbackHandle can relinquish its reference to create weak graphs. This is how weak_clone now works. --- examples/theme.rs | 18 +++++----- src/debug.rs | 2 +- src/value.rs | 92 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/examples/theme.rs b/examples/theme.rs index 0a81a4f..3e8d112 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -213,28 +213,26 @@ fn optional_editor(label: &str, color: &Dynamic) -> (Dynamic, } fn color_editor(color: &Dynamic) -> impl MakeWidget { - let hue = color.map_each(|color| color.hue.into_positive_degrees()); + let hue = color.map_each_cloned(|color| color.hue.into_positive_degrees()); hue.for_each_cloned({ let color = color.clone(); move |hue| { - if let Ok(mut source) = color.try_get() { - source.hue = OklabHue::new(hue); - color.set(source); - } + let mut source = color.get(); + source.hue = OklabHue::new(hue); + color.set(source); } }) .persist(); let hue_text = hue.linked_string(); - let saturation = color.map_each(|color| color.saturation); + let saturation = color.map_each_cloned(|color| color.saturation); saturation .for_each_cloned({ let color = color.clone(); move |saturation| { - if let Ok(mut source) = color.try_get() { - source.saturation = saturation; - color.set(source); - } + let mut source = color.get(); + source.saturation = saturation; + color.set(source); } }) .persist(); diff --git a/src/debug.rs b/src/debug.rs index 95c3052..a8896d8 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -53,7 +53,7 @@ impl DebugContext { section.values.lock().push(Box::new(RegisteredValue { label: label.into(), value: reader.clone(), - widget: make_observer(value.clone()).make_widget(), + widget: make_observer(value.weak_clone()).make_widget(), })) }); let this = self.clone(); diff --git a/src/value.rs b/src/value.rs index 0f7ead2..acece66 100644 --- a/src/value.rs +++ b/src/value.rs @@ -290,6 +290,30 @@ pub trait Source { mapped } + /// Returns a new [`Dynamic`] that contains a clone of each value from + /// `self`. + /// + /// The returned dynamic does not hold a strong reference to `self`, + /// ensuring that `self` can be cleaned up even if the returned dynamic + /// still exists. + fn weak_clone(&self) -> Dynamic + where + T: Clone + Send + 'static, + { + let mapped = Dynamic::new(self.get()); + let mapped_weak = mapped.downgrade(); + + mapped.set_source( + self.for_each_cloned_try(move |value| { + let mapped = mapped_weak.upgrade().ok_or(CallbackDisconnected)?; + *mapped.lock() = value.clone(); + Ok(()) + }) + .weak(), + ); + mapped + } + /// Returns a new dynamic that is updated using `U::from(T.clone())` each /// time `self` is updated. #[must_use] @@ -510,7 +534,7 @@ impl Source for Arc> { + 'static, { let this = WeakDynamic(Arc::downgrade(self)); - DynamicData::for_each(self, move || { + dynamic_for_each(self, move || { let this = this.upgrade().ok_or(CallbackDisconnected)?; this.map_generational(&mut for_each)?; Ok(()) @@ -523,7 +547,7 @@ impl Source for Arc> { F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, { let this = WeakDynamic(Arc::downgrade(self)); - DynamicData::for_each(self, move || { + dynamic_for_each(self, move || { let this = this.upgrade().ok_or(CallbackDisconnected)?; if let Ok(value) = this.try_map_generational(GenerationalValue::clone) { @@ -726,6 +750,7 @@ impl Source for Owned { let mut callbacks = self.callbacks.active.lock().ignore_poison(); CallbackHandle(CallbackHandleInner::Single(CallbackHandleData { id: Some(callbacks.push(Box::new(for_each))), + owner: None, callbacks: self.callbacks.clone(), })) } @@ -944,9 +969,10 @@ impl Dynamic { Ok(()) })); - let t_weak = self.downgrade(); + // The linked dynamic holds a reference to the original, since it's + // being created from the original. + let t = self.clone(); self.set_source(r.for_each_try(move |r| { - let t = t_weak.upgrade().ok_or(CallbackDisconnected)?; if let Some(update) = r_into_t(r).into() { let _result = t.replace(update); } @@ -1388,18 +1414,20 @@ impl DynamicData { Ok(old) } +} - pub fn for_each(&self, map: F) -> CallbackHandle - where - F: for<'a> FnMut() -> Result<(), CallbackDisconnected> + Send + 'static, - { - let state = self.state().expect("deadlocked"); - let mut data = state.callbacks.callbacks.lock().ignore_poison(); - CallbackHandle(CallbackHandleInner::Single(CallbackHandleData { - id: Some(data.callbacks.push(Box::new(map))), - callbacks: state.callbacks.clone(), - })) - } +fn dynamic_for_each(this: &Arc>, map: F) -> CallbackHandle +where + F: for<'a> FnMut() -> Result<(), CallbackDisconnected> + Send + 'static, + T: Send + 'static, +{ + let state = this.state().expect("deadlocked"); + let mut data = state.callbacks.callbacks.lock().ignore_poison(); + CallbackHandle(CallbackHandleInner::Single(CallbackHandleData { + id: Some(data.callbacks.push(Box::new(map))), + owner: Some(this.clone()), + callbacks: state.callbacks.clone(), + })) } /// A callback function is no longer connected to its source. @@ -1467,8 +1495,12 @@ enum CallbackHandleInner { Multi(Vec), } +trait ReferencedDynamic: Sync + Send + 'static {} +impl ReferencedDynamic for T where T: Sync + Send + 'static {} + struct CallbackHandleData { id: Option, + owner: Option>, callbacks: Arc, } @@ -1508,6 +1540,33 @@ impl CallbackHandle { } } } + + /// Drops any references to owning [`Dynamic`]s associated with this + /// callback. + /// + /// This enables creating weak connections between callback graphs. + pub fn forget_owners(&mut self) { + match &mut self.0 { + CallbackHandleInner::None => {} + CallbackHandleInner::Single(handle) => { + handle.owner = None; + } + CallbackHandleInner::Multi(handles) => { + for handle in handles { + handle.owner = None; + } + } + } + } + + /// Drops any references to owning [`Dynamic`]s associated with this + /// callback, and returns self. + /// + /// This uses [`Self::forget_owners()`]. + pub fn weak(mut self) -> Self { + self.forget_owners(); + self + } } impl Eq for CallbackHandle {} @@ -1539,6 +1598,7 @@ impl Drop for CallbackHandleData { } } } + impl PartialEq for CallbackHandleData { fn eq(&self, other: &Self) -> bool { self.id == other.id && Arc::ptr_eq(&self.callbacks, &other.callbacks) @@ -2932,7 +2992,7 @@ macro_rules! impl_tuple_for_each_cloned { } }; ($self:ident $for_each:ident $handles:ident [] [$type:ident $field:tt $var:ident]) => { - $handles += $self.$field.for_each(move |field: &$type| $for_each((field.clone(),))); + $handles += $self.$field.for_each_cloned(move |field| $for_each((field,))); }; ($self:ident $for_each:ident $handles:ident [] [$($type:ident $field:tt $var:ident),+]) => { let $for_each = Arc::new(Mutex::new($for_each)); From a197bb5e8132a4a47a08914815eed0f9b999e702 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 4 Jan 2024 13:56:45 -0800 Subject: [PATCH 08/10] Unit-tested, auto-generated screenshots This commit adds my first take at creating a harness for a user's guide using the new capture functionality. The example has tests that ensure the align widget creates the expected results. --- Cargo.lock | 14 +- Cargo.toml | 4 +- examples/offscreen-apng.rs | 1 + examples/offscreen.rs | 20 +-- guide/.gitignore | 1 + guide/book.toml | 6 + guide/guide-examples/Cargo.toml | 8 + guide/guide-examples/examples/align.rs | 137 +++++++++++++++ guide/guide-examples/src/lib.rs | 69 ++++++++ guide/src/SUMMARY.md | 3 + guide/src/chapter_1.md | 33 ++++ guide/src/examples/align-horizontal.png | Bin 0 -> 33666 bytes src/styles.rs | 9 +- src/widgets/space.rs | 40 ++++- src/window.rs | 212 ++++++++++++++++++++---- 15 files changed, 496 insertions(+), 61 deletions(-) create mode 100644 guide/.gitignore create mode 100644 guide/book.toml create mode 100644 guide/guide-examples/Cargo.toml create mode 100644 guide/guide-examples/examples/align.rs create mode 100644 guide/guide-examples/src/lib.rs create mode 100644 guide/src/SUMMARY.md create mode 100644 guide/src/chapter_1.md create mode 100644 guide/src/examples/align-horizontal.png diff --git a/Cargo.lock b/Cargo.lock index 7a9ed23..5e83ec8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -988,6 +988,13 @@ dependencies = [ "bitflags 2.4.1", ] +[[package]] +name = "guide-examples" +version = "0.0.0" +dependencies = [ + "cushy", +] + [[package]] name = "half" version = "2.2.1" @@ -1180,7 +1187,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#6b0d5fa0477daaf79f75a6f7dad819377a45e645" +source = "git+https://github.com/khonsulabs/kludgine#8017775228d22b5efce6d6b7a89e81dfc9b25961" dependencies = [ "ahash", "alot", @@ -1193,6 +1200,7 @@ dependencies = [ "intentional", "justjson", "lyon_tessellation", + "palette", "pollster", "smallvec", "unicode-bidi", @@ -2332,9 +2340,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.47" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 74fb601..504993e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cushy-macros"] +members = ["cushy-macros", "guide/guide-examples"] [package] name = "cushy" @@ -41,6 +41,7 @@ zeroize = "1.6.1" unicode-segmentation = "1.10.1" pollster = "0.3.0" png = "0.17.10" +image = { version = "0.24.7", features = ["png"] } # [patch.crates-io] @@ -60,7 +61,6 @@ opt-level = 2 [dev-dependencies] rand = "0.8.5" -image = { version = "0.24.7", features = ["png"] } [profile.release] # debug = true diff --git a/examples/offscreen-apng.rs b/examples/offscreen-apng.rs index b1e0452..e1e699b 100644 --- a/examples/offscreen-apng.rs +++ b/examples/offscreen-apng.rs @@ -17,6 +17,7 @@ fn main() { .unwrap(); let initial_point = Point::new(Px::new(140), Px::new(150)); recorder.set_cursor_position(initial_point); + recorder.set_cursor_visible(true); recorder.refresh().unwrap(); let mut animation = recorder.record_animated_png(60); animation diff --git a/examples/offscreen.rs b/examples/offscreen.rs index c1b43ed..7ac5d96 100644 --- a/examples/offscreen.rs +++ b/examples/offscreen.rs @@ -12,15 +12,7 @@ fn main() { .size(Size::new(320, 240)) .finish() .unwrap(); - image::save_buffer_with_format( - "examples/offscreen.png", - recorder.bytes(), - recorder.window.size().width.get(), - recorder.window.size().height.get(), - image::ColorType::Rgb8, - image::ImageFormat::Png, - ) - .unwrap(); + recorder.image().save("examples/offscreen.png").unwrap(); // Creating a recorder with alpha makes the virtual window transparent. let recorder = ui() @@ -29,15 +21,7 @@ fn main() { .size(Size::new(320, 240)) .finish() .unwrap(); - image::save_buffer_with_format( - "examples/offscreen-transparent.png", - recorder.bytes(), - recorder.window.size().width.get(), - recorder.window.size().height.get(), - image::ColorType::Rgba8, - image::ImageFormat::Png, - ) - .unwrap(); + recorder.image().save("examples/offscreen.png").unwrap(); } #[test] diff --git a/guide/.gitignore b/guide/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/guide/.gitignore @@ -0,0 +1 @@ +book diff --git a/guide/book.toml b/guide/book.toml new file mode 100644 index 0000000..0690ff1 --- /dev/null +++ b/guide/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Johnson"] +language = "en" +multilingual = false +src = "src" +title = "Cushy User's Guide" diff --git a/guide/guide-examples/Cargo.toml b/guide/guide-examples/Cargo.toml new file mode 100644 index 0000000..d1f540f --- /dev/null +++ b/guide/guide-examples/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "guide-examples" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +cushy = { version = "0.2.0", path = "../../" } diff --git a/guide/guide-examples/examples/align.rs b/guide/guide-examples/examples/align.rs new file mode 100644 index 0000000..2797060 --- /dev/null +++ b/guide/guide-examples/examples/align.rs @@ -0,0 +1,137 @@ +use cushy::figures::units::{Lp, Px}; +use cushy::figures::{Point, Size}; +use cushy::styles::{Edges, ThemePair}; +use cushy::widget::MakeWidget; +use cushy::widgets::Space; +use guide_examples::BookExample; + +fn content() -> impl MakeWidget { + Space::primary().size(Size::squared(Px::new(32))) +} + +fn main() { + BookExample::new( + "align-horizontal", + "Default Behavior" + .and(content()) + .and("align_left()") + .and({ + // ANCHOR: align-left + content().align_left() + // ANCHOR_END: align-left + }) + .and("pad_by().align_left()") + .and({ + // ANCHOR: align-left-pad + content() + .pad_by(Edges::default().with_left(Lp::inches(1))) + .align_left() + // ANCHOR_END: align-left-pad + }) + .and("centered()") + .and({ + // ANCHOR: centered + content().centered() + // ANCHOR_END: centered + }) + .and("pad_by().align_right()") + .and({ + // ANCHOR: align-right-pad + content() + .pad_by(Edges::default().with_right(Lp::inches(1))) + .align_right() + // ANCHOR_END: align-right-pad + }) + .and("align_right()") + .and({ + // ANCHOR: align-right + content().align_right() + // ANCHOR_END: align-right + }) + .into_rows(), + ) + .still_frame(|recorder| { + const LEFT: u32 = 40; + const PADDING: u32 = 96; + const RIGHT: u32 = 710; + const CENTER: u32 = 375; + + let container_color = ThemePair::default().dark.surface.lowest_container; + let primary = ThemePair::default().dark.primary.color; + + recorder.assert_pixel_color(Point::new(LEFT, 35), container_color, "surface"); + + // Default fills the entire space + recorder.assert_pixel_color(Point::new(LEFT, 70), primary, "default spacer"); + recorder.assert_pixel_color(Point::new(CENTER, 70), primary, "default spacer"); + recorder.assert_pixel_color(Point::new(RIGHT, 70), primary, "default spacer"); + + // align-left + recorder.assert_pixel_color(Point::new(LEFT, 140), primary, "align-left spacer"); + recorder.assert_pixel_color( + Point::new(LEFT + PADDING, 140), + container_color, + "align-left empty", + ); + + // align-left-pad + recorder.assert_pixel_color( + Point::new(LEFT + PADDING, 215), + primary, + "align-left-pad spacer", + ); + recorder.assert_pixel_color( + Point::new(LEFT, 215), + container_color, + "align-left-pad empty before", + ); + recorder.assert_pixel_color( + Point::new(CENTER, 215), + container_color, + "align-left-pad empty after", + ); + + // centered + recorder.assert_pixel_color(Point::new(CENTER, 295), primary, "centered spacer"); + recorder.assert_pixel_color( + Point::new(LEFT + PADDING, 295), + container_color, + "centered empty before", + ); + recorder.assert_pixel_color( + Point::new(RIGHT - PADDING, 295), + container_color, + "centered empty after", + ); + + // align-right-pad + recorder.assert_pixel_color( + Point::new(RIGHT - PADDING, 360), + primary, + "align-right-pad spacer", + ); + recorder.assert_pixel_color( + Point::new(CENTER, 360), + container_color, + "align-right-pad empty before", + ); + recorder.assert_pixel_color( + Point::new(RIGHT, 360), + container_color, + "align-right-pad empty after", + ); + + // align-right + recorder.assert_pixel_color(Point::new(RIGHT, 435), primary, "align-right spacer"); + recorder.assert_pixel_color( + Point::new(RIGHT - PADDING, 435), + container_color, + "align-right empty", + ); + }); +} + +#[test] +fn runs() { + main(); +} diff --git a/guide/guide-examples/src/lib.rs b/guide/guide-examples/src/lib.rs new file mode 100644 index 0000000..99930ff --- /dev/null +++ b/guide/guide-examples/src/lib.rs @@ -0,0 +1,69 @@ +use std::panic::AssertUnwindSafe; +use std::path::PathBuf; + +use cushy::figures::units::Px; +use cushy::figures::Size; +use cushy::widget::MakeWidget; +use cushy::widgets::container::ContainerShadow; +use cushy::window::{Rgba8, VirtualRecorder, VirtualRecorderBuilder}; + +pub struct BookExample { + name: &'static str, + recorder: VirtualRecorderBuilder, +} + +fn target_dir() -> PathBuf { + let target_dir = std::env::current_dir() + .expect("missing current dir") + .parent() + .expect("missing guide folder") + .join("src") + .join("examples"); + assert!( + target_dir.is_dir(), + "current directory is not guide-examples" + ); + + target_dir +} + +impl BookExample { + pub fn new(name: &'static str, interface: impl MakeWidget) -> Self { + Self { + name, + recorder: interface + .contain() + .shadow(ContainerShadow::drop(Px::new(16), Px::new(32))) + .width(Px::new(750)) + .build_recorder() + .with_alpha() + .resize_to_fit() + .size(Size::new(750, 432)), + } + } + + pub fn still_frame(self, test: Test) + where + Test: FnOnce(&mut VirtualRecorder), + { + let mut recorder = self.recorder.finish().unwrap(); + + let capture = std::env::var("CAPTURE").is_ok(); + let errored = std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut recorder))).is_err(); + if errored || capture { + let path = target_dir().join(format!("{}.png", self.name)); + recorder.image().save(&path).expect("error saving file"); + println!("Wrote {}", path.display()); + + if errored { + std::process::exit(-1); + } + } + } + + // pub fn animated(self, test: Test) + // where + // Test: FnOnce(&mut AnimationRecorder<'_, Rgb8>), + // { + // } +} diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md new file mode 100644 index 0000000..7390c82 --- /dev/null +++ b/guide/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/guide/src/chapter_1.md b/guide/src/chapter_1.md new file mode 100644 index 0000000..c94bb7e --- /dev/null +++ b/guide/src/chapter_1.md @@ -0,0 +1,33 @@ +# Aligning Widgets + +![align.rs - horizontal-align](/examples/align-horizontal.png) + +## Align a widget to the left + +```rust,no_run,no_playground +{{#include ../guide-examples/examples/align.rs:align-left}} +``` + +## Align a widget to the left, with padding + +```rust,no_run,no_playground +{{#include ../guide-examples/examples/align.rs:align-left-pad}} +``` + +## Align a widget to the center + +```rust,no_run,no_playground +{{#include ../guide-examples/examples/align.rs:centered}} +``` + +## Align a widget to the right, with padding + +```rust,no_run,no_playground +{{#include ../guide-examples/examples/align.rs:align-right-pad}} +``` + +## Align a widget to the right + +```rust,no_run,no_playground +{{#include ../guide-examples/examples/align.rs:align-right}} +``` diff --git a/guide/src/examples/align-horizontal.png b/guide/src/examples/align-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..c74c4d4cdbc84d4c508a5b3d66ebd80ecfb2f9e4 GIT binary patch literal 33666 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%0AvmD>>Vi9s=?i9tHJHXzNxr^7E^wuD8)}= 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 S%g(1P_-hYzUY~2d8~p bool { + (self.hue.into_degrees() - other.hue.into_degrees()).abs() < f32::EPSILON + && self.saturation == other.saturation + } +} + impl ColorSource { /// Returns a new source with the given hue (in degrees) and saturation (0.0 /// - 1.0). diff --git a/src/widgets/space.rs b/src/widgets/space.rs index 9bb7283..86dfa77 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -3,6 +3,8 @@ use figures::Size; use kludgine::Color; use crate::context::{GraphicsContext, LayoutContext}; +use crate::styles::components::PrimaryColor; +use crate::styles::{DynamicComponent, IntoDynamicComponentValue}; use crate::value::{IntoValue, Value}; use crate::widget::Widget; use crate::ConstraintLimit; @@ -10,7 +12,7 @@ use crate::ConstraintLimit; /// A widget that occupies space, optionally filling it with a color. #[derive(Debug, Clone)] pub struct Space { - color: Value, + color: Value, } impl Default for Space { @@ -24,7 +26,7 @@ impl Space { #[must_use] pub const fn clear() -> Self { Self { - color: Value::Constant(Color::CLEAR_BLACK), + color: Value::Constant(ColorSource::Color(Color::CLEAR_BLACK)), } } @@ -32,14 +34,38 @@ impl Space { #[must_use] pub fn colored(color: impl IntoValue) -> Self { Self { - color: color.into_value(), + color: color + .into_value() + .map_each(|color| ColorSource::Color(*color)), } } + + /// Returns a spacer that fills itself with `dynamic`'s color. + pub fn dynamic(dynamic: impl IntoDynamicComponentValue) -> Self { + Self { + color: dynamic + .into_dynamic_component() + .map_each(|component| ColorSource::Dynamic(component.clone())), + } + } + + /// Returns a spacer that fills itself with the value of [`PrimaryColor`]. + #[must_use] + pub fn primary() -> Self { + Self::dynamic(PrimaryColor) + } } impl Widget for Space { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { - let color = self.color.get_tracking_redraw(context); + let source = self.color.get_tracking_redraw(context); + let color = match source { + ColorSource::Color(color) => color, + ColorSource::Dynamic(component) => component + .resolve(context) + .and_then(|component| Color::try_from(component).ok()) + .unwrap_or(Color::CLEAR_BLACK), + }; context.fill(color); } @@ -51,3 +77,9 @@ impl Widget for Space { Size::default() } } + +#[derive(Debug, PartialEq, Clone)] +enum ColorSource { + Color(Color), + Dynamic(DynamicComponent), +} diff --git a/src/window.rs b/src/window.rs index ee3afa5..223b078 100644 --- a/src/window.rs +++ b/src/window.rs @@ -20,6 +20,7 @@ use figures::units::{Px, UPx}; use figures::{ Fraction, IntoSigned, IntoUnsigned, Point, Ranged, Rect, Round, ScreenScale, Size, Zero, }; +use image::{DynamicImage, RgbImage, RgbaImage}; use intentional::{Assert, Cast}; use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ @@ -728,6 +729,7 @@ pub trait WindowBehavior: Sized + 'static { } } +#[allow(clippy::struct_excessive_bools)] struct CushyWindow { behavior: T, tree: Tree, @@ -745,6 +747,7 @@ struct CushyWindow { keyboard_activated: Option, min_inner_size: Option>, max_inner_size: Option>, + resize_to_fit: bool, theme: Option>, current_theme: ThemePair, theme_mode: Value, @@ -1146,6 +1149,7 @@ where keyboard_activated: None, min_inner_size: None, max_inner_size: None, + resize_to_fit: false, current_theme, theme, theme_mode, @@ -1172,7 +1176,7 @@ where self.tree .new_frame(self.redraw_status.invalidations().drain()); - let resizable = window.is_resizable(); + let resizable = window.is_resizable() || self.resize_to_fit; let mut window = RunningWindow::new( window, graphics.id(), @@ -1237,6 +1241,8 @@ where new_size = new_size.min(max_size); } layout_context.request_inner_size(new_size); + } else if self.resize_to_fit && window_size != layout_size { + layout_context.request_inner_size(layout_size); } self.root.set_layout(Rect::from(render_size.into_signed())); @@ -1895,7 +1901,9 @@ pub(crate) mod sealed { use std::cell::RefCell; use figures::units::UPx; - use figures::Size; + use figures::{Point, Size}; + use image::DynamicImage; + use kludgine::Color; use crate::app::Cushy; use crate::context::sealed::InvalidationStatus; @@ -1940,6 +1948,8 @@ pub(crate) mod sealed { const HAS_ALPHA: bool; fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32); + fn load_image(data: &[u8], size: Size) -> DynamicImage; + fn pixel_color(location: Point, data: &[u8], size: Size) -> Color; } } @@ -2708,6 +2718,31 @@ impl sealed::CaptureFormat for Rgb8 { retain }); } + + fn load_image(data: &[u8], size: Size) -> DynamicImage { + DynamicImage::ImageRgb8( + RgbImage::from_vec(size.width.get(), size.height.get(), data.to_vec()) + .expect("incorrect dimensions"), + ) + } + + fn pixel_color(location: Point, data: &[u8], size: Size) -> Color { + let pixel_offset = pixel_offset(data, location, size, 3); + Color::new(pixel_offset[0], pixel_offset[1], pixel_offset[2], 255) + } +} + +fn pixel_offset( + data: &[u8], + location: Point, + size: Size, + bytes_per_component: u32, +) -> &[u8] { + assert!(location.x < size.width && location.y < size.height); + + let width = size.width.get(); + let index = location.y.get() * width + location.x.get(); + &data[usize::try_from(index * bytes_per_component).expect("offset out of bounds")..] } impl CaptureFormat for Rgba8 {} @@ -2727,6 +2762,23 @@ impl sealed::CaptureFormat for Rgba8 { }); } } + + fn load_image(data: &[u8], size: Size) -> DynamicImage { + DynamicImage::ImageRgba8( + RgbaImage::from_vec(size.width.get(), size.height.get(), data.to_vec()) + .expect("incorrect dimensions"), + ) + } + + fn pixel_color(location: Point, data: &[u8], size: Size) -> Color { + let pixel_offset = pixel_offset(data, location, size, 4); + Color::new( + pixel_offset[0], + pixel_offset[1], + pixel_offset[2], + pixel_offset[3], + ) + } } /// A builder of a [`VirtualRecorder`]. @@ -2735,6 +2787,7 @@ pub struct VirtualRecorderBuilder { size: Size, scale: f32, format: PhantomData, + resize_to_fit: bool, } impl VirtualRecorderBuilder { @@ -2745,6 +2798,7 @@ impl VirtualRecorderBuilder { size: Size::new(UPx::new(800), UPx::new(600)), scale: 1.0, format: PhantomData, + resize_to_fit: false, } } @@ -2756,6 +2810,7 @@ impl VirtualRecorderBuilder { contents: self.contents, size: self.size, scale: self.scale, + resize_to_fit: self.resize_to_fit, format: PhantomData, } } @@ -2787,9 +2842,17 @@ where self } + /// Sets this virtual recorder to allow updating its size based on the + /// contents being rendered. + #[must_use] + pub fn resize_to_fit(mut self) -> Self { + self.resize_to_fit = true; + self + } + /// Returns an initialized [`VirtualRecorder`]. pub fn finish(self) -> Result, VirtualRecorderError> { - VirtualRecorder::new(self.size, self.scale, self.contents) + VirtualRecorder::new(self.size, self.scale, self.resize_to_fit, self.contents) } } @@ -2864,7 +2927,9 @@ pub struct VirtualRecorder { queue: Arc, capture: Option>, data: Vec, + data_size: Size, cursor: Dynamic>, + cursor_visible: bool, cursor_graphic: Drawing, format: PhantomData, } @@ -2881,6 +2946,7 @@ where pub fn new( size: Size, scale: f32, + resize_to_fit: bool, contents: impl MakeWidget, ) -> Result { let wgpu = wgpu::Instance::default(); @@ -2912,11 +2978,18 @@ where queue: Arc::new(queue), cursor: Dynamic::default(), cursor_graphic: Drawing::default(), + cursor_visible: false, capture: None, data: Vec::new(), + data_size: Size::ZERO, format: PhantomData, }; + recorder.window.window.resize_to_fit = resize_to_fit; recorder.refresh()?; + + if resize_to_fit && recorder.window.state.size != recorder.window.size() { + recorder.refresh()?; + } Ok(recorder) } @@ -2927,6 +3000,52 @@ where &self.data } + /// Returns the color of the pixel at `location`. + /// + /// # Panics + /// + /// This function will panic if location is outside of the bounds of the + /// captured image. When the window's size has been changed, this function + /// operates on the size of the window when the last call to + /// [`Self::refresh()`] was made. + pub fn pixel_color(&self, location: Point) -> Color + where + Unit: Into, + { + Format::pixel_color(location.map(Into::into), self.bytes(), self.data_size) + } + + /// Asserts that the color of the pixel at `location` is `expected`. + /// + /// This function allows for slight color variations. This is because of how + /// colorspace corrections can lead to rounding errors. + /// + /// # Panics + /// + /// This function panics if the color is not the expected color. + pub fn assert_pixel_color(&self, location: Point, expected: Color, component: &str) + where + Unit: Into, + { + let location = location.map(Into::into); + let color = self.pixel_color(location); + let max_delta = color + .red() + .abs_diff(expected.red()) + .max(color.green().abs_diff(expected.green())) + .max(color.blue().abs_diff(expected.blue())) + .max(color.alpha().abs_diff(expected.alpha())); + assert!( + max_delta <= 1, + "assertion failed: {component} at {location:?} was {color:?}, not {expected:?}" + ); + } + + /// Returns the current contents as an image. + pub fn image(&self) -> DynamicImage { + Format::load_image(self.bytes(), self.data_size) + } + fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64, bytes_per_row: u32) { if self .capture @@ -2967,20 +3086,28 @@ where } fn redraw(&mut self) { - let render_size = self.window.kludgine.size().ceil(); + let mut render_size = self.window.kludgine.size().ceil(); + if self.window.state.size != render_size { + let current_scale = self.window.scale(); + self.window + .resize(self.window.state.size, current_scale, &self.queue); + render_size = self.window.state.size; + } let bytes_per_row = copy_buffer_aligned_bytes_per_row(render_size.width.get() * 4); let size = u64::from(bytes_per_row) * u64::from(render_size.height.get()); self.recreate_buffers_if_needed(render_size, size, bytes_per_row); let capture = self.capture.as_ref().assert("always initialized above"); - let mut gfx = self.window.graphics(&self.device, &self.queue); - let mut frame = self.cursor_graphic.new_frame(&mut gfx); - frame.draw_shape( - Shape::filled_circle(Px::new(4), Color::WHITE, Origin::Center) - .translate_by(self.cursor.get()), - ); - drop(frame); + if self.cursor_visible { + let mut gfx = self.window.graphics(&self.device, &self.queue); + let mut frame = self.cursor_graphic.new_frame(&mut gfx); + frame.draw_shape( + Shape::filled_circle(Px::new(4), Color::WHITE, Origin::Center) + .translate_by(self.cursor.get()), + ); + drop(frame); + } self.window.prepare(&self.device, &self.queue); @@ -3001,7 +3128,7 @@ where }, &self.device, &self.queue, - Some(&self.cursor_graphic), + self.cursor_visible.then_some(&self.cursor_graphic), ); } @@ -3012,6 +3139,7 @@ where let capture = self.capture.as_ref().assert("always initialized above"); capture.map_into::(&mut self.data, &self.device, &self.queue)?; + self.data_size = capture.texture.size(); Ok(()) } @@ -3021,11 +3149,28 @@ where self.cursor.set(position); } + /// Enables or disables drawing of the virtual cursor. + pub fn set_cursor_visible(&mut self, visible: bool) { + self.cursor_visible = visible; + } + /// Begins recording an animated png. pub fn record_animated_png(&mut self, target_fps: u8) -> AnimationRecorder<'_, Format> { AnimationRecorder { target_fps, - assembler: FrameAssembler::spawn::(self.device.clone(), self.queue.clone()), + assembler: Some(FrameAssembler::spawn::( + self.device.clone(), + self.queue.clone(), + )), + recorder: self, + } + } + + /// Returns a recorder that does not store any rendered frames. + pub fn simulate_animation(&mut self) -> AnimationRecorder<'_, Format> { + AnimationRecorder { + target_fps: 0, + assembler: None, recorder: self, } } @@ -3040,7 +3185,7 @@ fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { pub struct AnimationRecorder<'a, Format> { recorder: &'a mut VirtualRecorder, target_fps: u8, - assembler: FrameAssembler, + assembler: Option, } impl AnimationRecorder<'_, Format> @@ -3070,6 +3215,10 @@ where /// Waits until `time`, rendering frames as needed. pub fn wait_until(&mut self, time: Instant) -> Result<(), VirtualRecorderError> { + let Some(assembler) = self.assembler.as_ref() else { + return Ok(()); + }; + let frame_duration = Duration::from_micros(1_000_000 / u64::from(self.target_fps)); let mut last_frame = Instant::now(); @@ -3090,19 +3239,19 @@ where if final_frame || next_frame == now { // Try to reuse an existing capture instead of forcing an // allocation. - if let Ok(capture) = self.assembler.resuable_captures.try_recv() { + if let Ok(capture) = assembler.resuable_captures.try_recv() { self.recorder.capture = Some(capture); } let elapsed = now.saturating_duration_since(last_frame); last_frame = now; self.recorder.redraw(); let capture = self.recorder.capture.take().assert("always present"); - if self.assembler.sender.send((capture, elapsed)).is_err() { + if assembler.sender.send((capture, elapsed)).is_err() { break; } } - if now > time { + if final_frame { break; } @@ -3114,8 +3263,13 @@ where } /// Encodes the currently recorded frames into a new file at `path`. + /// + /// If this animation was created from + /// [`VirtualRecorder::simulate_animation`], this function will do nothing. pub fn write_to(self, path: impl AsRef) -> Result<(), VirtualRecorderError> { - let frames = self.assembler.finish()?; + let Some(frames) = self.assembler.map(FrameAssembler::finish).transpose()? else { + return Ok(()); + }; let mut file = std::fs::OpenOptions::new() .create(true) .truncate(true) @@ -3131,28 +3285,20 @@ where encoder.set_animated(u32::try_from(frames.len()).assert("too many frames"), 0)?; encoder.set_compression(png::Compression::Best); - let mut current_frame_delay = frames.first().assert("always at least one frame").duration; - encoder.set_frame_delay( - current_frame_delay - .as_millis() - // TODO should be checked - .cast(), - 1_000, - )?; + let mut current_frame_delay = Duration::ZERO; let mut writer = encoder.write_header()?; for frame in &frames { + writer.write_image_data(&frame.data)?; if current_frame_delay != frame.duration { current_frame_delay = frame.duration; + // This has a limitation that a single frame can't be longer + // than ~6.5 seconds, but it ensures frame timing is more + // accurate. writer.set_frame_delay( - current_frame_delay - .as_millis() - // TODO should be checked - .cast(), - 1_000, + u16::try_from(current_frame_delay.as_nanos() / 100_000).unwrap_or(u16::MAX), + 10_000, )?; } - - writer.write_image_data(&frame.data)?; } writer.finish()?; From 6ff766846f233a34fc8df7de91dccbf3965c1026 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 4 Jan 2024 15:54:58 -0800 Subject: [PATCH 09/10] 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 From 5a7878839660f2f03985f8819d2f84172de12662 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 4 Jan 2024 16:09:58 -0800 Subject: [PATCH 10/10] Enabling animated transparent capture --- src/window.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 8756d44..b93a7dd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -3425,7 +3425,11 @@ where self.recorder.window.size().width.get(), self.recorder.window.size().height.get(), ); - encoder.set_color(png::ColorType::Rgb); + encoder.set_color(if Format::HAS_ALPHA { + png::ColorType::Rgba + } else { + png::ColorType::Rgb + }); encoder.set_adaptive_filter(png::AdaptiveFilterType::Adaptive); encoder.set_animated(u32::try_from(frames.len()).assert("too many frames"), 0)?; encoder.set_compression(png::Compression::Best);