diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d7ed051..f3d77bc 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"] @@ -16,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: @@ -30,6 +36,13 @@ 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 + # 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/.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/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/Cargo.lock b/Cargo.lock index a9326a4..5e83ec8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,6 +582,7 @@ dependencies = [ "kempt", "kludgine", "palette", + "png", "pollster", "rand", "tracing", @@ -987,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" @@ -1179,7 +1187,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#8017775228d22b5efce6d6b7a89e81dfc9b25961" dependencies = [ "ahash", "alot", @@ -1192,6 +1200,7 @@ dependencies = [ "intentional", "justjson", "lyon_tessellation", + "palette", "pollster", "smallvec", "unicode-bidi", @@ -1827,9 +1836,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 +1867,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" dependencies = [ "unicode-ident", ] @@ -1891,9 +1900,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 +2182,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 +2340,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2360,18 +2369,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 +3194,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", @@ -3233,9 +3242,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 c8f633c..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" @@ -39,6 +39,9 @@ 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" +png = "0.17.10" +image = { version = "0.24.7", features = ["png"] } # [patch.crates-io] @@ -57,9 +60,7 @@ 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"] } [profile.release] # debug = true diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 1e68df8..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}; @@ -90,14 +91,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 +109,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 } @@ -116,9 +117,9 @@ 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<'_, '_>, + _context: &mut cushy::context::EventContext<'_>, ) -> cushy::widget::EventHandling { self.value.toggle(); diff --git a/examples/offscreen-apng.rs b/examples/offscreen-apng.rs new file mode 100644 index 0000000..e1e699b --- /dev/null +++ b/examples/offscreen-apng.rs @@ -0,0 +1,36 @@ +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() { + 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.set_cursor_visible(true); + 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-apng.png").unwrap(); +} diff --git a/examples/offscreen.rs b/examples/offscreen.rs new file mode 100644 index 0000000..7ac5d96 --- /dev/null +++ b/examples/offscreen.rs @@ -0,0 +1,30 @@ +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(); + recorder.image().save("examples/offscreen.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(); + recorder.image().save("examples/offscreen.png").unwrap(); +} + +#[test] +fn runs() { + main(); +} 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/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 0000000..c74c4d4 Binary files /dev/null and b/guide/src/examples/align-horizontal.png differ 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/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..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,16 +19,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, DeviceId, 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 +36,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 +53,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 +172,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 +185,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 +476,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 +519,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 +542,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 +571,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 +669,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 +677,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 +724,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 +770,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 +819,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 +835,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 +846,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 +881,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 +898,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 +1141,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 +1183,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 +1191,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 +1250,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 +1264,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 +1272,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 +1328,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 +1338,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..a8896d8 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)] @@ -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(); @@ -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..ccd8b63 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. @@ -1751,7 +1742,7 @@ impl FixedTheme { /// /// The goal of this type is to allow various tones of a given hue/saturation to /// be generated easily. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug)] pub struct ColorSource { /// A measurement of hue, in degees, from -180 to 180. /// @@ -1769,6 +1760,13 @@ pub struct ColorSource { pub saturation: ZeroToOne, } +impl PartialEq for ColorSource { + fn eq(&self, other: &Self) -> 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). @@ -2535,19 +2533,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 +2554,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 +2574,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 +2585,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..acece66 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, @@ -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) @@ -2508,7 +2568,7 @@ impl Value { /// updated. pub fn map_tracking_redraw( &self, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, map: impl FnOnce(&T) -> R, ) -> R { match self { @@ -2526,7 +2586,7 @@ impl Value { /// updated. pub fn map_tracking_invalidate( &self, - context: &WidgetContext<'_, '_>, + context: &WidgetContext<'_>, map: impl FnOnce(&T) -> R, ) -> R { match self { @@ -2573,7 +2633,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 +2644,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, { @@ -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)); diff --git a/src/widget.rs b/src/widget.rs index 927d9e5..25857bf 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; @@ -45,7 +43,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::{ + CushyWindowBuilder, DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, Window, + WindowBehavior, WindowHandle, WindowLocal, +}; use crate::ConstraintLimit; /// A type that makes up a graphical user interface. @@ -264,7 +266,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 +299,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 +347,7 @@ pub trait Widget: Send + Debug + 'static { fn advance_focus( &mut self, direction: VisualOrder, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -357,21 +355,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 +382,7 @@ pub trait Widget: Send + Debug + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -397,7 +395,7 @@ pub trait Widget: Send + Debug + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -408,7 +406,7 @@ pub trait Widget: Send + Debug + 'static { location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -420,7 +418,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 +426,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 +438,7 @@ pub trait Widget: Send + Debug + 'static { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -451,7 +449,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 +542,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 +550,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 +564,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 +582,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 +594,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 +610,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 +618,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 +657,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 +696,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -715,7 +709,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Point, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -726,7 +720,7 @@ pub trait WrapperWidget: Debug + Send + 'static { location: Option>, device_id: DeviceId, button: MouseButton, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) { } @@ -738,7 +732,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 +740,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 +752,7 @@ pub trait WrapperWidget: Debug + Send + 'static { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { IGNORED } @@ -770,13 +764,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 +787,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 +795,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 +840,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 +850,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 +860,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 +870,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 +884,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 +892,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 +916,16 @@ pub trait MakeWidget: Sized { Window::new(self.make_widget()) } + /// Returns a builder for a [`VirtualWindow`](crate::window::VirtualWindow). + fn build_virtual_window(self) -> CushyWindowBuilder { + CushyWindowBuilder::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 +1589,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 +2184,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 +2283,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 +2291,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 +2313,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 +2344,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 +2367,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..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. @@ -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..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)] @@ -69,7 +70,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 +149,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 +158,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 +169,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 +181,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..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. @@ -121,8 +120,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 +142,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 +155,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 +167,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 +178,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 +189,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 +200,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 +211,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 +222,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 +234,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 +247,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 +263,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 +281,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 +297,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 +313,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 +327,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 +346,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 +365,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 +378,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 +394,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 +408,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 +426,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 +443,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 +458,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 +471,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 +488,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 +508,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 +516,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 +535,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 +564,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 +578,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 +590,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 +602,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 +611,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 +624,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 +636,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 +645,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 +655,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 +673,7 @@ trait AdjustChildConstraintsFunc: Send { fn invoke( &mut self, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size; } @@ -711,15 +681,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 +700,7 @@ trait PositionChildFunc: Send { &mut self, size: Size, available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout; } @@ -738,45 +708,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 +756,7 @@ trait ThreeParamEventFunc: Send { location: P1, device_id: P2, button: P3, - context: &mut EventContext<'_, '_>, + context: &mut EventContext<'_>, ) -> R; } @@ -795,16 +764,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..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. @@ -122,7 +123,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 +162,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 +218,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 +271,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 +297,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 +307,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(); @@ -320,9 +317,9 @@ 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<'_, '_>, + context: &mut EventContext<'_>, ) -> EventHandling { if self.hit_test(location, context) { self.mouse_buttons_pressed += 1; @@ -337,9 +334,9 @@ 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<'_, '_>, + context: &mut EventContext<'_>, ) { self.mouse_buttons_pressed -= 1; if self.mouse_buttons_pressed == 0 { @@ -348,7 +345,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..515bdc0 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,20 +962,20 @@ 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 } 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<'_, '_>, + 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) } @@ -1008,9 +997,9 @@ 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<'_, '_>, + context: &mut EventContext<'_>, ) { let cursor_location = self.cursor_from_point(location, context); if self.selection.cursor != cursor_location { @@ -1023,15 +1012,15 @@ 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<'_, '_>, + _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) @@ -1199,10 +1188,10 @@ 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<'_, '_>, + 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..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. @@ -97,7 +98,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 +131,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 +145,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 +199,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 +303,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 +331,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 +381,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 +398,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..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. @@ -144,7 +145,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 +239,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 +258,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 +418,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 +501,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 +553,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 +575,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 +596,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 +620,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 +630,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 +653,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 +665,7 @@ where _location: Option>, _device_id: DeviceId, _button: MouseButton, - _context: &mut EventContext<'_, '_>, + _context: &mut EventContext<'_>, ) { self.mouse_buttons_down -= 1; } @@ -674,7 +675,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 +700,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..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,22 +34,52 @@ 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); + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { + 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); } fn layout( &mut self, _available_space: Size, - _context: &mut LayoutContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { Size::default() } } + +#[derive(Debug, PartialEq, Clone)] +enum ColorSource { + Color(Color), + Dynamic(DynamicComponent), +} 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..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. @@ -64,7 +65,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 +104,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 +119,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 +129,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 +142,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 +160,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 +171,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 +185,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 +201,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..b93a7dd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,30 +4,41 @@ 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, Mutex, MutexGuard, OnceLock}; +use std::sync::{mpsc, Arc, 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::{ + 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::{ - DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, + 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::shapes::Shape; +use kludgine::wgpu::{self, CompositeAlphaMode, COPY_BYTES_PER_ROW_ALIGNMENT}; +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::{ @@ -48,9 +59,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 +244,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 +301,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 +323,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,8 +652,7 @@ where App: Application + ?Sized, { let cushy = app.cushy().clone(); - - let handle = CushyWindow::::open_with( + let handle = OpenWindow::::open_with( app, sealed::Context { user: self.context, @@ -389,9 +663,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 +693,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 +705,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 W) -> bool + where + W: PlatformWindow, + { true } @@ -446,7 +726,8 @@ pub trait WindowBehavior: Sized + 'static { } } -struct CushyWindow { +#[allow(clippy::struct_excessive_bools)] +struct OpenWindow { behavior: T, tree: Tree, root: MountedWidget, @@ -463,6 +744,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, @@ -472,27 +754,29 @@ struct CushyWindow { on_closed: Option, } -impl CushyWindow +impl OpenWindow where T: WindowBehavior, { fn request_close( should_close: &mut bool, behavior: &mut T, - window: &mut RunningWindow<'_>, + window: &mut RunningWindow>, ) -> bool { *should_close |= behavior.close_requested(window); *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 @@ -544,12 +828,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(); @@ -689,19 +976,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); @@ -729,6 +1019,7 @@ where target.deactivate(); } } + HANDLED } Key::Named(NamedKey::Tab) if !window.modifiers().possible_shortcut() => { @@ -754,6 +1045,7 @@ where target.advance_focus(); } } + HANDLED } Key::Named(NamedKey::Enter) => { self.keyboard_activate_widget( @@ -762,6 +1054,7 @@ where window, kludgine, ); + HANDLED } Key::Named(NamedKey::Escape) => { self.keyboard_activate_widget( @@ -770,6 +1063,7 @@ where window, kludgine, ); + HANDLED } _ => { tracing::event!( @@ -779,43 +1073,35 @@ where state = ?input.state, "Ignored Keyboard Input", ); + IGNORED } } } -} -#[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 +1117,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); @@ -872,6 +1146,7 @@ where keyboard_activated: None, min_inner_size: None, max_inner_size: None, + resize_to_fit: false, current_theme, theme, theme_mode, @@ -882,11 +1157,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 +1173,7 @@ where self.tree .new_frame(self.redraw_status.invalidations().drain()); - let resizable = window.winit().is_resizable(); + let resizable = window.is_resizable() || self.resize_to_fit; let mut window = RunningWindow::new( window, graphics.id(), @@ -953,9 +1227,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 +1237,9 @@ 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); + } 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())); @@ -990,6 +1262,404 @@ 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 + } + } + + 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)] +enum RootMode { + Fit, + Expand, + Align, +} + +impl kludgine::app::WindowBehavior for OpenWindow +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>, @@ -1081,10 +1751,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<'_, ()>) {} @@ -1101,81 +1768,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<'_, ()>) {} @@ -1186,219 +1794,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( @@ -1442,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(()); @@ -1451,8 +1877,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()), @@ -1472,7 +1898,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; @@ -1491,9 +1919,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 +1940,14 @@ 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); + fn load_image(data: &[u8], size: Size) -> DynamicImage; + fn pixel_color(location: Point, data: &[u8], size: Size) -> Color; + } } /// Controls whether the light or dark theme is applied. @@ -1678,6 +2114,7 @@ impl Hash for WindowHandle { enum InnerWindowHandle { Pending(Arc), Known(kludgine::app::WindowHandle), + Virtual(WindowDynamicState), } impl InnerWindowHandle { @@ -1697,6 +2134,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 +2221,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 +2234,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 +2251,1346 @@ 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 CushyWindowBuilder { + widget: WidgetInstance, + multisample_count: u32, + initial_size: Size, + scale: f32, + transparent: bool, +} + +impl CushyWindowBuilder { + /// Returns a new builder for a Cushy 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 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 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 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 window. + #[must_use] + 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: self.multisample_count, + ..Default::default() + }, + self.initial_size, + self.scale, + ); + let window = OpenWindow::::new( + self.widget, + window, + &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: self.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, + }, + ); + + 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, window: W, device: &wgpu::Device, queue: &wgpu::Queue) + where + W: PlatformWindowImplementation, + { + self.window.prepare( + window, + &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 { + 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 { + 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) + } + + /// 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, 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.cushy.request_close(&mut self.state) { + 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 + } + + /// 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.cushy.size() + } + + /// Returns the current DPI scale of the window. + pub const fn scale(&self) -> Fraction { + 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.cushy.resize(new_size, new_scale.into(), queue); + } + + /// 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.cushy + .keyboard_input(&mut self.state, 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.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.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.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.cushy.cursor_left(&mut self.state); + } + + /// 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.cushy + .mouse_input(&mut self.state, device_id, state, button) + } +} + +/// 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 + }); + } + + 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 {} + +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 + }); + } + } + + 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`]. +pub struct VirtualRecorderBuilder { + contents: WidgetInstance, + size: Size, + scale: f32, + format: PhantomData, + resize_to_fit: bool, +} + +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, + resize_to_fit: false, + } + } + + /// 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, + resize_to_fit: self.resize_to_fit, + 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 + } + + /// 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 + } + + /// 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.resize_to_fit, self.contents) + } +} + +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: Arc, + queue: Arc, + capture: Option>, + data: Vec, + data_size: Size, + cursor: Dynamic>, + cursor_visible: bool, + cursor_graphic: Drawing, + 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, + resize_to_fit: bool, + 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 window = contents + .build_virtual_window() + .size(size) + .scale(scale) + .transparent() + .finish_virtual(&device, &queue); + + let mut recorder = Self { + window, + device: Arc::new(device), + 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.cushy.window.resize_to_fit = resize_to_fit; + recorder.refresh()?; + + if resize_to_fit && recorder.window.state.size != recorder.window.size() { + 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 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 + .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(Box::new(Capture { + bytes, + bytes_per_row, + buffer, + texture, + multisample, + })); + } + } + + fn redraw(&mut self) { + let mut render_size = self.window.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"); + + 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); + + self.window.render_with( + &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, + self.cursor_visible.then_some(&self.cursor_graphic), + ); + } + + /// Redraws the contents. + pub fn refresh(&mut self) -> Result<(), wgpu::BufferAsyncError> { + self.redraw(); + + 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(()) + } + + /// Sets the cursor position immediately. + pub fn set_cursor_position(&self, position: Point) { + 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: 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, + } + } +} + +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 animated PNG recorder. +pub struct AnimationRecorder<'a, Format> { + recorder: &'a mut VirtualRecorder, + target_fps: u8, + assembler: Option, +} + +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 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(); + + 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 { + // Try to reuse an existing capture instead of forcing an + // allocation. + 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 assembler.sender.send((capture, elapsed)).is_err() { + break; + } + } + + if final_frame { + 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`. + /// + /// 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 Some(frames) = self.assembler.map(FrameAssembler::finish).transpose()? else { + return Ok(()); + }; + 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(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); + + 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( + u16::try_from(current_frame_delay.as_nanos() / 100_000).unwrap_or(u16::MAX), + 10_000, + )?; + } + } + + writer.finish()?; + + file.sync_all()?; + + Ok(()) + } +} + +struct Frame { + data: Vec, + duration: Duration, +} + +/// 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), + /// 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 { + 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 + } +} + +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 { + /// 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) + } +} + +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)); + } +}