mirror of
https://github.com/danbulant/cushy
synced 2026-06-08 09:01:12 +00:00
Async, better scroll, Input::on_key
This commit is contained in:
parent
0026a6db0d
commit
501eecd7a5
11 changed files with 274 additions and 70 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -96,7 +96,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "appit"
|
name = "appit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/appit#91c540c2a2db69eb25ea47eccb7aac1eb911933e"
|
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"raw-window-handle 0.5.2",
|
"raw-window-handle 0.5.2",
|
||||||
"winit",
|
"winit",
|
||||||
|
|
@ -847,7 +847,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kludgine"
|
name = "kludgine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/kludgine#35b83e14e71a02f3cd9a96e865094f5b3ad1407c"
|
source = "git+https://github.com/khonsulabs/kludgine#ce69ff4ecf5995a3120d2fc56f4fa6c16381d6b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"alot",
|
"alot",
|
||||||
|
|
@ -2428,18 +2428,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.23"
|
version = "0.7.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107"
|
checksum = "092cd76b01a033a9965b9097da258689d9e17c69ded5dcf41bca001dd20ebc6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.7.23"
|
version = "0.7.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d"
|
checksum = "a13a20a7c6a90e2034bcc65495799da92efcec6a8dd4f3fcb6f7a48988637ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ kempt = "0.2.1"
|
||||||
|
|
||||||
# [patch."https://github.com/khonsulabs/kludgine"]
|
# [patch."https://github.com/khonsulabs/kludgine"]
|
||||||
# kludgine = { path = "../kludgine2" }
|
# kludgine = { path = "../kludgine2" }
|
||||||
|
# [patch."https://github.com/khonsulabs/appit"]
|
||||||
|
# appit = { path = "../appit" }
|
||||||
# [patch."https://github.com/khonsulabs/figures"]
|
# [patch."https://github.com/khonsulabs/figures"]
|
||||||
# figures = { path = "../figures" }
|
# figures = { path = "../figures" }
|
||||||
|
|
||||||
|
|
|
||||||
46
examples/gameui.rs
Normal file
46
examples/gameui.rs
Normal 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()
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ use alot::{LotId, Lots};
|
||||||
use kempt::Set;
|
use kempt::Set;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
use crate::impl_all_tuples;
|
|
||||||
use crate::value::Dynamic;
|
use crate::value::Dynamic;
|
||||||
|
|
||||||
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
|
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
|
||||||
|
|
|
||||||
20
src/lib.rs
20
src/lib.rs
|
|
@ -2,6 +2,9 @@
|
||||||
#![warn(clippy::pedantic, missing_docs)]
|
#![warn(clippy::pedantic, missing_docs)]
|
||||||
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
mod graphics;
|
mod graphics;
|
||||||
|
|
@ -9,7 +12,6 @@ mod names;
|
||||||
pub mod styles;
|
pub mod styles;
|
||||||
mod tick;
|
mod tick;
|
||||||
mod tree;
|
mod tree;
|
||||||
mod utils;
|
|
||||||
pub mod value;
|
pub mod value;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
pub mod widgets;
|
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]
|
#[macro_export]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
macro_rules! count {
|
macro_rules! count {
|
||||||
|
|
@ -115,16 +120,3 @@ macro_rules! styles {
|
||||||
$crate::styles!($($component => $value),*)
|
$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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
15
src/utils.rs
15
src/utils.rs
|
|
@ -62,3 +62,18 @@ impl<T> Deref for Lazy<T> {
|
||||||
self.once.get_or_init(self.init)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
81
src/value.rs
81
src/value.rs
|
|
@ -1,8 +1,10 @@
|
||||||
//! Types for storing and interacting with values in Widgets.
|
//! Types for storing and interacting with values in Widgets.
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::future::Future;
|
||||||
use std::panic::AssertUnwindSafe;
|
use std::panic::AssertUnwindSafe;
|
||||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
|
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
|
||||||
|
use std::task::{Poll, Waker};
|
||||||
|
|
||||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||||
use crate::context::{WidgetContext, WindowHandle};
|
use crate::context::{WidgetContext, WindowHandle};
|
||||||
|
|
@ -24,6 +26,7 @@ impl<T> Dynamic<T> {
|
||||||
callbacks: Vec::new(),
|
callbacks: Vec::new(),
|
||||||
windows: Vec::new(),
|
windows: Vec::new(),
|
||||||
readers: 0,
|
readers: 0,
|
||||||
|
wakers: Vec::new(),
|
||||||
}),
|
}),
|
||||||
sync: AssertUnwindSafe(Condvar::new()),
|
sync: AssertUnwindSafe(Condvar::new()),
|
||||||
}))
|
}))
|
||||||
|
|
@ -39,7 +42,7 @@ impl<T> Dynamic<T> {
|
||||||
/// function, all observers will be notified that the contents have been
|
/// function, all observers will be notified that the contents have been
|
||||||
/// updated.
|
/// updated.
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
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
|
/// 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.
|
/// the contents have been updated.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn replace(&self, new_value: T) -> T {
|
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,
|
/// 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);
|
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.
|
/// Returns a new reference-based reader for this dynamic value.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn create_ref_reader(&self) -> DynamicReader<T> {
|
pub fn create_ref_reader(&self) -> DynamicReader<T> {
|
||||||
|
|
@ -207,21 +226,26 @@ impl<T> DynamicData<T> {
|
||||||
self.state().wrapped.clone()
|
self.state().wrapped.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
|
||||||
let mut state = self.state();
|
let mut state = self.state();
|
||||||
let old = {
|
let old = {
|
||||||
let state = &mut *state;
|
let state = &mut *state;
|
||||||
let generation = state.wrapped.generation.next();
|
let mut changed = true;
|
||||||
let result = map(&mut state.wrapped.value);
|
let result = map(&mut state.wrapped.value, &mut changed);
|
||||||
state.wrapped.generation = generation;
|
if changed {
|
||||||
|
state.wrapped.generation = state.wrapped.generation.next();
|
||||||
|
|
||||||
for callback in &mut state.callbacks {
|
for callback in &mut state.callbacks {
|
||||||
callback.update(&state.wrapped);
|
callback.update(&state.wrapped);
|
||||||
}
|
}
|
||||||
for window in state.windows.drain(..) {
|
for window in state.windows.drain(..) {
|
||||||
window.redraw();
|
window.redraw();
|
||||||
|
}
|
||||||
|
for waker in state.wakers.drain(..) {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
};
|
};
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
@ -261,6 +285,7 @@ struct State<T> {
|
||||||
wrapped: GenerationalValue<T>,
|
wrapped: GenerationalValue<T>,
|
||||||
callbacks: Vec<Box<dyn ValueCallback<T>>>,
|
callbacks: Vec<Box<dyn ValueCallback<T>>>,
|
||||||
windows: Vec<WindowHandle>,
|
windows: Vec<WindowHandle>,
|
||||||
|
wakers: Vec<Waker>,
|
||||||
readers: usize,
|
readers: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,6 +370,14 @@ impl<T> DynamicReader<T> {
|
||||||
.map_or_else(PoisonError::into_inner, |g| g);
|
.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> {
|
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]
|
#[test]
|
||||||
fn disconnecting_reader_from_dynamic() {
|
fn disconnecting_reader_from_dynamic() {
|
||||||
let value = Dynamic::new(1);
|
let value = Dynamic::new(1);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use std::panic::UnwindSafe;
|
||||||
use std::time::Duration;
|
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::app::winit::keyboard::Key;
|
||||||
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
|
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
|
||||||
use kludgine::figures::units::Px;
|
use kludgine::figures::units::Px;
|
||||||
|
|
@ -18,7 +19,7 @@ use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}
|
||||||
use crate::styles::Styles;
|
use crate::styles::Styles;
|
||||||
use crate::utils::ModifiersExt;
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Generation, IntoValue, Value};
|
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);
|
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 {
|
pub struct Input {
|
||||||
/// The value of this widget.
|
/// The value of this widget.
|
||||||
pub text: Value<String>,
|
pub text: Value<String>,
|
||||||
|
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||||
editor: Option<LiveEditor>,
|
editor: Option<LiveEditor>,
|
||||||
cursor_state: CursorState,
|
cursor_state: CursorState,
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +45,22 @@ impl Input {
|
||||||
text: initial_text.into_value(),
|
text: initial_text.into_value(),
|
||||||
editor: None,
|
editor: None,
|
||||||
cursor_state: CursorState::default(),
|
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 {
|
fn editor_mut(&mut self, kludgine: &mut Kludgine, styles: &Styles) -> &mut Editor {
|
||||||
match (&self.editor, self.text.generation()) {
|
match (&self.editor, self.text.generation()) {
|
||||||
(Some(editor), generation) if editor.generation == generation => {}
|
(Some(editor), generation) if editor.generation == generation => {}
|
||||||
|
|
@ -315,6 +330,10 @@ impl Widget for Input {
|
||||||
_is_synthetic: bool,
|
_is_synthetic: bool,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
if let Some(on_key) = &mut self.on_key {
|
||||||
|
on_key.invoke(input.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
if !input.state.is_pressed() {
|
if !input.state.is_pressed() {
|
||||||
return IGNORED;
|
return IGNORED;
|
||||||
}
|
}
|
||||||
|
|
@ -322,11 +341,11 @@ impl Widget for Input {
|
||||||
let styles = context.query_styles(&[&TextColor]);
|
let styles = context.query_styles(&[&TextColor]);
|
||||||
let editor = self.editor_mut(context.kludgine, &styles);
|
let editor = self.editor_mut(context.kludgine, &styles);
|
||||||
|
|
||||||
println!(
|
// println!(
|
||||||
"Keyboard input: {:?}. {:?}, {:?}",
|
// "Keyboard input: {:?}. {:?}, {:?}",
|
||||||
input.logical_key, input.text, input.physical_key
|
// input.logical_key, input.text, input.physical_key
|
||||||
);
|
// );
|
||||||
let handled = match (input.logical_key, input.text) {
|
let (text_changed, handled) = match (input.logical_key, input.text) {
|
||||||
(key @ (Key::Backspace | Key::Delete), _) => {
|
(key @ (Key::Backspace | Key::Delete), _) => {
|
||||||
editor.action(
|
editor.action(
|
||||||
context.kludgine.font_system(),
|
context.kludgine.font_system(),
|
||||||
|
|
@ -336,7 +355,7 @@ impl Widget for Input {
|
||||||
_ => unreachable!("previously matched"),
|
_ => unreachable!("previously matched"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
HANDLED
|
(true, HANDLED)
|
||||||
}
|
}
|
||||||
(key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
|
(key @ (Key::ArrowLeft | Key::ArrowDown | Key::ArrowUp | Key::ArrowRight), _) => {
|
||||||
let modifiers = context.modifiers();
|
let modifiers = context.modifiers();
|
||||||
|
|
@ -362,18 +381,30 @@ impl Widget for Input {
|
||||||
_ => unreachable!("previously matched"),
|
_ => unreachable!("previously matched"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
HANDLED
|
(false, HANDLED)
|
||||||
}
|
}
|
||||||
(_, Some(text)) if !context.modifiers().state().primary() => {
|
(_, Some(text)) if !context.modifiers().state().primary() => {
|
||||||
editor.insert_string(&text, None);
|
editor.insert_string(&text, None);
|
||||||
HANDLED
|
(true, HANDLED)
|
||||||
}
|
}
|
||||||
(_, _) => IGNORED,
|
(_, _) => (false, IGNORED),
|
||||||
};
|
};
|
||||||
|
|
||||||
if handled.is_break() {
|
if handled.is_break() {
|
||||||
context.set_needs_redraw();
|
context.set_needs_redraw();
|
||||||
self.cursor_state.force_on();
|
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
|
handled
|
||||||
|
|
|
||||||
|
|
@ -48,27 +48,52 @@ impl ChildWidget {
|
||||||
pub struct Scroll {
|
pub struct Scroll {
|
||||||
contents: ChildWidget,
|
contents: ChildWidget,
|
||||||
content_size: Size<Px>,
|
content_size: Size<Px>,
|
||||||
scroll: Point<Px>,
|
control_size: Size<Px>,
|
||||||
max_scroll: Point<Px>,
|
scroll: Dynamic<Point<Px>>,
|
||||||
|
enabled: Point<bool>,
|
||||||
|
max_scroll: Dynamic<Point<Px>>,
|
||||||
scrollbar_opacity: Dynamic<ZeroToOne>,
|
scrollbar_opacity: Dynamic<ZeroToOne>,
|
||||||
scrollbar_opacity_animation: AnimationHandle,
|
scrollbar_opacity_animation: AnimationHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scroll {
|
impl Scroll {
|
||||||
/// Returns a new scroll widget containing `contents`.
|
/// Returns a new scroll widget containing `contents`.
|
||||||
pub fn new(contents: impl MakeWidget) -> Self {
|
fn construct(contents: impl MakeWidget, enabled: Point<bool>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
contents: ChildWidget::Instance(contents.make_widget()),
|
contents: ChildWidget::Instance(contents.make_widget()),
|
||||||
|
enabled,
|
||||||
content_size: Size::default(),
|
content_size: Size::default(),
|
||||||
scroll: Point::default(),
|
control_size: Size::default(),
|
||||||
max_scroll: Point::default(),
|
scroll: Dynamic::new(Point::default()),
|
||||||
|
max_scroll: Dynamic::new(Point::default()),
|
||||||
scrollbar_opacity: Dynamic::default(),
|
scrollbar_opacity: Dynamic::default(),
|
||||||
scrollbar_opacity_animation: AnimationHandle::new(),
|
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) {
|
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)
|
.get_or_default(&ScrollBarThickness)
|
||||||
.into_px(context.graphics.scale());
|
.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(
|
let max_extents = Size::new(
|
||||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.x.into_unsigned()),
|
if self.enabled.x {
|
||||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.y.into_unsigned()),
|
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());
|
let managed = self.contents.managed(&mut context.as_event_context());
|
||||||
self.content_size = context
|
let new_content_size = context
|
||||||
.for_other(&managed)
|
.for_other(&managed)
|
||||||
.measure(max_extents)
|
.measure(max_extents)
|
||||||
.into_signed();
|
.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(
|
let region = Rect::new(
|
||||||
self.scroll,
|
scroll,
|
||||||
self.content_size
|
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();
|
context.for_child(&managed, region).redraw();
|
||||||
|
|
||||||
let horizontal_bar =
|
if max_scroll_x != 0 {
|
||||||
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 {
|
|
||||||
context.graphics.draw_shape(
|
context.graphics.draw_shape(
|
||||||
&Shape::filled_rect(
|
&Shape::filled_rect(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
|
|
@ -148,11 +212,7 @@ impl Widget for Scroll {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let vertical_bar =
|
if max_scroll_y != 0 {
|
||||||
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 {
|
|
||||||
context.graphics.draw_shape(
|
context.graphics.draw_shape(
|
||||||
&Shape::filled_rect(
|
&Shape::filled_rect(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
|
|
@ -166,6 +226,10 @@ impl Widget for Scroll {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.control_size = control_size;
|
||||||
|
self.max_scroll
|
||||||
|
.update(Point::new(max_scroll_x, max_scroll_y));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure(
|
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();
|
context.set_needs_redraw();
|
||||||
|
|
||||||
// TODO make this only returned handled if we actually scrolled.
|
// TODO make this only returned handled if we actually scrolled.
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ where
|
||||||
if !handled {
|
if !handled {
|
||||||
match input.physical_key {
|
match input.physical_key {
|
||||||
KeyCode::KeyW
|
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) {
|
if self.request_close(&mut window) {
|
||||||
window.set_needs_redraw();
|
window.set_needs_redraw();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use crate::impl_all_tuples;
|
|
||||||
|
|
||||||
/// Invokes a function with a clone of `self`.
|
/// Invokes a function with a clone of `self`.
|
||||||
pub trait WithClone: Sized {
|
pub trait WithClone: Sized {
|
||||||
/// The type that results from cloning.
|
/// The type that results from cloning.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue