Async, better scroll, Input::on_key

This commit is contained in:
Jonathan Johnson 2023-11-03 07:15:34 -07:00
parent 0026a6db0d
commit 501eecd7a5
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
11 changed files with 274 additions and 70 deletions

12
Cargo.lock generated
View file

@ -96,7 +96,7 @@ dependencies = [
[[package]]
name = "appit"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/appit#91c540c2a2db69eb25ea47eccb7aac1eb911933e"
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
dependencies = [
"raw-window-handle 0.5.2",
"winit",
@ -847,7 +847,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kludgine"
version = "0.1.0"
source = "git+https://github.com/khonsulabs/kludgine#35b83e14e71a02f3cd9a96e865094f5b3ad1407c"
source = "git+https://github.com/khonsulabs/kludgine#ce69ff4ecf5995a3120d2fc56f4fa6c16381d6b5"
dependencies = [
"ahash",
"alot",
@ -2428,18 +2428,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
[[package]]
name = "zerocopy"
version = "0.7.23"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107"
checksum = "092cd76b01a033a9965b9097da258689d9e17c69ded5dcf41bca001dd20ebc6d"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.23"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d"
checksum = "a13a20a7c6a90e2034bcc65495799da92efcec6a8dd4f3fcb6f7a48988637ead"
dependencies = [
"proc-macro2",
"quote",

View file

@ -15,6 +15,8 @@ kempt = "0.2.1"
# [patch."https://github.com/khonsulabs/kludgine"]
# kludgine = { path = "../kludgine2" }
# [patch."https://github.com/khonsulabs/appit"]
# appit = { path = "../appit" }
# [patch."https://github.com/khonsulabs/figures"]
# figures = { path = "../figures" }

46
examples/gameui.rs Normal file
View file

@ -0,0 +1,46 @@
use gooey::value::Dynamic;
use gooey::widget::{HANDLED, IGNORED};
use gooey::widgets::{Canvas, Expand, Input, Label, Scroll, Stack};
use gooey::{widgets, Run};
use kludgine::app::winit::event::ElementState;
use kludgine::app::winit::keyboard::Key;
use kludgine::figures::{Point, Rect};
use kludgine::shapes::Shape;
use kludgine::Color;
fn main() -> gooey::Result {
let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100));
let chat_message = Dynamic::new(String::new());
Expand::new(Stack::rows(widgets![
Expand::new(Stack::columns(widgets![
Expand::new(Scroll::vertical(Label::new(chat_log.clone()))),
Expand::weighted(
2,
Canvas::new(|context| {
let entire_canvas = Rect::from(context.graphics.size());
context.graphics.draw_shape(
&Shape::filled_rect(entire_canvas, Color::RED),
Point::default(),
None,
None,
);
})
)
])),
Input::new(chat_message.clone()).on_key(move |input| {
match (input.state, input.logical_key) {
(ElementState::Pressed, Key::Enter) => {
let new_message = chat_message.map_mut(|text| std::mem::take(text));
chat_log.map_mut(|chat_log| {
chat_log.push_str(&new_message);
chat_log.push('\n');
});
HANDLED
}
_ => IGNORED,
}
}),
]))
.run()
}

View file

@ -11,7 +11,6 @@ use alot::{LotId, Lots};
use kempt::Set;
use kludgine::Color;
use crate::impl_all_tuples;
use crate::value::Dynamic;
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());

View file

@ -2,6 +2,9 @@
#![warn(clippy::pedantic, missing_docs)]
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
#[macro_use]
mod utils;
pub mod animation;
pub mod context;
mod graphics;
@ -9,7 +12,6 @@ mod names;
pub mod styles;
mod tick;
mod tree;
mod utils;
pub mod value;
pub mod widget;
pub mod widgets;
@ -88,6 +90,9 @@ macro_rules! widgets {
}};
}
/// Counts the number of expressions passed to it.
///
/// This is used inside of Gooey macros to preallocate collections.
#[macro_export]
#[doc(hidden)]
macro_rules! count {
@ -115,16 +120,3 @@ macro_rules! styles {
$crate::styles!($($component => $value),*)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_all_tuples {
($macro_name:ident) => {
$macro_name!(T0 0);
$macro_name!(T0 0, T1 1);
$macro_name!(T0 0, T1 1, T2 2);
$macro_name!(T0 0, T1 1, T2 2, T3 3);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5);
}
}

View file

@ -62,3 +62,18 @@ impl<T> Deref for Lazy<T> {
self.once.get_or_init(self.init)
}
}
/// Invokes the provided macro with a pattern that can be matched using this
/// macro_rules expression: `$($type:ident $field:tt),+`, where `$type` is an
/// identifier to use for the generic parameter and `$field` is the field index
/// inside of the tuple.
macro_rules! impl_all_tuples {
($macro_name:ident) => {
$macro_name!(T0 0);
$macro_name!(T0 0, T1 1);
$macro_name!(T0 0, T1 1, T2 2);
$macro_name!(T0 0, T1 1, T2 2, T3 3);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5);
}
}

View file

@ -1,8 +1,10 @@
//! Types for storing and interacting with values in Widgets.
use std::fmt::Debug;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::task::{Poll, Waker};
use crate::animation::{DynamicTransition, LinearInterpolate};
use crate::context::{WidgetContext, WindowHandle};
@ -24,6 +26,7 @@ impl<T> Dynamic<T> {
callbacks: Vec::new(),
windows: Vec::new(),
readers: 0,
wakers: Vec::new(),
}),
sync: AssertUnwindSafe(Condvar::new()),
}))
@ -39,7 +42,7 @@ impl<T> Dynamic<T> {
/// function, all observers will be notified that the contents have been
/// updated.
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
self.0.map_mut(map)
self.0.map_mut(|value, _| map(value))
}
/// Attaches `for_each` to this value so that it is invoked each time the
@ -106,7 +109,8 @@ impl<T> Dynamic<T> {
/// the contents have been updated.
#[must_use]
pub fn replace(&self, new_value: T) -> T {
self.0.map_mut(|value| std::mem::replace(value, new_value))
self.0
.map_mut(|value, _| std::mem::replace(value, new_value))
}
/// Stores `new_value` in this dynamic. Before returning from this function,
@ -115,6 +119,21 @@ impl<T> Dynamic<T> {
let _old = self.replace(new_value);
}
/// Updates this dynamic with `new_value`, but only if `new_value` is not
/// equal to the currently stored value.
pub fn update(&self, new_value: T)
where
T: Eq,
{
self.0.map_mut(|value, changed| {
if *value == new_value {
*changed = false;
} else {
*value = new_value;
}
});
}
/// Returns a new reference-based reader for this dynamic value.
#[must_use]
pub fn create_ref_reader(&self) -> DynamicReader<T> {
@ -207,21 +226,26 @@ impl<T> DynamicData<T> {
self.state().wrapped.clone()
}
#[must_use]
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
let mut state = self.state();
let old = {
let state = &mut *state;
let generation = state.wrapped.generation.next();
let result = map(&mut state.wrapped.value);
state.wrapped.generation = generation;
let mut changed = true;
let result = map(&mut state.wrapped.value, &mut changed);
if changed {
state.wrapped.generation = state.wrapped.generation.next();
for callback in &mut state.callbacks {
callback.update(&state.wrapped);
}
for window in state.windows.drain(..) {
window.redraw();
for callback in &mut state.callbacks {
callback.update(&state.wrapped);
}
for window in state.windows.drain(..) {
window.redraw();
}
for waker in state.wakers.drain(..) {
waker.wake();
}
}
result
};
drop(state);
@ -261,6 +285,7 @@ struct State<T> {
wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>,
windows: Vec<WindowHandle>,
wakers: Vec<Waker>,
readers: usize,
}
@ -345,6 +370,14 @@ impl<T> DynamicReader<T> {
.map_or_else(PoisonError::into_inner, |g| g);
}
}
/// Suspends the current async task until the contained value has been
/// updated or there are no remaining writers for the value.
///
/// Returns true if a newly updated value was discovered.
pub fn block_until_updated_async(&mut self) -> BlockUntilUpdatedFuture<'_, T> {
BlockUntilUpdatedFuture(self)
}
}
impl<T> Clone for DynamicReader<T> {
@ -364,6 +397,30 @@ impl<T> Drop for DynamicReader<T> {
}
}
/// Suspends the current async task until the contained value has been
/// updated or there are no remaining writers for the value.
///
/// Yeilds true if a newly updated value was discovered.
#[derive(Debug)]
#[must_use = "futures must be .await'ed to be executed"]
pub struct BlockUntilUpdatedFuture<'a, T>(&'a mut DynamicReader<T>);
impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
type Output = bool;
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let mut state = self.0.source.state();
if state.wrapped.generation != self.0.read_generation {
return Poll::Ready(true);
} else if state.readers == Arc::strong_count(&self.0.source) {
return Poll::Ready(false);
}
state.wakers.push(cx.waker().clone());
Poll::Pending
}
}
#[test]
fn disconnecting_reader_from_dynamic() {
let value = Dynamic::new(1);

View file

@ -1,8 +1,9 @@
use std::cmp::Ordering;
use std::fmt::Debug;
use std::panic::UnwindSafe;
use std::time::Duration;
use kludgine::app::winit::event::Ime;
use kludgine::app::winit::event::{Ime, KeyEvent};
use kludgine::app::winit::keyboard::Key;
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
use kludgine::figures::units::Px;
@ -18,7 +19,7 @@ use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}
use crate::styles::Styles;
use crate::utils::ModifiersExt;
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED};
const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
@ -27,6 +28,7 @@ const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500);
pub struct Input {
/// The value of this widget.
pub text: Value<String>,
on_key: Option<Callback<KeyEvent, EventHandling>>,
editor: Option<LiveEditor>,
cursor_state: CursorState,
}
@ -43,9 +45,22 @@ impl Input {
text: initial_text.into_value(),
editor: None,
cursor_state: CursorState::default(),
on_key: None,
}
}
/// Sets the `on_key` callback.
///
/// This function is called for every keyboard input event. If [`HANDLED`]
/// is returned, this widget will ignore the event.
pub fn on_key<F>(mut self, on_key: F) -> Self
where
F: FnMut(KeyEvent) -> EventHandling + Send + UnwindSafe + 'static,
{
self.on_key = Some(Callback::new(on_key));
self
}
fn editor_mut(&mut self, kludgine: &mut Kludgine, styles: &Styles) -> &mut Editor {
match (&self.editor, self.text.generation()) {
(Some(editor), generation) if editor.generation == generation => {}
@ -315,6 +330,10 @@ impl Widget for Input {
_is_synthetic: bool,
context: &mut EventContext<'_, '_>,
) -> EventHandling {
if let Some(on_key) = &mut self.on_key {
on_key.invoke(input.clone())?;
}
if !input.state.is_pressed() {
return IGNORED;
}
@ -322,11 +341,11 @@ impl Widget for Input {
let styles = context.query_styles(&[&TextColor]);
let editor = self.editor_mut(context.kludgine, &styles);
println!(
"Keyboard input: {:?}. {:?}, {:?}",
input.logical_key, input.text, input.physical_key
);
let handled = match (input.logical_key, input.text) {
// println!(
// "Keyboard input: {:?}. {:?}, {:?}",
// input.logical_key, input.text, input.physical_key
// );
let (text_changed, handled) = match (input.logical_key, input.text) {
(key @ (Key::Backspace | Key::Delete), _) => {
editor.action(
context.kludgine.font_system(),
@ -336,7 +355,7 @@ impl Widget for Input {
_ => unreachable!("previously matched"),
},
);
HANDLED
(true, HANDLED)
}
(key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
let modifiers = context.modifiers();
@ -362,18 +381,30 @@ impl Widget for Input {
_ => unreachable!("previously matched"),
},
);
HANDLED
(false, HANDLED)
}
(_, Some(text)) if !context.modifiers().state().primary() => {
editor.insert_string(&text, None);
HANDLED
(true, HANDLED)
}
(_, _) => IGNORED,
(_, _) => (false, IGNORED),
};
if handled.is_break() {
context.set_needs_redraw();
self.cursor_state.force_on();
if text_changed {
if let Value::Dynamic(value) = &self.text {
let state = self.editor.as_mut().expect("just_used");
value.map_mut(|text| {
text.clear();
for line in &state.editor.buffer().lines {
text.push_str(line.text());
}
});
state.generation = Some(value.generation());
}
}
}
handled

View file

@ -48,27 +48,52 @@ impl ChildWidget {
pub struct Scroll {
contents: ChildWidget,
content_size: Size<Px>,
scroll: Point<Px>,
max_scroll: Point<Px>,
control_size: Size<Px>,
scroll: Dynamic<Point<Px>>,
enabled: Point<bool>,
max_scroll: Dynamic<Point<Px>>,
scrollbar_opacity: Dynamic<ZeroToOne>,
scrollbar_opacity_animation: AnimationHandle,
}
impl Scroll {
/// Returns a new scroll widget containing `contents`.
pub fn new(contents: impl MakeWidget) -> Self {
fn construct(contents: impl MakeWidget, enabled: Point<bool>) -> Self {
Self {
contents: ChildWidget::Instance(contents.make_widget()),
enabled,
content_size: Size::default(),
scroll: Point::default(),
max_scroll: Point::default(),
control_size: Size::default(),
scroll: Dynamic::new(Point::default()),
max_scroll: Dynamic::new(Point::default()),
scrollbar_opacity: Dynamic::default(),
scrollbar_opacity_animation: AnimationHandle::new(),
}
}
/// Returns a new scroll widget containing `contents` that allows scrolling
/// vertically or horizontally.
pub fn new(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(true, true))
}
/// Returns a new scroll widget that allows scrolling `contents`
/// horizontally.
pub fn horizontal(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(true, false))
}
/// Returns a new scroll widget that allows scrolling `contents` vertically.
pub fn vertical(contents: impl MakeWidget) -> Self {
Self::construct(contents, Point::new(false, true))
}
fn constrain_scroll(&mut self) {
self.scroll = self.scroll.max(self.max_scroll).min(Point::default());
let scroll = self.scroll.get();
let clamped = scroll.max(self.max_scroll.get()).min(Point::default());
if clamped != scroll {
self.scroll.set(clamped);
}
}
}
@ -111,29 +136,68 @@ impl Widget for Scroll {
.get_or_default(&ScrollBarThickness)
.into_px(context.graphics.scale());
let mut scroll = self.scroll.get();
let current_max_scroll = self.max_scroll.get();
let control_size = context.graphics.region().size;
let max_extents = Size::new(
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.x.into_unsigned()),
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.y.into_unsigned()),
if self.enabled.x {
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
} else {
ConstraintLimit::Known(control_size.width.into_unsigned())
},
if self.enabled.y {
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
} else {
ConstraintLimit::Known(control_size.height.into_unsigned())
},
);
let managed = self.contents.managed(&mut context.as_event_context());
self.content_size = context
let new_content_size = context
.for_other(&managed)
.measure(max_extents)
.into_signed();
let control_size = context.graphics.region().size;
let horizontal_bar = scrollbar_region(scroll.x, new_content_size.width, control_size.width);
let max_scroll_x = if self.enabled.x {
-horizontal_bar.amount_hidden
} else {
Px(0)
};
let vertical_bar = scrollbar_region(scroll.y, new_content_size.height, control_size.height);
let max_scroll_y = if self.enabled.y {
-vertical_bar.amount_hidden
} else {
Px(0)
};
// Preserve the current scroll if the widget has resized
if self.content_size.width != new_content_size.width
|| self.control_size.width != control_size.width
{
self.content_size.width = new_content_size.width;
let scroll_pct = scroll.x.into_float() / current_max_scroll.x.into_float();
scroll.x = max_scroll_x * scroll_pct;
}
if self.content_size.height != new_content_size.height
|| self.control_size.height != control_size.height
{
self.content_size.height = new_content_size.height;
let scroll_pct = scroll.y.into_float() / current_max_scroll.y.into_float();
scroll.y = max_scroll_y * scroll_pct;
}
self.scroll.update(scroll);
let region = Rect::new(
self.scroll,
scroll,
self.content_size
.min(Size::new(Px::MAX, Px::MAX) - self.scroll.max(Point::default())),
.min(Size::new(Px::MAX, Px::MAX) - scroll.max(Point::default())),
);
context.for_child(&managed, region).redraw();
let horizontal_bar =
scrollbar_region(self.scroll.x, self.content_size.width, control_size.width);
self.max_scroll.x = -horizontal_bar.amount_hidden;
if horizontal_bar.size > 0 {
if max_scroll_x != 0 {
context.graphics.draw_shape(
&Shape::filled_rect(
Rect::new(
@ -148,11 +212,7 @@ impl Widget for Scroll {
);
}
let vertical_bar =
scrollbar_region(self.scroll.y, self.content_size.height, control_size.height);
self.max_scroll.y = -vertical_bar.amount_hidden;
if vertical_bar.size > 0 {
if max_scroll_y != 0 {
context.graphics.draw_shape(
&Shape::filled_rect(
Rect::new(
@ -166,6 +226,10 @@ impl Widget for Scroll {
None,
);
}
self.control_size = control_size;
self.max_scroll
.update(Point::new(max_scroll_x, max_scroll_y));
}
fn measure(
@ -191,7 +255,7 @@ impl Widget for Scroll {
}
};
self.scroll += amount.cast();
self.scroll.map_mut(|scroll| *scroll += amount.cast());
context.set_needs_redraw();
// TODO make this only returned handled if we actually scrolled.

View file

@ -297,7 +297,7 @@ where
if !handled {
match input.physical_key {
KeyCode::KeyW
if window.modifiers().state().primary() && dbg!(input.state).is_pressed() =>
if window.modifiers().state().primary() && input.state.is_pressed() =>
{
if self.request_close(&mut window) {
window.set_needs_redraw();

View file

@ -1,5 +1,3 @@
use crate::impl_all_tuples;
/// Invokes a function with a clone of `self`.
pub trait WithClone: Sized {
/// The type that results from cloning.