mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +00:00
Guide intro + KeyEvent
Also fixed virtual window's refresh handling.
This commit is contained in:
parent
31de0bc458
commit
cda13c42a2
15 changed files with 317 additions and 44 deletions
|
|
@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
`Label::text` is now named `display` and `Label::new()` now accepts an
|
||||
`IntoReadOnly<T>` instead of `IntoValue<String>`.
|
||||
- `Dynamic<Children>::wrap` has been renamed to `into_wrap` for consistency.
|
||||
- Cushy now has its own `KeyEvent` type, as winit's has private fields. This
|
||||
prevented simulating input in a `VirtualWindow`.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
16
guide/guide-examples/examples/hello-world.rs
Normal file
16
guide/guide-examples/examples/hello-world.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// ANCHOR: example
|
||||
use cushy::Run;
|
||||
|
||||
fn main() -> cushy::Result {
|
||||
"Hello, World!".run()
|
||||
}
|
||||
// ANCHOR_END: example
|
||||
|
||||
#[test]
|
||||
fn book() {
|
||||
fn hello_world() -> impl MakeWidget {
|
||||
"Hello, World!"
|
||||
}
|
||||
|
||||
guide_examples::book_example!(hello_world).untested_still_frame();
|
||||
}
|
||||
49
guide/guide-examples/examples/intro.rs
Normal file
49
guide/guide-examples/examples/intro.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// ANCHOR: example
|
||||
use cushy::value::{Dynamic, Source};
|
||||
use cushy::widget::MakeWidget;
|
||||
use cushy::widgets::input::{Input, InputValue};
|
||||
use cushy::Run;
|
||||
|
||||
fn main() -> cushy::Result {
|
||||
// Create storage for user to enter a name.
|
||||
let name: Dynamic<String> = Dynamic::default();
|
||||
|
||||
// Create our label by using `map_each` to format the name, first checking
|
||||
// if it is empty.
|
||||
let greeting: Dynamic<String> = name.map_each(|name| {
|
||||
let name = if name.is_empty() { "World" } else { name };
|
||||
format!("Hello, {name}!")
|
||||
});
|
||||
|
||||
// Create the input widget with a placeholder.
|
||||
let name_input: Input = name.into_input().placeholder("Name");
|
||||
|
||||
// Stack our widgets as rows, and run the app.
|
||||
name_input.and(greeting).into_rows().run()
|
||||
}
|
||||
// ANCHOR_END: example
|
||||
|
||||
#[test]
|
||||
fn book() {
|
||||
use std::time::Duration;
|
||||
|
||||
fn intro() -> impl MakeWidget {
|
||||
let subject: Dynamic<String> = Dynamic::default();
|
||||
let greeting: Dynamic<String> = subject.map_each(|subject| {
|
||||
let subject = if subject.is_empty() { "World" } else { subject };
|
||||
format!("Hello, {subject}!")
|
||||
});
|
||||
|
||||
let name_input: Input = subject.into_input().placeholder("Name");
|
||||
|
||||
name_input.and(greeting).into_rows()
|
||||
}
|
||||
|
||||
guide_examples::book_example!(intro).animated(|animation| {
|
||||
animation.wait_for(Duration::from_millis(1_000)).unwrap();
|
||||
animation
|
||||
.animate_text_input("Ferris 🦀", Duration::from_secs(1))
|
||||
.unwrap();
|
||||
animation.wait_for(Duration::from_millis(1_000)).unwrap();
|
||||
});
|
||||
}
|
||||
|
|
@ -5,13 +5,49 @@ use cushy::figures::units::Px;
|
|||
use cushy::figures::Size;
|
||||
use cushy::widget::MakeWidget;
|
||||
use cushy::widgets::container::ContainerShadow;
|
||||
use cushy::window::{Rgba8, VirtualRecorder, VirtualRecorderBuilder};
|
||||
use cushy::window::{AnimationRecorder, Rgba8, VirtualRecorder, VirtualRecorderBuilder};
|
||||
|
||||
pub struct BookExample {
|
||||
pub struct BookExampleBuilder {
|
||||
name: &'static str,
|
||||
recorder: VirtualRecorderBuilder<Rgba8>,
|
||||
}
|
||||
|
||||
impl BookExampleBuilder {
|
||||
pub fn finish(self) -> BookExample {
|
||||
let mut recorder = self.recorder.finish().expect("error creating recorder");
|
||||
recorder.window.set_focused(true);
|
||||
BookExample {
|
||||
name: self.name,
|
||||
recorder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn untested_still_frame(self) {
|
||||
self.finish().untested_still_frame()
|
||||
}
|
||||
|
||||
pub fn prepare_with<Prepare>(self, prepare: Prepare) -> BookExample
|
||||
where
|
||||
Prepare: FnOnce(&mut VirtualRecorder<Rgba8>),
|
||||
{
|
||||
self.finish().prepare_with(prepare)
|
||||
}
|
||||
|
||||
pub fn still_frame<Test>(self, test: Test)
|
||||
where
|
||||
Test: FnOnce(&mut VirtualRecorder<Rgba8>),
|
||||
{
|
||||
self.finish().still_frame(test);
|
||||
}
|
||||
|
||||
pub fn animated<Test>(self, test: Test)
|
||||
where
|
||||
Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>),
|
||||
{
|
||||
self.finish().animated(test);
|
||||
}
|
||||
}
|
||||
|
||||
fn target_dir() -> PathBuf {
|
||||
let target_dir = std::env::current_dir()
|
||||
.expect("missing current dir")
|
||||
|
|
@ -27,9 +63,14 @@ fn target_dir() -> PathBuf {
|
|||
target_dir
|
||||
}
|
||||
|
||||
pub struct BookExample {
|
||||
name: &'static str,
|
||||
recorder: VirtualRecorder<Rgba8>,
|
||||
}
|
||||
|
||||
impl BookExample {
|
||||
pub fn new(name: &'static str, interface: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
pub fn build(name: &'static str, interface: impl MakeWidget) -> BookExampleBuilder {
|
||||
BookExampleBuilder {
|
||||
name,
|
||||
recorder: interface
|
||||
.contain()
|
||||
|
|
@ -42,17 +83,31 @@ impl BookExample {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn still_frame<Test>(self, test: Test)
|
||||
pub fn untested_still_frame(self) {
|
||||
self.still_frame(|_| {});
|
||||
}
|
||||
|
||||
pub fn prepare_with<Prepare>(mut self, prepare: Prepare) -> Self
|
||||
where
|
||||
Prepare: FnOnce(&mut VirtualRecorder<Rgba8>),
|
||||
{
|
||||
prepare(&mut self.recorder);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn still_frame<Test>(mut self, test: Test)
|
||||
where
|
||||
Test: FnOnce(&mut VirtualRecorder<Rgba8>),
|
||||
{
|
||||
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();
|
||||
let errored =
|
||||
std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut self.recorder))).is_err();
|
||||
if errored || capture {
|
||||
let path = target_dir().join(format!("{}.png", self.name));
|
||||
recorder.image().save(&path).expect("error saving file");
|
||||
self.recorder
|
||||
.image()
|
||||
.save(&path)
|
||||
.expect("error saving file");
|
||||
println!("Wrote {}", path.display());
|
||||
|
||||
if errored {
|
||||
|
|
@ -61,16 +116,28 @@ impl BookExample {
|
|||
}
|
||||
}
|
||||
|
||||
// pub fn animated<Test>(self, test: Test)
|
||||
// where
|
||||
// Test: FnOnce(&mut AnimationRecorder<'_, Rgb8>),
|
||||
// {
|
||||
// }
|
||||
pub fn animated<Test>(mut self, test: Test)
|
||||
where
|
||||
Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>),
|
||||
{
|
||||
let mut animation = self.recorder.record_animated_png(60);
|
||||
let capture = std::env::var("CAPTURE").is_ok();
|
||||
let errored = std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut animation))).is_err();
|
||||
if errored || capture {
|
||||
let path = target_dir().join(format!("{}.png", self.name));
|
||||
animation.write_to(&path).expect("error saving file");
|
||||
println!("Wrote {}", path.display());
|
||||
|
||||
if errored {
|
||||
std::process::exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! book_example {
|
||||
($name:ident) => {
|
||||
guide_examples::BookExample::new(stringify!($name), $name())
|
||||
guide_examples::BookExample::build(stringify!($name), $name())
|
||||
};
|
||||
}
|
||||
|
|
|
|||
BIN
guide/src/examples/hello_world.png
Normal file
BIN
guide/src/examples/hello_world.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
guide/src/examples/intro.png
Normal file
BIN
guide/src/examples/intro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -7,6 +7,54 @@ This is a user's guide for [Cushy][cushy], a [Rust][rust] GUI crate. The
|
|||
functionality quickly. This guide is aimed to providing an example-rich
|
||||
walkthrough of how to use and extend Cushy.
|
||||
|
||||
## A "Hello, World" Example
|
||||
|
||||
Here's the simplest "Hello, World" example:
|
||||
|
||||
```rust,no_run,no_playground
|
||||
{{#include ../guide-examples/examples/hello-world.rs:example}}
|
||||
```
|
||||
|
||||
When run, the app just displays the text as one would hope:
|
||||
|
||||

|
||||
|
||||
That was a little too easy. Let's take it a step further by letting a user type
|
||||
in their name and have a label display "Hello, {name}!":
|
||||
|
||||
```rust,no_run,no_playground
|
||||
{{#include ../guide-examples/examples/intro.rs:example}}
|
||||
```
|
||||
|
||||
This app looks like this when executed:
|
||||
|
||||

|
||||
|
||||
In this example, both `name` and `greeting` are [`Dynamic<String>`s][dynamic]. A
|
||||
`Dynamic<T>` is an `Arc<Mutex<T>>`-like type that is able to invoke a set of
|
||||
callbacks when its contents is changed. This simple feature is the core of
|
||||
Cushy's reactive data model.
|
||||
|
||||
Each time `name` is changed, the `map_each` closure will be executed and
|
||||
`greeting` will be updated with the result. Now that we have the individual
|
||||
pieces of data our user interface is going to work with, we can start assembling
|
||||
the interface.
|
||||
|
||||
First, we create `name_input` by converting the `Dynamic<String>` into a text
|
||||
[`Input<String>`][input]. Since `Dynamic<String>` can be used as a
|
||||
[`Label`][label], all that's left is laying out our two widgets.
|
||||
|
||||
To layout `name_input` and `greeting`, we use a [`Stack`][stack] to lay out the
|
||||
widgets as rows.
|
||||
|
||||
Don't worry if this example seems a bit magical or confusing as to how it works.
|
||||
Cushy can feel magical to use. But, it should never be a mystery. The goal of
|
||||
this guide is to try and explain how and why Cushy works the way it does.
|
||||
|
||||
[cushy]: <https://github.com/khonsulabs/cushy>
|
||||
[rust]: <https://rust-lang.org/>
|
||||
[docs]: <{{docs}}>
|
||||
[dynamic]: <{{docs}}/value/struct.Dynamic.html>
|
||||
[input]: <{{docs}}/widgets/input/struct.Input.html>
|
||||
[label]: <{{docs}}/widgets/label/struct.Label.html>
|
||||
[stack]: <{{docs}}/widgets/stack/struct.Stack.html>
|
||||
|
|
|
|||
|
|
@ -4,7 +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::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::window::CursorIcon;
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::{Color, Kludgine, KludgineId};
|
||||
|
|
@ -19,7 +19,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
|
|||
use crate::tree::Tree;
|
||||
use crate::value::{IntoValue, Source, Value};
|
||||
use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance};
|
||||
use crate::window::{CursorState, DeviceId, PlatformWindow, ThemeMode};
|
||||
use crate::window::{CursorState, DeviceId, KeyEvent, PlatformWindow, ThemeMode};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A context to an event function.
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ use ahash::AHashSet;
|
|||
use figures::units::Px;
|
||||
use figures::Point;
|
||||
use intentional::Assert;
|
||||
use kludgine::app::winit::event::{ElementState, KeyEvent, MouseButton};
|
||||
use kludgine::app::winit::event::{ElementState, MouseButton};
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
|
||||
use crate::context::WidgetContext;
|
||||
use crate::utils::IgnorePoison;
|
||||
use crate::value::{Destination, Dynamic};
|
||||
use crate::widget::{EventHandling, HANDLED, IGNORED};
|
||||
use crate::window::KeyEvent;
|
||||
|
||||
/// A fixed-rate callback that provides access to tracked input on its
|
||||
/// associated widget.
|
||||
|
|
|
|||
|
|
@ -12,7 +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::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::window::CursorIcon;
|
||||
use kludgine::Color;
|
||||
|
||||
|
|
@ -45,8 +45,8 @@ use crate::widgets::{
|
|||
};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::window::{
|
||||
CushyWindowBuilder, DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, Window,
|
||||
WindowBehavior, WindowHandle, WindowLocal,
|
||||
CushyWindowBuilder, DeviceId, KeyEvent, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder,
|
||||
Window, WindowBehavior, WindowHandle, WindowLocal,
|
||||
};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::fmt::Debug;
|
|||
|
||||
use figures::units::Px;
|
||||
use figures::{Point, Size};
|
||||
use kludgine::app::winit::event::{Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::window::CursorIcon;
|
||||
use kludgine::Color;
|
||||
|
||||
|
|
@ -11,7 +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::window::{DeviceId, KeyEvent};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A callback-based custom widget.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use figures::{
|
|||
Abs, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size,
|
||||
};
|
||||
use intentional::Cast;
|
||||
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
|
||||
use kludgine::app::winit::event::{ElementState, Ime};
|
||||
use kludgine::app::winit::keyboard::{Key, NamedKey};
|
||||
use kludgine::app::winit::window::{CursorIcon, ImePurpose};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
|
|
@ -27,13 +27,14 @@ use crate::styles::components::{HighlightColor, IntrinsicPadding, OutlineColor,
|
|||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value};
|
||||
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
||||
use crate::window::KeyEvent;
|
||||
use crate::{ConstraintLimit, Lazy};
|
||||
|
||||
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
||||
|
||||
/// A text input widget.
|
||||
#[must_use]
|
||||
pub struct Input<Storage> {
|
||||
pub struct Input<Storage = String> {
|
||||
/// The value of this widget.
|
||||
pub value: Dynamic<Storage>,
|
||||
/// The placeholder text to display when no value is present.
|
||||
|
|
@ -1149,8 +1150,6 @@ where
|
|||
);
|
||||
}
|
||||
context.redraw_in(cursor_state.remaining_until_blink);
|
||||
} else {
|
||||
context.redraw_when_changed(context.window().focused());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1190,7 +1189,7 @@ where
|
|||
fn keyboard_input(
|
||||
&mut self,
|
||||
_device_id: crate::window::DeviceId,
|
||||
input: kludgine::app::winit::event::KeyEvent,
|
||||
input: KeyEvent,
|
||||
_is_synthetic: bool,
|
||||
context: &mut EventContext<'_>,
|
||||
) -> EventHandling {
|
||||
|
|
|
|||
|
|
@ -21,7 +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::window::{DeviceId, KeyEvent};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that allows sliding between two values.
|
||||
|
|
@ -673,7 +673,7 @@ where
|
|||
fn keyboard_input(
|
||||
&mut self,
|
||||
_device_id: DeviceId,
|
||||
input: kludgine::app::winit::event::KeyEvent,
|
||||
input: KeyEvent,
|
||||
_is_synthetic: bool,
|
||||
_context: &mut EventContext<'_>,
|
||||
) -> EventHandling {
|
||||
|
|
|
|||
|
|
@ -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::{ElementState, KeyEvent, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::event::{ElementState, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::window::CursorIcon;
|
||||
use kludgine::tilemap;
|
||||
use kludgine::tilemap::TileMapFocus;
|
||||
|
|
@ -12,7 +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::window::{DeviceId, KeyEvent};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A layered tile-based 2d game surface.
|
||||
|
|
|
|||
115
src/window.rs
115
src/window.rs
|
|
@ -24,9 +24,11 @@ use image::{DynamicImage, RgbImage, RgbaImage};
|
|||
use intentional::{Assert, Cast};
|
||||
use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use kludgine::app::winit::event::{
|
||||
ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
use kludgine::app::winit::keyboard::{
|
||||
Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey, SmolStr,
|
||||
};
|
||||
use kludgine::app::winit::keyboard::{Key, NamedKey};
|
||||
use kludgine::app::winit::window::{self, CursorIcon};
|
||||
use kludgine::app::{winit, WindowBehavior as _};
|
||||
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
|
||||
|
|
@ -35,6 +37,7 @@ 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 unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::animation::{
|
||||
AnimationTarget, Easing, LinearInterpolate, PercentBetween, Spawn, ZeroToOne,
|
||||
|
|
@ -1298,6 +1301,14 @@ where
|
|||
self.root.invalidate();
|
||||
}
|
||||
|
||||
pub fn set_focused(&mut self, focused: bool) {
|
||||
self.focused.set(focused);
|
||||
}
|
||||
|
||||
pub fn set_occluded(&mut self, occluded: bool) {
|
||||
self.occluded.set(occluded);
|
||||
}
|
||||
|
||||
pub fn keyboard_input<W>(
|
||||
&mut self,
|
||||
window: W,
|
||||
|
|
@ -1674,7 +1685,7 @@ where
|
|||
window: kludgine::app::Window<'_, WindowCommand>,
|
||||
_kludgine: &mut Kludgine,
|
||||
) {
|
||||
self.focused.set(window.focused());
|
||||
self.set_focused(window.focused());
|
||||
}
|
||||
|
||||
fn occlusion_changed(
|
||||
|
|
@ -1682,7 +1693,7 @@ where
|
|||
window: kludgine::app::Window<'_, WindowCommand>,
|
||||
_kludgine: &mut Kludgine,
|
||||
) {
|
||||
self.occluded.set(window.ocluded());
|
||||
self.set_occluded(window.ocluded());
|
||||
}
|
||||
|
||||
fn render<'pass>(
|
||||
|
|
@ -1778,10 +1789,16 @@ where
|
|||
window: kludgine::app::Window<'_, WindowCommand>,
|
||||
kludgine: &mut Kludgine,
|
||||
device_id: winit::event::DeviceId,
|
||||
input: KeyEvent,
|
||||
input: winit::event::KeyEvent,
|
||||
is_synthetic: bool,
|
||||
) {
|
||||
self.keyboard_input(window, kludgine, device_id.into(), input, is_synthetic);
|
||||
self.keyboard_input(
|
||||
window,
|
||||
kludgine,
|
||||
device_id.into(),
|
||||
input.into(),
|
||||
is_synthetic,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_wheel(
|
||||
|
|
@ -2570,6 +2587,14 @@ impl CushyWindow {
|
|||
kludgine::Graphics::new(&mut self.kludgine, device, queue)
|
||||
}
|
||||
|
||||
pub fn set_focused(&mut self, focused: bool) {
|
||||
self.window.set_focused(focused);
|
||||
}
|
||||
|
||||
pub fn set_occluded(&mut self, occluded: bool) {
|
||||
self.window.set_occluded(occluded);
|
||||
}
|
||||
|
||||
/// Requests that the window close.
|
||||
///
|
||||
/// Returns true if the request should be honored.
|
||||
|
|
@ -2699,6 +2724,7 @@ impl VirtualWindow {
|
|||
.map(|i| now.duration_since(i))
|
||||
.unwrap_or_default();
|
||||
self.last_rendered_at = Some(now);
|
||||
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
||||
self.cushy.prepare(&mut self.state, device, queue);
|
||||
}
|
||||
|
||||
|
|
@ -2726,7 +2752,6 @@ impl VirtualWindow {
|
|||
queue: &wgpu::Queue,
|
||||
additional_drawing: Option<&Drawing>,
|
||||
) -> Option<wgpu::SubmissionIndex> {
|
||||
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
||||
self.cushy
|
||||
.render_with(pass, device, queue, additional_drawing)
|
||||
}
|
||||
|
|
@ -2739,7 +2764,6 @@ impl VirtualWindow {
|
|||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
) -> Option<wgpu::SubmissionIndex> {
|
||||
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
||||
self.cushy.render_into(texture, load_op, device, queue)
|
||||
}
|
||||
|
||||
|
|
@ -2766,6 +2790,14 @@ impl VirtualWindow {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&mut self, focused: bool) {
|
||||
self.cushy.set_focused(focused);
|
||||
}
|
||||
|
||||
pub fn set_occluded(&mut self, occluded: bool) {
|
||||
self.cushy.set_occluded(occluded);
|
||||
}
|
||||
|
||||
/// Returns true if this window should no longer be open.
|
||||
#[must_use]
|
||||
pub fn closed(&self) -> bool {
|
||||
|
|
@ -3363,6 +3395,40 @@ where
|
|||
self.wait_for(over)
|
||||
}
|
||||
|
||||
pub fn animate_text_input(
|
||||
&mut self,
|
||||
text: &str,
|
||||
duration: Duration,
|
||||
) -> Result<(), VirtualRecorderError> {
|
||||
let graphemes = text.graphemes(true).count();
|
||||
let delay_per_event =
|
||||
Duration::from_nanos(duration.as_nanos().cast::<u64>() / graphemes.cast::<u64>() / 2);
|
||||
for grapheme in text.graphemes(true) {
|
||||
let grapheme = SmolStr::new(grapheme);
|
||||
let mut event = KeyEvent {
|
||||
physical_key: PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)),
|
||||
logical_key: Key::Character(grapheme.clone()),
|
||||
text: Some(SmolStr::new(grapheme)),
|
||||
location: KeyLocation::Standard,
|
||||
state: ElementState::Pressed,
|
||||
repeat: false,
|
||||
};
|
||||
let _handled =
|
||||
self.recorder
|
||||
.window
|
||||
.keyboard_input(DeviceId::Virtual(0), event.clone(), true);
|
||||
self.wait_for(delay_per_event)?;
|
||||
|
||||
event.state = ElementState::Released;
|
||||
let _handled = self
|
||||
.recorder
|
||||
.window
|
||||
.keyboard_input(DeviceId::Virtual(0), event, true);
|
||||
self.wait_for(delay_per_event)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Waits for `duration`, rendering frames as needed.
|
||||
pub fn wait_for(&mut self, duration: Duration) -> Result<(), VirtualRecorderError> {
|
||||
self.wait_until(Instant::now() + duration)
|
||||
|
|
@ -3391,7 +3457,7 @@ where
|
|||
RedrawTarget::At(instant) => now.min(instant),
|
||||
};
|
||||
|
||||
if final_frame || next_frame == now {
|
||||
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() {
|
||||
|
|
@ -3399,6 +3465,7 @@ where
|
|||
}
|
||||
let elapsed = now.saturating_duration_since(last_frame);
|
||||
last_frame = now;
|
||||
println!("Redrawing");
|
||||
self.recorder.redraw();
|
||||
let capture = self.recorder.capture.take().assert("always present");
|
||||
if assembler.sender.send((capture, elapsed)).is_err() {
|
||||
|
|
@ -3447,8 +3514,7 @@ where
|
|||
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 {
|
||||
if current_frame_delay != frame.duration && frames.len() > 1 {
|
||||
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
|
||||
|
|
@ -3458,9 +3524,10 @@ where
|
|||
10_000,
|
||||
)?;
|
||||
}
|
||||
writer.write_image_data(&frame.data)?;
|
||||
}
|
||||
|
||||
writer.finish()?;
|
||||
writer.finish().unwrap();
|
||||
|
||||
file.sync_all()?;
|
||||
|
||||
|
|
@ -3604,3 +3671,27 @@ impl FrameAssembler {
|
|||
let _result = result.send(Ok(assembled));
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a keyboard input targeting a window.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeyEvent {
|
||||
pub physical_key: PhysicalKey,
|
||||
pub logical_key: Key,
|
||||
pub text: Option<SmolStr>,
|
||||
pub location: KeyLocation,
|
||||
pub state: ElementState,
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
impl From<winit::event::KeyEvent> for KeyEvent {
|
||||
fn from(event: winit::event::KeyEvent) -> Self {
|
||||
Self {
|
||||
physical_key: event.physical_key,
|
||||
logical_key: event.logical_key,
|
||||
text: event.text,
|
||||
location: event.location,
|
||||
state: event.state,
|
||||
repeat: event.repeat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue