mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 19:20:36 +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
|
`Label::text` is now named `display` and `Label::new()` now accepts an
|
||||||
`IntoReadOnly<T>` instead of `IntoValue<String>`.
|
`IntoReadOnly<T>` instead of `IntoValue<String>`.
|
||||||
- `Dynamic<Children>::wrap` has been renamed to `into_wrap` for consistency.
|
- `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
|
### 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::figures::Size;
|
||||||
use cushy::widget::MakeWidget;
|
use cushy::widget::MakeWidget;
|
||||||
use cushy::widgets::container::ContainerShadow;
|
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,
|
name: &'static str,
|
||||||
recorder: VirtualRecorderBuilder<Rgba8>,
|
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 {
|
fn target_dir() -> PathBuf {
|
||||||
let target_dir = std::env::current_dir()
|
let target_dir = std::env::current_dir()
|
||||||
.expect("missing current dir")
|
.expect("missing current dir")
|
||||||
|
|
@ -27,9 +63,14 @@ fn target_dir() -> PathBuf {
|
||||||
target_dir
|
target_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct BookExample {
|
||||||
|
name: &'static str,
|
||||||
|
recorder: VirtualRecorder<Rgba8>,
|
||||||
|
}
|
||||||
|
|
||||||
impl BookExample {
|
impl BookExample {
|
||||||
pub fn new(name: &'static str, interface: impl MakeWidget) -> Self {
|
pub fn build(name: &'static str, interface: impl MakeWidget) -> BookExampleBuilder {
|
||||||
Self {
|
BookExampleBuilder {
|
||||||
name,
|
name,
|
||||||
recorder: interface
|
recorder: interface
|
||||||
.contain()
|
.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
|
where
|
||||||
Test: FnOnce(&mut VirtualRecorder<Rgba8>),
|
Test: FnOnce(&mut VirtualRecorder<Rgba8>),
|
||||||
{
|
{
|
||||||
let mut recorder = self.recorder.finish().unwrap();
|
|
||||||
|
|
||||||
let capture = std::env::var("CAPTURE").is_ok();
|
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 {
|
if errored || capture {
|
||||||
let path = target_dir().join(format!("{}.png", self.name));
|
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());
|
println!("Wrote {}", path.display());
|
||||||
|
|
||||||
if errored {
|
if errored {
|
||||||
|
|
@ -61,16 +116,28 @@ impl BookExample {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn animated<Test>(self, test: Test)
|
pub fn animated<Test>(mut self, test: Test)
|
||||||
// where
|
where
|
||||||
// Test: FnOnce(&mut AnimationRecorder<'_, Rgb8>),
|
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_export]
|
||||||
macro_rules! book_example {
|
macro_rules! book_example {
|
||||||
($name:ident) => {
|
($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
|
functionality quickly. This guide is aimed to providing an example-rich
|
||||||
walkthrough of how to use and extend Cushy.
|
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>
|
[cushy]: <https://github.com/khonsulabs/cushy>
|
||||||
[rust]: <https://rust-lang.org/>
|
[rust]: <https://rust-lang.org/>
|
||||||
[docs]: <{{docs}}>
|
[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::units::{Lp, Px, UPx};
|
||||||
use figures::{IntoSigned, Point, Px2D, Rect, Round, ScreenScale, Size, Zero};
|
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::app::winit::window::CursorIcon;
|
||||||
use kludgine::shapes::{Shape, StrokeOptions};
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
use kludgine::{Color, Kludgine, KludgineId};
|
use kludgine::{Color, Kludgine, KludgineId};
|
||||||
|
|
@ -19,7 +19,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::value::{IntoValue, Source, Value};
|
use crate::value::{IntoValue, Source, Value};
|
||||||
use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance};
|
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;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A context to an event function.
|
/// A context to an event function.
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,14 @@ use ahash::AHashSet;
|
||||||
use figures::units::Px;
|
use figures::units::Px;
|
||||||
use figures::Point;
|
use figures::Point;
|
||||||
use intentional::Assert;
|
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 kludgine::app::winit::keyboard::Key;
|
||||||
|
|
||||||
use crate::context::WidgetContext;
|
use crate::context::WidgetContext;
|
||||||
use crate::utils::IgnorePoison;
|
use crate::utils::IgnorePoison;
|
||||||
use crate::value::{Destination, Dynamic};
|
use crate::value::{Destination, Dynamic};
|
||||||
use crate::widget::{EventHandling, HANDLED, IGNORED};
|
use crate::widget::{EventHandling, HANDLED, IGNORED};
|
||||||
|
use crate::window::KeyEvent;
|
||||||
|
|
||||||
/// A fixed-rate callback that provides access to tracked input on its
|
/// A fixed-rate callback that provides access to tracked input on its
|
||||||
/// associated widget.
|
/// associated widget.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use alot::LotId;
|
||||||
use figures::units::{Px, UPx};
|
use figures::units::{Px, UPx};
|
||||||
use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
|
use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
|
||||||
use intentional::Assert;
|
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::app::winit::window::CursorIcon;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
|
|
@ -45,8 +45,8 @@ use crate::widgets::{
|
||||||
};
|
};
|
||||||
use crate::window::sealed::WindowCommand;
|
use crate::window::sealed::WindowCommand;
|
||||||
use crate::window::{
|
use crate::window::{
|
||||||
CushyWindowBuilder, DeviceId, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder, Window,
|
CushyWindowBuilder, DeviceId, KeyEvent, Rgb8, RunningWindow, ThemeMode, VirtualRecorderBuilder,
|
||||||
WindowBehavior, WindowHandle, WindowLocal,
|
Window, WindowBehavior, WindowHandle, WindowLocal,
|
||||||
};
|
};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::fmt::Debug;
|
||||||
|
|
||||||
use figures::units::Px;
|
use figures::units::Px;
|
||||||
use figures::{Point, Size};
|
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::app::winit::window::CursorIcon;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::styles::VisualOrder;
|
||||||
use crate::value::{IntoValue, Value};
|
use crate::value::{IntoValue, Value};
|
||||||
use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED};
|
use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED};
|
||||||
use crate::widgets::Space;
|
use crate::widgets::Space;
|
||||||
use crate::window::DeviceId;
|
use crate::window::{DeviceId, KeyEvent};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A callback-based custom widget.
|
/// A callback-based custom widget.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use figures::{
|
||||||
Abs, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size,
|
Abs, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size,
|
||||||
};
|
};
|
||||||
use intentional::Cast;
|
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::keyboard::{Key, NamedKey};
|
||||||
use kludgine::app::winit::window::{CursorIcon, ImePurpose};
|
use kludgine::app::winit::window::{CursorIcon, ImePurpose};
|
||||||
use kludgine::shapes::{Shape, StrokeOptions};
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
|
|
@ -27,13 +27,14 @@ use crate::styles::components::{HighlightColor, IntrinsicPadding, OutlineColor,
|
||||||
use crate::utils::ModifiersExt;
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value};
|
use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value};
|
||||||
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
|
||||||
|
use crate::window::KeyEvent;
|
||||||
use crate::{ConstraintLimit, Lazy};
|
use crate::{ConstraintLimit, Lazy};
|
||||||
|
|
||||||
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
/// A text input widget.
|
/// A text input widget.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct Input<Storage> {
|
pub struct Input<Storage = String> {
|
||||||
/// The value of this widget.
|
/// The value of this widget.
|
||||||
pub value: Dynamic<Storage>,
|
pub value: Dynamic<Storage>,
|
||||||
/// The placeholder text to display when no value is present.
|
/// The placeholder text to display when no value is present.
|
||||||
|
|
@ -1149,8 +1150,6 @@ where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
context.redraw_in(cursor_state.remaining_until_blink);
|
context.redraw_in(cursor_state.remaining_until_blink);
|
||||||
} else {
|
|
||||||
context.redraw_when_changed(context.window().focused());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1190,7 +1189,7 @@ where
|
||||||
fn keyboard_input(
|
fn keyboard_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
_device_id: crate::window::DeviceId,
|
_device_id: crate::window::DeviceId,
|
||||||
input: kludgine::app::winit::event::KeyEvent,
|
input: KeyEvent,
|
||||||
_is_synthetic: bool,
|
_is_synthetic: bool,
|
||||||
context: &mut EventContext<'_>,
|
context: &mut EventContext<'_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::styles::components::{
|
||||||
use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder};
|
use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder};
|
||||||
use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value};
|
use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value};
|
||||||
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
||||||
use crate::window::DeviceId;
|
use crate::window::{DeviceId, KeyEvent};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A widget that allows sliding between two values.
|
/// A widget that allows sliding between two values.
|
||||||
|
|
@ -673,7 +673,7 @@ where
|
||||||
fn keyboard_input(
|
fn keyboard_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
_device_id: DeviceId,
|
_device_id: DeviceId,
|
||||||
input: kludgine::app::winit::event::KeyEvent,
|
input: KeyEvent,
|
||||||
_is_synthetic: bool,
|
_is_synthetic: bool,
|
||||||
_context: &mut EventContext<'_>,
|
_context: &mut EventContext<'_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::fmt::Debug;
|
||||||
use figures::units::{Px, UPx};
|
use figures::units::{Px, UPx};
|
||||||
use figures::{Point, Size};
|
use figures::{Point, Size};
|
||||||
use intentional::Cast;
|
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::app::winit::window::CursorIcon;
|
||||||
use kludgine::tilemap;
|
use kludgine::tilemap;
|
||||||
use kludgine::tilemap::TileMapFocus;
|
use kludgine::tilemap::TileMapFocus;
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable};
|
||||||
use crate::tick::Tick;
|
use crate::tick::Tick;
|
||||||
use crate::value::{Dynamic, IntoValue, Value};
|
use crate::value::{Dynamic, IntoValue, Value};
|
||||||
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
||||||
use crate::window::DeviceId;
|
use crate::window::{DeviceId, KeyEvent};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A layered tile-based 2d game surface.
|
/// 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 intentional::{Assert, Cast};
|
||||||
use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize};
|
use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||||
use kludgine::app::winit::event::{
|
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::window::{self, CursorIcon};
|
||||||
use kludgine::app::{winit, WindowBehavior as _};
|
use kludgine::app::{winit, WindowBehavior as _};
|
||||||
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
|
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::wgpu::{self, CompositeAlphaMode, COPY_BYTES_PER_ROW_ALIGNMENT};
|
||||||
use kludgine::{Color, DrawableExt, Kludgine, KludgineId, Origin, Texture};
|
use kludgine::{Color, DrawableExt, Kludgine, KludgineId, Origin, Texture};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::animation::{
|
use crate::animation::{
|
||||||
AnimationTarget, Easing, LinearInterpolate, PercentBetween, Spawn, ZeroToOne,
|
AnimationTarget, Easing, LinearInterpolate, PercentBetween, Spawn, ZeroToOne,
|
||||||
|
|
@ -1298,6 +1301,14 @@ where
|
||||||
self.root.invalidate();
|
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>(
|
pub fn keyboard_input<W>(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: W,
|
window: W,
|
||||||
|
|
@ -1674,7 +1685,7 @@ where
|
||||||
window: kludgine::app::Window<'_, WindowCommand>,
|
window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
_kludgine: &mut Kludgine,
|
_kludgine: &mut Kludgine,
|
||||||
) {
|
) {
|
||||||
self.focused.set(window.focused());
|
self.set_focused(window.focused());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn occlusion_changed(
|
fn occlusion_changed(
|
||||||
|
|
@ -1682,7 +1693,7 @@ where
|
||||||
window: kludgine::app::Window<'_, WindowCommand>,
|
window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
_kludgine: &mut Kludgine,
|
_kludgine: &mut Kludgine,
|
||||||
) {
|
) {
|
||||||
self.occluded.set(window.ocluded());
|
self.set_occluded(window.ocluded());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'pass>(
|
fn render<'pass>(
|
||||||
|
|
@ -1778,10 +1789,16 @@ where
|
||||||
window: kludgine::app::Window<'_, WindowCommand>,
|
window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
kludgine: &mut Kludgine,
|
kludgine: &mut Kludgine,
|
||||||
device_id: winit::event::DeviceId,
|
device_id: winit::event::DeviceId,
|
||||||
input: KeyEvent,
|
input: winit::event::KeyEvent,
|
||||||
is_synthetic: bool,
|
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(
|
fn mouse_wheel(
|
||||||
|
|
@ -2570,6 +2587,14 @@ impl CushyWindow {
|
||||||
kludgine::Graphics::new(&mut self.kludgine, device, queue)
|
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.
|
/// Requests that the window close.
|
||||||
///
|
///
|
||||||
/// Returns true if the request should be honored.
|
/// Returns true if the request should be honored.
|
||||||
|
|
@ -2699,6 +2724,7 @@ impl VirtualWindow {
|
||||||
.map(|i| now.duration_since(i))
|
.map(|i| now.duration_since(i))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
self.last_rendered_at = Some(now);
|
self.last_rendered_at = Some(now);
|
||||||
|
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
||||||
self.cushy.prepare(&mut self.state, device, queue);
|
self.cushy.prepare(&mut self.state, device, queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2726,7 +2752,6 @@ impl VirtualWindow {
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
additional_drawing: Option<&Drawing>,
|
additional_drawing: Option<&Drawing>,
|
||||||
) -> Option<wgpu::SubmissionIndex> {
|
) -> Option<wgpu::SubmissionIndex> {
|
||||||
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
|
||||||
self.cushy
|
self.cushy
|
||||||
.render_with(pass, device, queue, additional_drawing)
|
.render_with(pass, device, queue, additional_drawing)
|
||||||
}
|
}
|
||||||
|
|
@ -2739,7 +2764,6 @@ impl VirtualWindow {
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
queue: &wgpu::Queue,
|
queue: &wgpu::Queue,
|
||||||
) -> Option<wgpu::SubmissionIndex> {
|
) -> Option<wgpu::SubmissionIndex> {
|
||||||
self.state.dynamic.redraw_target.set(RedrawTarget::Never);
|
|
||||||
self.cushy.render_into(texture, load_op, device, queue)
|
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.
|
/// Returns true if this window should no longer be open.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn closed(&self) -> bool {
|
pub fn closed(&self) -> bool {
|
||||||
|
|
@ -3363,6 +3395,40 @@ where
|
||||||
self.wait_for(over)
|
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.
|
/// Waits for `duration`, rendering frames as needed.
|
||||||
pub fn wait_for(&mut self, duration: Duration) -> Result<(), VirtualRecorderError> {
|
pub fn wait_for(&mut self, duration: Duration) -> Result<(), VirtualRecorderError> {
|
||||||
self.wait_until(Instant::now() + duration)
|
self.wait_until(Instant::now() + duration)
|
||||||
|
|
@ -3391,7 +3457,7 @@ where
|
||||||
RedrawTarget::At(instant) => now.min(instant),
|
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
|
// Try to reuse an existing capture instead of forcing an
|
||||||
// allocation.
|
// allocation.
|
||||||
if let Ok(capture) = assembler.resuable_captures.try_recv() {
|
if let Ok(capture) = assembler.resuable_captures.try_recv() {
|
||||||
|
|
@ -3399,6 +3465,7 @@ where
|
||||||
}
|
}
|
||||||
let elapsed = now.saturating_duration_since(last_frame);
|
let elapsed = now.saturating_duration_since(last_frame);
|
||||||
last_frame = now;
|
last_frame = now;
|
||||||
|
println!("Redrawing");
|
||||||
self.recorder.redraw();
|
self.recorder.redraw();
|
||||||
let capture = self.recorder.capture.take().assert("always present");
|
let capture = self.recorder.capture.take().assert("always present");
|
||||||
if assembler.sender.send((capture, elapsed)).is_err() {
|
if assembler.sender.send((capture, elapsed)).is_err() {
|
||||||
|
|
@ -3447,8 +3514,7 @@ where
|
||||||
let mut current_frame_delay = Duration::ZERO;
|
let mut current_frame_delay = Duration::ZERO;
|
||||||
let mut writer = encoder.write_header()?;
|
let mut writer = encoder.write_header()?;
|
||||||
for frame in &frames {
|
for frame in &frames {
|
||||||
writer.write_image_data(&frame.data)?;
|
if current_frame_delay != frame.duration && frames.len() > 1 {
|
||||||
if current_frame_delay != frame.duration {
|
|
||||||
current_frame_delay = frame.duration;
|
current_frame_delay = frame.duration;
|
||||||
// This has a limitation that a single frame can't be longer
|
// This has a limitation that a single frame can't be longer
|
||||||
// than ~6.5 seconds, but it ensures frame timing is more
|
// than ~6.5 seconds, but it ensures frame timing is more
|
||||||
|
|
@ -3458,9 +3524,10 @@ where
|
||||||
10_000,
|
10_000,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
writer.write_image_data(&frame.data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.finish()?;
|
writer.finish().unwrap();
|
||||||
|
|
||||||
file.sync_all()?;
|
file.sync_all()?;
|
||||||
|
|
||||||
|
|
@ -3604,3 +3671,27 @@ impl FrameAssembler {
|
||||||
let _result = result.send(Ok(assembled));
|
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