mirror of
https://github.com/danbulant/cushy
synced 2026-06-15 12:31:11 +00:00
Scroll and Animations
Scroll is only working to the absolute barest of requirements.
This commit is contained in:
parent
93a9545cc4
commit
79be9a063b
20 changed files with 1320 additions and 121 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -4,9 +4,9 @@ version = 3
|
|||
|
||||
[[package]]
|
||||
name = "ab_glyph"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1061f3ff92c2f65800df1f12fc7b4ff44ee14783104187dd04dfee6f11b0fd2"
|
||||
checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225"
|
||||
dependencies = [
|
||||
"ab_glyph_rasterizer",
|
||||
"owned_ttf_parser",
|
||||
|
|
@ -457,7 +457,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#5b00ee6772e8b59067bc69f2f4eeb06cc0cc62b7"
|
||||
source = "git+https://github.com/khonsulabs/figures#29a89d05f9fc8e81c5071fad49456329203d3bfc"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
@ -636,6 +636,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"alot",
|
||||
"interner",
|
||||
"kempt",
|
||||
"kludgine",
|
||||
]
|
||||
|
||||
|
|
@ -755,9 +756,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.2"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
||||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
|
|
@ -813,13 +814,19 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
version = "0.3.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
|
||||
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kempt"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8f97ab12df8bfb8a0a286ae251d3c46699c58bf5c2d280533e854e965965412"
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
|
|
@ -840,6 +847,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#35b83e14e71a02f3cd9a96e865094f5b3ad1407c"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -1241,11 +1249,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4"
|
||||
checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7"
|
||||
dependencies = [
|
||||
"ttf-parser 0.19.2",
|
||||
"ttf-parser 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1803,9 +1811,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
||||
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
|
|
@ -1813,9 +1821,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
||||
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
|
|
@ -1828,9 +1836,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
|
||||
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
|
|
@ -1840,9 +1848,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
||||
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -1850,9 +1858,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
||||
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1863,9 +1871,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.87"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
||||
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
|
|
@ -2322,9 +2330,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.17"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
|
||||
checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
@ -2420,18 +2428,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.18"
|
||||
version = "0.7.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901"
|
||||
checksum = "686b7e407015242119c33dab17b8f61ba6843534de936d94368856528eae4dcc"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.18"
|
||||
version = "0.7.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b"
|
||||
checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
12
Cargo.toml
12
Cargo.toml
|
|
@ -11,9 +11,15 @@ kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [
|
|||
] }
|
||||
alot = "0.3"
|
||||
interner = "0.2.1"
|
||||
# appit = { git = "https://github.com/khonsulabs/appit" }
|
||||
kempt = "0.2.1"
|
||||
|
||||
[patch."https://github.com/khonsulabs/kludgine"]
|
||||
kludgine = { path = "../kludgine2" }
|
||||
# [patch."https://github.com/khonsulabs/kludgine"]
|
||||
# kludgine = { path = "../kludgine2" }
|
||||
# [patch."https://github.com/khonsulabs/figures"]
|
||||
# figures = { path = "../figures" }
|
||||
|
||||
# [patch.crates-io]
|
||||
# kempt = { path = "../objectmap" }
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
|
|
|||
30
examples/animation.rs
Normal file
30
examples/animation.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use gooey::animation::{Animation, AnimationHandle, Spawn};
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widgets::{Button, Label, Stack};
|
||||
use gooey::{widgets, Run, WithClone};
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let animation = Dynamic::new(AnimationHandle::new());
|
||||
let value = Dynamic::new(50);
|
||||
let label = value.map_each(|value| value.to_string());
|
||||
Stack::columns(widgets![
|
||||
Button::new("To 0").on_click(animate_to(&animation, &value, 0)),
|
||||
Label::new(label),
|
||||
Button::new("To 100").on_click(animate_to(&animation, &value, 100)),
|
||||
])
|
||||
.run()
|
||||
}
|
||||
|
||||
fn animate_to(
|
||||
animation: &Dynamic<AnimationHandle>,
|
||||
value: &Dynamic<u8>,
|
||||
target: u8,
|
||||
) -> impl FnMut(()) {
|
||||
(animation, value).with_clone(|(animation, value)| {
|
||||
move |_| {
|
||||
animation.set(Animation::linear(value.clone(), target, Duration::from_secs(1)).spawn())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
use std::string::ToString;
|
||||
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::widgets::stack::Stack;
|
||||
use gooey::widgets::{Button, Label};
|
||||
use gooey::widgets::{Button, Label, Scroll, Stack};
|
||||
use gooey::{widgets, Run};
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let counter = Dynamic::new(0i32);
|
||||
let label = counter.map_each(ToString::to_string);
|
||||
Stack::rows(widgets![
|
||||
Scroll::new(Stack::rows(widgets![
|
||||
Label::new(label),
|
||||
Button::new("+").on_click(counter.with_clone(|counter| {
|
||||
move |_| {
|
||||
|
|
@ -20,6 +19,6 @@ fn main() -> gooey::Result {
|
|||
counter.set(counter.get() - 1);
|
||||
}
|
||||
})),
|
||||
])
|
||||
]))
|
||||
.run()
|
||||
}
|
||||
|
|
|
|||
21
examples/scroll.rs
Normal file
21
examples/scroll.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::Widgets;
|
||||
use gooey::widgets::{Button, Scroll, Stack};
|
||||
use gooey::Run;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
Scroll::new(Stack::rows(
|
||||
(0..30)
|
||||
.map(|i| {
|
||||
let count = Dynamic::new(0);
|
||||
|
||||
Button::new(count.map_each(move |count| format!("Row {i}: {count}"))).on_click(
|
||||
move |_| {
|
||||
count.map_mut(|count| *count += 1);
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Widgets>(),
|
||||
))
|
||||
.run()
|
||||
}
|
||||
509
src/animation.rs
Normal file
509
src/animation.rs
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
//! Types for creating animations.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{ControlFlow, Deref};
|
||||
use std::sync::{Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use alot::{LotId, Lots};
|
||||
use kempt::Set;
|
||||
|
||||
use crate::value::Dynamic;
|
||||
|
||||
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
|
||||
static NEW_ANIMATIONS: Condvar = Condvar::new();
|
||||
|
||||
fn thread_state() -> MutexGuard<'static, Animating> {
|
||||
static THREAD: OnceLock<()> = OnceLock::new();
|
||||
THREAD.get_or_init(|| {
|
||||
thread::spawn(animation_thread);
|
||||
});
|
||||
ANIMATIONS
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
}
|
||||
|
||||
fn animation_thread() {
|
||||
let mut state = thread_state();
|
||||
loop {
|
||||
if state.running.is_empty() {
|
||||
state.last_updated = None;
|
||||
state = NEW_ANIMATIONS
|
||||
.wait(state)
|
||||
.map_or_else(PoisonError::into_inner, |g| g);
|
||||
} else {
|
||||
let start = Instant::now();
|
||||
let last_tick = state.last_updated.unwrap_or(start);
|
||||
let elapsed = start - last_tick;
|
||||
state.last_updated = Some(start);
|
||||
|
||||
let mut index = 0;
|
||||
while index < state.running.len() {
|
||||
let animation_id = *state.running.member(index).expect("index in bounds");
|
||||
if state.animations[animation_id].animate(elapsed).is_break() {
|
||||
state.running.remove_member(index);
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
let next_tick = last_tick + Duration::from_millis(16);
|
||||
std::thread::sleep(
|
||||
next_tick
|
||||
.checked_duration_since(Instant::now())
|
||||
.unwrap_or(Duration::from_millis(16)),
|
||||
);
|
||||
state = thread_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Animating {
|
||||
animations: Lots<Box<dyn Animate>>,
|
||||
running: Set<LotId>,
|
||||
last_updated: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Animating {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
animations: Lots::new(),
|
||||
running: Set::new(),
|
||||
last_updated: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn(&mut self, animation: Box<dyn Animate>) -> AnimationHandle {
|
||||
let id = self.animations.push(animation);
|
||||
|
||||
if self.running.is_empty() {
|
||||
NEW_ANIMATIONS.notify_one();
|
||||
}
|
||||
|
||||
self.running.insert(id);
|
||||
|
||||
AnimationHandle(Some(id))
|
||||
}
|
||||
|
||||
fn remove_animation(&mut self, id: LotId) {
|
||||
self.animations.remove(id);
|
||||
self.running.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can animate.
|
||||
pub trait Animate: Send + Sync {
|
||||
/// Update the animation by progressing the timeline by `elapsed`.
|
||||
///
|
||||
/// When the animation is complete, return `ControlFlow::Break` with the
|
||||
/// remaining time that was not needed to complete the animation. This is
|
||||
/// used in multi-step animation processes to ensure time is accurately
|
||||
/// tracked.
|
||||
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration>;
|
||||
}
|
||||
|
||||
/// Describes a change to a new value for a [`Dynamic`] over a specified
|
||||
/// [`Duration`], using the `Easing` generic parameter to control how the value
|
||||
/// is interpolated.
|
||||
#[must_use = "animations are not performed until they are spawned"]
|
||||
pub struct Animation<T, Easing = Linear> {
|
||||
value: Dynamic<T>,
|
||||
end: T,
|
||||
duration: Duration,
|
||||
_easing: PhantomData<Easing>,
|
||||
}
|
||||
|
||||
impl<T> Animation<T, Linear>
|
||||
where
|
||||
T: LinearInterpolate + Clone + Send + Sync + 'static,
|
||||
{
|
||||
/// Returns a linearly interpolated animation that transitions `value` to
|
||||
/// `end_value` over `duration`.
|
||||
pub fn linear(value: Dynamic<T>, end_value: T, duration: Duration) -> Self {
|
||||
Self::new(value, end_value, duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Easing> Animation<T, Easing>
|
||||
where
|
||||
T: LinearInterpolate + Clone + Send + Sync + 'static,
|
||||
Easing: self::Easing,
|
||||
{
|
||||
/// Returns an animation that transitions `value` to `end_value` over
|
||||
/// `duration` using `Easing` for interpolation.
|
||||
pub fn new(value: Dynamic<T>, end_value: T, duration: Duration) -> Self {
|
||||
Self {
|
||||
value,
|
||||
end: end_value,
|
||||
duration,
|
||||
_easing: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Easing> IntoAnimate for Animation<T, Easing>
|
||||
where
|
||||
T: LinearInterpolate + Clone + Send + Sync + 'static,
|
||||
Easing: self::Easing,
|
||||
{
|
||||
type Animate = RunningAnimation<T, Easing>;
|
||||
|
||||
fn into_animate(self) -> Self::Animate {
|
||||
RunningAnimation {
|
||||
start: self.value.get(),
|
||||
animation: self,
|
||||
elapsed: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can convert into `Box<dyn Animate>`.
|
||||
pub trait BoxAnimate {
|
||||
/// Returns the boxed animation.
|
||||
fn boxed(self) -> Box<dyn Animate>;
|
||||
}
|
||||
|
||||
/// A type that can be converted into an animation.
|
||||
pub trait IntoAnimate: Sized + Send + Sync {
|
||||
/// The running animation type.
|
||||
type Animate: Animate;
|
||||
|
||||
/// Return this change as a running animation.
|
||||
fn into_animate(self) -> Self::Animate;
|
||||
|
||||
/// Returns an combined animation that performs `self` and `other` in
|
||||
/// sequence.
|
||||
fn chain<Other: IntoAnimate>(self, other: Other) -> Chain<Self, Other> {
|
||||
Chain::new(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BoxAnimate for T
|
||||
where
|
||||
T: IntoAnimate + 'static,
|
||||
{
|
||||
fn boxed(self) -> Box<dyn Animate> {
|
||||
Box::new(self.into_animate())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Animate`] implementor that has been boxed as a trait object.
|
||||
pub struct BoxedAnimation(Box<dyn Animate>);
|
||||
|
||||
/// An animation that can be spawned.
|
||||
pub trait Spawn {
|
||||
/// Spawns the animation, returning a handle that tracks the animation.
|
||||
///
|
||||
/// When the returned handle is dropped, the animation is stopped.
|
||||
fn spawn(self) -> AnimationHandle;
|
||||
}
|
||||
|
||||
impl<T> Spawn for T
|
||||
where
|
||||
T: BoxAnimate,
|
||||
{
|
||||
fn spawn(self) -> AnimationHandle {
|
||||
self.boxed().spawn()
|
||||
}
|
||||
}
|
||||
|
||||
impl Spawn for Box<dyn Animate> {
|
||||
fn spawn(self) -> AnimationHandle {
|
||||
thread_state().spawn(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Easing> Animate for RunningAnimation<T, Easing>
|
||||
where
|
||||
T: LinearInterpolate + Clone + Send + Sync,
|
||||
Easing: self::Easing,
|
||||
{
|
||||
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration> {
|
||||
self.elapsed = self.elapsed.checked_add(elapsed).unwrap_or(Duration::MAX);
|
||||
|
||||
if let Some(remaining_elapsed) = self.elapsed.checked_sub(self.animation.duration) {
|
||||
self.animation.value.set(self.animation.end.clone());
|
||||
ControlFlow::Break(remaining_elapsed)
|
||||
} else {
|
||||
let progress = Easing::ease(ZeroToOne::new(
|
||||
self.elapsed.as_secs_f32() / self.animation.duration.as_secs_f32(),
|
||||
));
|
||||
self.animation
|
||||
.value
|
||||
.set(self.start.lerp(&self.animation.end, progress));
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A running [`Animation`] that changes a [`Dynamic`] over a specified
|
||||
/// [`Duration`], using the `Easing` generic parameter to control how the value
|
||||
/// is interpolated.
|
||||
///
|
||||
/// The initial value for interpolation is recorded at the time this type is
|
||||
/// created: [`IntoAnimate::into_animate`]. [`Easing`] is used to customize how
|
||||
/// interpolation is performed.
|
||||
pub struct RunningAnimation<T, Easing> {
|
||||
animation: Animation<T, Easing>,
|
||||
start: T,
|
||||
elapsed: Duration,
|
||||
}
|
||||
|
||||
/// A handle to a spawned animation. When dropped, the associated animation will
|
||||
/// be stopped.
|
||||
#[derive(Default, Debug)]
|
||||
#[must_use]
|
||||
pub struct AnimationHandle(Option<LotId>);
|
||||
|
||||
impl AnimationHandle {
|
||||
/// Returns an empty handle that references no animation.
|
||||
pub const fn new() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
|
||||
/// Cancels the animation immediately.
|
||||
///
|
||||
/// This has the same effect as dropping the handle.
|
||||
pub fn clear(&mut self) {
|
||||
if let Some(id) = self.0.take() {
|
||||
thread_state().remove_animation(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AnimationHandle {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// An animation combinator that runs animation `A`, then animation `B`.
|
||||
pub struct Chain<A: IntoAnimate, B: IntoAnimate>(A, B);
|
||||
|
||||
/// A [`Chain`] that is currently animating.
|
||||
pub struct RunningChain<A: IntoAnimate, B: IntoAnimate>(Option<ChainState<A, B>>);
|
||||
|
||||
enum ChainState<A: IntoAnimate, B: IntoAnimate> {
|
||||
AnimatingFirst(A::Animate, B),
|
||||
AnimatingSecond(B::Animate),
|
||||
}
|
||||
|
||||
impl<A, B> Chain<A, B>
|
||||
where
|
||||
A: IntoAnimate,
|
||||
B: IntoAnimate,
|
||||
{
|
||||
/// Returns a new instance with `first` and `second`.
|
||||
pub const fn new(first: A, second: B) -> Self {
|
||||
Self(first, second)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> IntoAnimate for Chain<A, B>
|
||||
where
|
||||
A: IntoAnimate,
|
||||
B: IntoAnimate,
|
||||
{
|
||||
type Animate = RunningChain<A, B>;
|
||||
|
||||
fn into_animate(self) -> Self::Animate {
|
||||
let a = self.0.into_animate();
|
||||
RunningChain(Some(ChainState::AnimatingFirst(a, self.1)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Animate for RunningChain<A, B>
|
||||
where
|
||||
A: IntoAnimate,
|
||||
B: IntoAnimate,
|
||||
{
|
||||
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration> {
|
||||
match self.0.as_mut().expect("invalid state") {
|
||||
ChainState::AnimatingFirst(a, _) => match a.animate(elapsed) {
|
||||
ControlFlow::Continue(()) => ControlFlow::Continue(()),
|
||||
ControlFlow::Break(remaining) => {
|
||||
let Some(ChainState::AnimatingFirst(_, b)) = self.0.take() else {
|
||||
unreachable!("invalid state")
|
||||
};
|
||||
self.0 = Some(ChainState::AnimatingSecond(b.into_animate()));
|
||||
self.animate(remaining)
|
||||
}
|
||||
},
|
||||
ChainState::AnimatingSecond(b) => b.animate(elapsed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a linear interpolation between two values.
|
||||
pub trait LinearInterpolate {
|
||||
/// Interpolate linearly between `self` and `target` using `percent`.
|
||||
#[must_use]
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self;
|
||||
}
|
||||
|
||||
macro_rules! impl_lerp_for_int {
|
||||
($type:ident, $unsigned:ident, $float:ident) => {
|
||||
impl LinearInterpolate for $type {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
let percent = $float::from(percent);
|
||||
let delta = target.abs_diff(*self);
|
||||
let delta = (delta as $float * percent).round() as $unsigned;
|
||||
if target > self {
|
||||
self.checked_add_unsigned(delta).expect("direction checked")
|
||||
} else {
|
||||
self.checked_sub_unsigned(delta).expect("direction checked")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_lerp_for_uint {
|
||||
($type:ident, $float:ident) => {
|
||||
impl LinearInterpolate for $type {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
let percent = $float::from(percent);
|
||||
if let Some(delta) = target.checked_sub(*self) {
|
||||
*self + (delta as $float * percent).round() as $type
|
||||
} else {
|
||||
*self - ((*self - *target) as $float * percent).round() as $type
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_lerp_for_uint!(u8, f32);
|
||||
impl_lerp_for_uint!(u16, f32);
|
||||
impl_lerp_for_uint!(u32, f32);
|
||||
impl_lerp_for_uint!(u64, f32);
|
||||
impl_lerp_for_uint!(u128, f64);
|
||||
impl_lerp_for_uint!(usize, f64);
|
||||
impl_lerp_for_int!(i8, u8, f32);
|
||||
impl_lerp_for_int!(i16, u16, f32);
|
||||
impl_lerp_for_int!(i32, u32, f32);
|
||||
impl_lerp_for_int!(i64, u64, f32);
|
||||
impl_lerp_for_int!(i128, u128, f64);
|
||||
impl_lerp_for_int!(isize, usize, f64);
|
||||
|
||||
#[test]
|
||||
fn integer_lerps() {
|
||||
#[track_caller]
|
||||
fn test_lerps<T: LinearInterpolate + Debug + Eq>(a: &T, b: &T, mid: &T) {
|
||||
assert_eq!(&a.lerp(b, 1.), b);
|
||||
assert_eq!(&a.lerp(b, 0.), a);
|
||||
assert_eq!(&a.lerp(b, 0.5), mid);
|
||||
}
|
||||
|
||||
test_lerps(&u8::MIN, &u8::MAX, &128);
|
||||
test_lerps(&u16::MIN, &u16::MAX, &32_768);
|
||||
test_lerps(&u32::MIN, &u32::MAX, &2_147_483_648);
|
||||
test_lerps(&i8::MIN, &i8::MAX, &0);
|
||||
test_lerps(&i16::MIN, &i16::MAX, &0);
|
||||
test_lerps(&i32::MIN, &i32::MAX, &0);
|
||||
test_lerps(&i64::MIN, &i64::MAX, &0);
|
||||
test_lerps(&i128::MIN, &i128::MAX, &0);
|
||||
test_lerps(&isize::MIN, &isize::MAX, &0);
|
||||
}
|
||||
|
||||
/// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity.
|
||||
///
|
||||
/// Because of these restrictions, this type implements `Ord` and `Eq`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ZeroToOne(f32);
|
||||
|
||||
impl ZeroToOne {
|
||||
/// The maximum value this type can contain.
|
||||
pub const ONE: Self = Self(1.);
|
||||
/// The minimum type this type can contain.
|
||||
pub const ZERO: Self = Self(0.);
|
||||
|
||||
/// Returns a new instance after clamping `value` between +0.0 and 1.0.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if `value` is not a number.
|
||||
#[must_use]
|
||||
pub fn new(value: f32) -> Self {
|
||||
assert!(!value.is_nan());
|
||||
|
||||
Self(value.clamp(0., 1.))
|
||||
}
|
||||
|
||||
/// Returns the contained floating point value.
|
||||
#[must_use]
|
||||
pub fn into_f32(self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ZeroToOne {
|
||||
fn default() -> Self {
|
||||
Self::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ZeroToOne {
|
||||
type Target = f32;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ZeroToOne {}
|
||||
|
||||
impl PartialEq for ZeroToOne {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
*self == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<f32> for ZeroToOne {
|
||||
fn eq(&self, other: &f32) -> bool {
|
||||
(self.0 - *other).abs() < f32::EPSILON
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ZeroToOne {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.total_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ZeroToOne {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<f32> for ZeroToOne {
|
||||
fn partial_cmp(&self, other: &f32) -> Option<std::cmp::Ordering> {
|
||||
Some(self.0.total_cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl LinearInterpolate for ZeroToOne {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
let delta = **target - **self;
|
||||
ZeroToOne::new(**self + delta * percent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs easing for value interpolation.
|
||||
pub trait Easing: Send + Sync + 'static {
|
||||
/// Returns a ratio between 0.0 and 1.0 of
|
||||
fn ease(progress: ZeroToOne) -> f32;
|
||||
}
|
||||
|
||||
/// An [`Easing`] function that produces a steady, linear transition.
|
||||
pub enum Linear {}
|
||||
|
||||
impl Easing for Linear {
|
||||
fn ease(progress: ZeroToOne) -> f32 {
|
||||
*progress
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ use kludgine::app::winit::event::{
|
|||
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
||||
use kludgine::figures::{Point, Rect, Size};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::Kludgine;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ use crate::graphics::Graphics;
|
|||
use crate::styles::components::HighlightColor;
|
||||
use crate::styles::{ComponentDefaultvalue, Styles};
|
||||
use crate::value::Dynamic;
|
||||
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget};
|
||||
use crate::widget::{EventHandling, ManagedWidget, WidgetInstance};
|
||||
use crate::window::RunningWindow;
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
|
|
@ -126,25 +126,26 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
}
|
||||
|
||||
pub(crate) fn hover(&mut self, location: Point<Px>) {
|
||||
let newly_hovered = match self.current_node.tree.hover(Some(self.current_node)) {
|
||||
Ok(old_hover) => {
|
||||
if let Some(old_hover) = old_hover {
|
||||
let mut old_hover_context = self.for_other(&old_hover);
|
||||
old_hover.lock().unhover(&mut old_hover_context);
|
||||
}
|
||||
true
|
||||
if let Ok(changes) = self.current_node.tree.hover(Some(self.current_node)) {
|
||||
for unhovered in changes.unhovered {
|
||||
let mut context = self.for_other(&unhovered);
|
||||
unhovered.lock().unhover(&mut context);
|
||||
}
|
||||
for hover in changes.hovered {
|
||||
let mut context = self.for_other(&hover);
|
||||
hover.lock().hover(location, &mut context);
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
if newly_hovered {
|
||||
self.current_node.lock().hover(location, self);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_hover(&mut self) {
|
||||
if let Ok(Some(old_hover)) = self.current_node.tree.hover(None) {
|
||||
let mut old_hover_context = self.for_other(&old_hover);
|
||||
old_hover.lock().unhover(&mut old_hover_context);
|
||||
if let Ok(changes) = self.current_node.tree.hover(None) {
|
||||
assert!(changes.hovered.is_empty());
|
||||
|
||||
for old_hover in changes.unhovered {
|
||||
let mut old_hover_context = self.for_other(&old_hover);
|
||||
old_hover.lock().unhover(&mut old_hover_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,8 +255,24 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a new `GraphicsContext` that allows invoking graphics functions
|
||||
/// for `widget` and restricts the drawing area to `region`.
|
||||
///
|
||||
/// This is equivalent to calling
|
||||
/// `self.for_other(widget).clipped_to(region)`.
|
||||
pub fn for_child<'child>(
|
||||
&'child mut self,
|
||||
widget: &'child ManagedWidget,
|
||||
region: Rect<Px>,
|
||||
) -> GraphicsContext<'child, 'window, 'child, 'gfx, 'pass> {
|
||||
GraphicsContext {
|
||||
widget: self.widget.for_other(widget),
|
||||
graphics: Exclusive::Owned(self.graphics.clipped_to(region)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new graphics context that renders to the `clip` rectangle.
|
||||
pub fn clipped_to(&mut self, clip: Rect<UPx>) -> GraphicsContext<'_, 'window, '_, 'gfx, 'pass> {
|
||||
pub fn clipped_to(&mut self, clip: Rect<Px>) -> GraphicsContext<'_, 'window, '_, 'gfx, 'pass> {
|
||||
GraphicsContext {
|
||||
widget: self.widget.borrowed(),
|
||||
graphics: Exclusive::Owned(self.graphics.clipped_to(clip)),
|
||||
|
|
@ -267,7 +284,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
/// To ensure the correct color is used, include [`HighlightColor`] in the
|
||||
/// styles request.
|
||||
pub fn draw_focus_ring_using(&mut self, styles: &Styles) {
|
||||
let visible_rect = Rect::from(self.graphics.size() - (UPx(1), UPx(1)));
|
||||
let visible_rect = Rect::from(self.graphics.region().size - (Px(1), Px(1)));
|
||||
let focus_ring = Shape::stroked_rect(
|
||||
visible_rect,
|
||||
styles.get_or_default(&HighlightColor),
|
||||
|
|
@ -291,11 +308,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
/// Invokes [`Widget::redraw()`](crate::widget::Widget::redraw) on this
|
||||
/// context's widget.
|
||||
pub fn redraw(&mut self) {
|
||||
// TODO this should not use clip_rect, because it forces UPx, and once
|
||||
// we have scrolling, we can have negative offsets of rectangles where
|
||||
// it's clipped partially.
|
||||
self.current_node
|
||||
.note_rendered_rect(self.graphics.clip_rect().into_signed());
|
||||
self.current_node.note_rendered_rect(self.graphics.region());
|
||||
self.current_node.lock().redraw(self);
|
||||
}
|
||||
}
|
||||
|
|
@ -326,7 +339,7 @@ pub trait AsEventContext<'window> {
|
|||
/// Pushes a new child widget into the widget hierarchy beneathq the
|
||||
/// context's widget.
|
||||
#[must_use]
|
||||
fn push_child(&mut self, child: BoxedWidget) -> ManagedWidget {
|
||||
fn push_child(&mut self, child: WidgetInstance) -> ManagedWidget {
|
||||
let mut context = self.as_event_context();
|
||||
let pushed_widget = context
|
||||
.current_node
|
||||
|
|
@ -502,12 +515,19 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
self.pending_state.active.as_ref() == Some(self.current_node)
|
||||
}
|
||||
|
||||
/// Returns true if this widget is currently hovered.
|
||||
/// Returns true if this widget is currently hovered, even if the cursor is
|
||||
/// over a child widget.
|
||||
#[must_use]
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.current_node.hovered()
|
||||
}
|
||||
|
||||
/// Returns true if this widget that is directly beneath the cursor.
|
||||
#[must_use]
|
||||
pub fn primary_hover(&self) -> bool {
|
||||
self.current_node.primary_hover()
|
||||
}
|
||||
|
||||
/// Returns true if this widget is currently focused for user input.
|
||||
#[must_use]
|
||||
pub fn focused(&self) -> bool {
|
||||
|
|
|
|||
252
src/graphics.rs
252
src/graphics.rs
|
|
@ -1,13 +1,21 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::Rect;
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{
|
||||
self, Angle, Fraction, IntoSigned, IntoUnsigned, IsZero, Point, Rect, ScreenScale, ScreenUnit,
|
||||
Size,
|
||||
};
|
||||
use kludgine::render::Renderer;
|
||||
use kludgine::ClipGuard;
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::text::{MeasuredText, Text, TextOrigin};
|
||||
use kludgine::{
|
||||
cosmic_text, ClipGuard, Color, Kludgine, ShaderScalable, ShapeSource, TextureSource,
|
||||
};
|
||||
|
||||
/// A 2d graphics context
|
||||
pub struct Graphics<'clip, 'gfx, 'pass> {
|
||||
renderer: RenderContext<'clip, 'gfx, 'pass>,
|
||||
region: Rect<Px>,
|
||||
}
|
||||
|
||||
enum RenderContext<'clip, 'gfx, 'pass> {
|
||||
|
|
@ -20,10 +28,48 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
|
|||
#[must_use]
|
||||
pub fn new(renderer: Renderer<'gfx, 'pass>) -> Self {
|
||||
Self {
|
||||
region: renderer.clip_rect().into_signed(),
|
||||
renderer: RenderContext::Renderer(renderer),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the offset relative to the clipping rect that the graphics
|
||||
/// context renders at.
|
||||
///
|
||||
/// This is used when rendering controls that are partially offscreen to the
|
||||
/// left or top of the window's origin.
|
||||
///
|
||||
/// In general, this is handled automatically. This function should only be
|
||||
/// needed when using [`inner_graphics()`](Self::inner_graphics).
|
||||
#[must_use]
|
||||
pub fn translation(&self) -> Point<Px> {
|
||||
let clip_origin = self.renderer.clip_rect().origin.into_signed();
|
||||
-Point::new(
|
||||
if clip_origin.x <= self.region.origin.x {
|
||||
Px(0)
|
||||
} else {
|
||||
clip_origin.x - self.region.origin.x
|
||||
},
|
||||
if clip_origin.y <= self.region.origin.y {
|
||||
Px(0)
|
||||
} else {
|
||||
clip_origin.y - self.region.origin.y
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the underlying renderer.
|
||||
///
|
||||
/// Note: Kludgine graphics contexts only support clipping. This type adds
|
||||
/// [`self.translation()`](Self::translation) to the offset of each drawing
|
||||
/// call. When using the underlying renderer, any drawing calls will need
|
||||
/// this offset as well, otherwise the widget that is being rendered will
|
||||
/// not render correctly when placed in a [`Scroll`](crate::widgets::Scroll)
|
||||
/// widget.
|
||||
pub fn inner_graphics(&mut self) -> &mut Renderer<'gfx, 'pass> {
|
||||
&mut self.renderer
|
||||
}
|
||||
|
||||
/// Returns a context that has been clipped to `clip`.
|
||||
///
|
||||
/// The new clipping rectangle is interpreted relative to the current
|
||||
|
|
@ -32,27 +78,217 @@ impl<'clip, 'gfx, 'pass> Graphics<'clip, 'gfx, 'pass> {
|
|||
///
|
||||
/// The returned context will report the clipped size, and all drawing
|
||||
/// operations will be relative to the origin of `clip`.
|
||||
pub fn clipped_to(&mut self, clip: Rect<UPx>) -> Graphics<'_, 'gfx, 'pass> {
|
||||
pub fn clipped_to(&mut self, clip: Rect<Px>) -> Graphics<'_, 'gfx, 'pass> {
|
||||
let region = clip + self.region.origin;
|
||||
let new_clip = self
|
||||
.renderer
|
||||
.clip_rect()
|
||||
.intersection(®ion.into_unsigned())
|
||||
.map(|intersection| intersection - self.renderer.clip_rect().origin)
|
||||
.unwrap_or_default();
|
||||
|
||||
Graphics {
|
||||
renderer: RenderContext::Clipped(self.deref_mut().clipped_to(clip)),
|
||||
renderer: RenderContext::Clipped(self.renderer.clipped_to(new_clip)),
|
||||
region,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current clipping rectangle.
|
||||
///
|
||||
/// The clipping rectangle is represented in unsigned pixels in the window's
|
||||
/// coordinate system.
|
||||
#[must_use]
|
||||
pub fn clip_rect(&self) -> Rect<UPx> {
|
||||
self.renderer.clip_rect()
|
||||
}
|
||||
|
||||
/// Returns the current region being rendered to.
|
||||
///
|
||||
/// The rendering region utilizes signed pixels, which allows it to
|
||||
/// represent regions that are out of bounds of the window's visible region.
|
||||
#[must_use]
|
||||
pub fn region(&self) -> Rect<Px> {
|
||||
self.region
|
||||
}
|
||||
|
||||
/// Returns the visible region of the graphics context.
|
||||
///
|
||||
/// This is the intersection of [`Self::region()`] and
|
||||
/// [`Self::clip_rect()`].
|
||||
#[must_use]
|
||||
pub fn visible_rect(&self) -> Option<Rect<UPx>> {
|
||||
self.clip_rect().intersection(&self.region.into_unsigned())
|
||||
}
|
||||
|
||||
/// Returns the size of the current region.
|
||||
///
|
||||
/// This is `self.region().size` converted to unsigned pixels.
|
||||
#[must_use]
|
||||
pub fn size(&self) -> Size<UPx> {
|
||||
self.region.size.into_unsigned()
|
||||
}
|
||||
|
||||
/// Returns the current DPI scaling factor applied to the window this
|
||||
/// context is attached to.
|
||||
#[must_use]
|
||||
pub fn scale(&self) -> Fraction {
|
||||
self.renderer.scale()
|
||||
}
|
||||
|
||||
/// Draws a shape at the origin, rotating and scaling as needed.
|
||||
pub fn draw_shape<Unit>(
|
||||
&mut self,
|
||||
shape: &Shape<Unit, false>,
|
||||
origin: Point<Unit>,
|
||||
rotation_rads: Option<Angle>,
|
||||
scale: Option<f32>,
|
||||
) where
|
||||
Unit: IsZero + ShaderScalable + figures::ScreenUnit + Copy,
|
||||
{
|
||||
let translate = origin + Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer
|
||||
.draw_shape(shape, translate, rotation_rads, scale);
|
||||
}
|
||||
|
||||
/// Draws `texture` at `destination`, scaling as necessary.
|
||||
pub fn draw_texture<Unit>(&mut self, texture: &impl TextureSource, destination: Rect<Unit>)
|
||||
where
|
||||
Unit: figures::ScreenUnit + ShaderScalable,
|
||||
i32: From<<Unit as IntoSigned>::Signed>,
|
||||
{
|
||||
let translate = Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer.draw_texture(texture, destination + translate);
|
||||
}
|
||||
|
||||
/// Draws a shape that was created with texture coordinates, applying the
|
||||
/// provided texture.
|
||||
pub fn draw_textured_shape<Unit>(
|
||||
&mut self,
|
||||
shape: &impl ShapeSource<Unit, true>,
|
||||
texture: &impl TextureSource,
|
||||
origin: Point<Unit>,
|
||||
rotation: Option<Angle>,
|
||||
scale: Option<f32>,
|
||||
) where
|
||||
Unit: IsZero + ShaderScalable + figures::ScreenUnit + Copy,
|
||||
i32: From<<Unit as IntoSigned>::Signed>,
|
||||
{
|
||||
let translate = origin + Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer
|
||||
.draw_textured_shape(shape, texture, translate, rotation, scale);
|
||||
}
|
||||
|
||||
/// Measures `text` using the current text settings.
|
||||
///
|
||||
/// `default_color` does not affect the
|
||||
pub fn measure_text<'a, Unit>(&mut self, text: impl Into<Text<'a, Unit>>) -> MeasuredText<Unit>
|
||||
where
|
||||
Unit: figures::ScreenUnit,
|
||||
{
|
||||
self.renderer.measure_text(text)
|
||||
}
|
||||
|
||||
/// Draws `text` using the current text settings.
|
||||
pub fn draw_text<'a, Unit>(
|
||||
&mut self,
|
||||
text: impl Into<Text<'a, Unit>>,
|
||||
translate: Point<Unit>,
|
||||
rotation: Option<Angle>,
|
||||
scale: Option<f32>,
|
||||
) where
|
||||
Unit: ScreenUnit,
|
||||
{
|
||||
let translate = translate + Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer.draw_text(text, translate, rotation, scale);
|
||||
}
|
||||
|
||||
/// Prepares the text layout contained in `buffer` to be rendered.
|
||||
///
|
||||
/// When the text in `buffer` has no color defined, `default_color` will be
|
||||
/// used.
|
||||
///
|
||||
/// `origin` allows controlling how the text will be drawn relative to the
|
||||
/// coordinate provided in [`render()`](crate::PreparedGraphic::render).
|
||||
pub fn draw_text_buffer<Unit>(
|
||||
&mut self,
|
||||
buffer: &cosmic_text::Buffer,
|
||||
default_color: Color,
|
||||
origin: TextOrigin<Px>,
|
||||
translate: Point<Unit>,
|
||||
rotation: Option<Angle>,
|
||||
scale: Option<f32>,
|
||||
) where
|
||||
Unit: ScreenUnit,
|
||||
{
|
||||
let translate = translate + Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer
|
||||
.draw_text_buffer(buffer, default_color, origin, translate, rotation, scale);
|
||||
}
|
||||
|
||||
/// Measures `buffer` and caches the results using `default_color` when
|
||||
/// the buffer has no color associated with text.
|
||||
pub fn measure_text_buffer<Unit>(
|
||||
&mut self,
|
||||
buffer: &cosmic_text::Buffer,
|
||||
default_color: Color,
|
||||
) -> MeasuredText<Unit>
|
||||
where
|
||||
Unit: figures::ScreenUnit,
|
||||
{
|
||||
self.renderer.measure_text_buffer(buffer, default_color)
|
||||
}
|
||||
|
||||
/// Prepares the text layout contained in `buffer` to be rendered.
|
||||
///
|
||||
/// When the text in `buffer` has no color defined, `default_color` will be
|
||||
/// used.
|
||||
///
|
||||
/// `origin` allows controlling how the text will be drawn relative to the
|
||||
/// coordinate provided in [`render()`](crate::PreparedGraphic::render).
|
||||
pub fn draw_measured_text<Unit>(
|
||||
&mut self,
|
||||
text: &MeasuredText<Unit>,
|
||||
origin: TextOrigin<Unit>,
|
||||
translate: Point<Unit>,
|
||||
rotation: Option<Angle>,
|
||||
scale: Option<f32>,
|
||||
) where
|
||||
Unit: ScreenUnit,
|
||||
{
|
||||
let translate = translate + Point::<Unit>::from_px(self.translation(), self.scale());
|
||||
self.renderer
|
||||
.draw_measured_text(text, origin, translate, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gfx, 'pass> Deref for Graphics<'_, 'gfx, 'pass> {
|
||||
type Target = Kludgine;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.renderer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gfx, 'pass> DerefMut for Graphics<'_, 'gfx, 'pass> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.renderer
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gfx, 'pass> Deref for RenderContext<'_, 'gfx, 'pass> {
|
||||
type Target = Renderer<'gfx, 'pass>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self.renderer {
|
||||
match self {
|
||||
RenderContext::Renderer(renderer) => renderer,
|
||||
RenderContext::Clipped(clipped) => clipped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gfx, 'pass> DerefMut for Graphics<'_, 'gfx, 'pass> {
|
||||
impl<'gfx, 'pass> DerefMut for RenderContext<'_, 'gfx, 'pass> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
match &mut self.renderer {
|
||||
match self {
|
||||
RenderContext::Renderer(renderer) => renderer,
|
||||
RenderContext::Clipped(clipped) => &mut *clipped,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#![warn(clippy::pedantic, missing_docs)]
|
||||
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
||||
|
||||
pub mod animation;
|
||||
pub mod context;
|
||||
mod graphics;
|
||||
mod names;
|
||||
|
|
@ -13,6 +14,8 @@ pub mod value;
|
|||
pub mod widget;
|
||||
pub mod widgets;
|
||||
pub mod window;
|
||||
pub use with_clone::WithClone;
|
||||
mod with_clone;
|
||||
|
||||
pub use kludgine;
|
||||
use kludgine::app::winit::error::EventLoopError;
|
||||
|
|
@ -23,7 +26,7 @@ pub use self::graphics::Graphics;
|
|||
pub use self::tick::{InputState, Tick};
|
||||
|
||||
/// A limit used when measuring a widget.
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum ConstraintLimit {
|
||||
/// The widget is expected to occupy a known size.
|
||||
Known(UPx),
|
||||
|
|
|
|||
84
src/tree.rs
84
src/tree.rs
|
|
@ -7,7 +7,7 @@ use kludgine::figures::units::Px;
|
|||
use kludgine::figures::{Point, Rect};
|
||||
|
||||
use crate::styles::{ComponentDefaultvalue, Styles};
|
||||
use crate::widget::{BoxedWidget, ManagedWidget};
|
||||
use crate::widget::{ManagedWidget, WidgetInstance};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Tree {
|
||||
|
|
@ -15,7 +15,11 @@ pub struct Tree {
|
|||
}
|
||||
|
||||
impl Tree {
|
||||
pub fn push_boxed(&self, widget: BoxedWidget, parent: Option<&ManagedWidget>) -> ManagedWidget {
|
||||
pub fn push_boxed(
|
||||
&self,
|
||||
widget: WidgetInstance,
|
||||
parent: Option<&ManagedWidget>,
|
||||
) -> ManagedWidget {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let id = WidgetId(data.nodes.push(Node {
|
||||
widget: widget.clone(),
|
||||
|
|
@ -42,6 +46,7 @@ impl Tree {
|
|||
|
||||
pub(crate) fn note_rendered_rect(&self, widget: WidgetId, rect: Rect<Px>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
rect.extents();
|
||||
data.nodes[widget.0].last_rendered_location = Some(rect);
|
||||
data.render_order.push(widget);
|
||||
}
|
||||
|
|
@ -56,9 +61,31 @@ impl Tree {
|
|||
data.render_order.clear();
|
||||
}
|
||||
|
||||
pub fn hover(&self, new_hover: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
|
||||
pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> Result<HoverResults, ()> {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.update_tracked_widget(new_hover, self, |data| &mut data.hover)
|
||||
let mut hovered = new_hover
|
||||
.map(|new_hover| data.widget_hierarchy(new_hover.id, self))
|
||||
.unwrap_or_default();
|
||||
match data.update_tracked_widget(new_hover, self, |data| &mut data.hover)? {
|
||||
Some(old_hover) => {
|
||||
let mut old_hovered = data.widget_hierarchy(old_hover.id, self);
|
||||
// For any widgets that were shared, remove them, as they don't
|
||||
// need to have their events fired again.
|
||||
while !old_hovered.is_empty() && old_hovered.get(0) == hovered.get(0) {
|
||||
old_hovered.remove(0);
|
||||
hovered.remove(0);
|
||||
}
|
||||
|
||||
Ok(HoverResults {
|
||||
unhovered: old_hovered,
|
||||
hovered,
|
||||
})
|
||||
}
|
||||
None => Ok(HoverResults {
|
||||
unhovered: Vec::new(),
|
||||
hovered,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus(&self, new_focus: Option<&ManagedWidget>) -> Result<Option<ManagedWidget>, ()> {
|
||||
|
|
@ -76,11 +103,7 @@ impl Tree {
|
|||
|
||||
pub fn widget(&self, id: WidgetId) -> ManagedWidget {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
ManagedWidget {
|
||||
id,
|
||||
widget: data.nodes[id.0].widget.clone(),
|
||||
tree: self.clone(),
|
||||
}
|
||||
data.widget(id, self)
|
||||
}
|
||||
|
||||
pub fn active_widget(&self) -> Option<WidgetId> {
|
||||
|
|
@ -97,6 +120,19 @@ impl Tree {
|
|||
.hover
|
||||
}
|
||||
|
||||
pub fn is_hovered(&self, id: WidgetId) -> bool {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
let mut search = data.hover;
|
||||
while let Some(hovered) = search {
|
||||
if hovered == id {
|
||||
return true;
|
||||
}
|
||||
search = data.nodes[hovered.0].parent;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn focused_widget(&self) -> Option<WidgetId> {
|
||||
self.data
|
||||
.lock()
|
||||
|
|
@ -143,6 +179,11 @@ impl Tree {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HoverResults {
|
||||
pub unhovered: Vec<ManagedWidget>,
|
||||
pub hovered: Vec<ManagedWidget>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TreeData {
|
||||
nodes: Lots<Node>,
|
||||
|
|
@ -153,6 +194,14 @@ struct TreeData {
|
|||
}
|
||||
|
||||
impl TreeData {
|
||||
fn widget(&self, id: WidgetId, tree: &Tree) -> ManagedWidget {
|
||||
ManagedWidget {
|
||||
id,
|
||||
widget: self.nodes[id.0].widget.clone(),
|
||||
tree: tree.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_child(&mut self, child: WidgetId, parent: WidgetId) {
|
||||
let removed_node = self.nodes.remove(child.0).expect("widget already removed");
|
||||
let parent = &mut self.nodes[parent.0];
|
||||
|
|
@ -171,6 +220,21 @@ impl TreeData {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn widget_hierarchy(&self, mut widget: WidgetId, tree: &Tree) -> Vec<ManagedWidget> {
|
||||
let mut hierarchy = Vec::new();
|
||||
loop {
|
||||
hierarchy.push(self.widget(widget, tree));
|
||||
let Some(parent) = self.nodes[widget.0].parent else {
|
||||
break;
|
||||
};
|
||||
widget = parent;
|
||||
}
|
||||
|
||||
hierarchy.reverse();
|
||||
|
||||
hierarchy
|
||||
}
|
||||
|
||||
fn update_tracked_widget(
|
||||
&mut self,
|
||||
new_widget: Option<&ManagedWidget>,
|
||||
|
|
@ -218,7 +282,7 @@ impl TreeData {
|
|||
}
|
||||
|
||||
pub struct Node {
|
||||
pub widget: BoxedWidget,
|
||||
pub widget: WidgetInstance,
|
||||
pub children: Vec<WidgetId>,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub last_rendered_location: Option<Rect<Px>>,
|
||||
|
|
|
|||
|
|
@ -166,14 +166,14 @@ where
|
|||
T: Widget,
|
||||
{
|
||||
fn run(self) -> crate::Result {
|
||||
BoxedWidget::new(self).run()
|
||||
WidgetInstance::new(self).run()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can create a widget.
|
||||
pub trait MakeWidget: Sized {
|
||||
/// Returns a new widget.
|
||||
fn make_widget(self) -> BoxedWidget;
|
||||
fn make_widget(self) -> WidgetInstance;
|
||||
|
||||
/// Runs the widget this type creates as an application.
|
||||
fn run(self) -> crate::Result {
|
||||
|
|
@ -185,8 +185,8 @@ impl<T> MakeWidget for T
|
|||
where
|
||||
T: Widget,
|
||||
{
|
||||
fn make_widget(self) -> BoxedWidget {
|
||||
BoxedWidget::new(self)
|
||||
fn make_widget(self) -> WidgetInstance {
|
||||
WidgetInstance::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -209,9 +209,9 @@ pub const IGNORED: EventHandling = EventHandling::Continue(EventIgnored);
|
|||
|
||||
/// An instance of a [`Widget`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BoxedWidget(Arc<Mutex<dyn Widget>>);
|
||||
pub struct WidgetInstance(Arc<Mutex<dyn Widget>>);
|
||||
|
||||
impl BoxedWidget {
|
||||
impl WidgetInstance {
|
||||
/// Returns a new instance containing `widget`.
|
||||
pub fn new<W>(widget: W) -> Self
|
||||
where
|
||||
|
|
@ -225,28 +225,28 @@ impl BoxedWidget {
|
|||
}
|
||||
}
|
||||
|
||||
impl Run for BoxedWidget {
|
||||
impl Run for WidgetInstance {
|
||||
fn run(self) -> crate::Result {
|
||||
Window::<BoxedWidget>::new(self).run()
|
||||
Window::<WidgetInstance>::new(self).run()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for BoxedWidget {}
|
||||
impl Eq for WidgetInstance {}
|
||||
|
||||
impl PartialEq for BoxedWidget {
|
||||
impl PartialEq for WidgetInstance {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowBehavior for BoxedWidget {
|
||||
impl WindowBehavior for WidgetInstance {
|
||||
type Context = Self;
|
||||
|
||||
fn initialize(_window: &mut RunningWindow<'_>, context: Self::Context) -> Self {
|
||||
context
|
||||
}
|
||||
|
||||
fn make_root(&mut self) -> BoxedWidget {
|
||||
fn make_root(&mut self) -> WidgetInstance {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +297,7 @@ where
|
|||
#[derive(Clone)]
|
||||
pub struct ManagedWidget {
|
||||
pub(crate) id: WidgetId,
|
||||
pub(crate) widget: BoxedWidget,
|
||||
pub(crate) widget: WidgetInstance,
|
||||
pub(crate) tree: Tree,
|
||||
}
|
||||
|
||||
|
|
@ -334,6 +334,12 @@ impl ManagedWidget {
|
|||
/// Returns true if this widget is currently the hovered widget.
|
||||
#[must_use]
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.tree.is_hovered(self.id)
|
||||
}
|
||||
|
||||
/// Returns true if this widget that is directly beneath the cursor.
|
||||
#[must_use]
|
||||
pub fn primary_hover(&self) -> bool {
|
||||
self.tree.hovered_widget() == Some(self.id)
|
||||
}
|
||||
|
||||
|
|
@ -360,8 +366,8 @@ impl PartialEq for ManagedWidget {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq<BoxedWidget> for ManagedWidget {
|
||||
fn eq(&self, other: &BoxedWidget) -> bool {
|
||||
impl PartialEq<WidgetInstance> for ManagedWidget {
|
||||
fn eq(&self, other: &WidgetInstance) -> bool {
|
||||
&self.widget == other
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +376,7 @@ impl PartialEq<BoxedWidget> for ManagedWidget {
|
|||
#[derive(Debug, Default)]
|
||||
#[must_use]
|
||||
pub struct Widgets {
|
||||
ordered: Vec<BoxedWidget>,
|
||||
ordered: Vec<WidgetInstance>,
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
|
|
@ -431,7 +437,7 @@ where
|
|||
}
|
||||
|
||||
impl Deref for Widgets {
|
||||
type Target = [BoxedWidget];
|
||||
type Target = [WidgetInstance];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ordered
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ mod button;
|
|||
mod canvas;
|
||||
mod input;
|
||||
mod label;
|
||||
mod scroll;
|
||||
pub mod stack;
|
||||
mod style;
|
||||
mod tilemap;
|
||||
|
|
@ -12,5 +13,7 @@ pub use button::Button;
|
|||
pub use canvas::Canvas;
|
||||
pub use input::Input;
|
||||
pub use label::Label;
|
||||
pub use scroll::Scroll;
|
||||
pub use stack::Stack;
|
||||
pub use style::Style;
|
||||
pub use tilemap::TileMap;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ impl Button {
|
|||
|
||||
impl Widget for Button {
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let center = Point::from(context.graphics.size()) / 2;
|
||||
let size = context.graphics.region().size;
|
||||
let center = Point::from(size) / 2;
|
||||
self.label.redraw_when_changed(context);
|
||||
|
||||
let styles = context.query_style(&[
|
||||
|
|
@ -68,7 +69,7 @@ impl Widget for Button {
|
|||
&ButtonHoverBackground,
|
||||
]);
|
||||
|
||||
let visible_rect = Rect::from(context.graphics.size() - (UPx(1), UPx(1)));
|
||||
let visible_rect = Rect::from(size - (Px(1), Px(1)));
|
||||
|
||||
let background = if context.active() {
|
||||
styles.get_or_default(&ButtonActiveBackground)
|
||||
|
|
@ -86,12 +87,11 @@ impl Widget for Button {
|
|||
context.draw_focus_ring_using(&styles);
|
||||
}
|
||||
|
||||
let width = context.graphics.size().width;
|
||||
self.label.map(|label| {
|
||||
context.graphics.draw_text(
|
||||
Text::new(label, styles.get_or_default(&TextColor))
|
||||
.origin(kludgine::text::TextOrigin::Center)
|
||||
.wrap_at(width),
|
||||
.wrap_at(size.width),
|
||||
center,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -27,14 +27,15 @@ impl Widget for Label {
|
|||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
self.text.redraw_when_changed(context);
|
||||
|
||||
let center = Point::from(context.graphics.size()) / 2;
|
||||
let size = context.graphics.region().size;
|
||||
let center = Point::from(size) / 2;
|
||||
let styles = context.query_style(&[&TextColor]);
|
||||
let width = context.graphics.size().width;
|
||||
|
||||
self.text.map(|contents| {
|
||||
context.graphics.draw_text(
|
||||
Text::new(contents, styles.get_or_default(&TextColor))
|
||||
.origin(TextOrigin::Center)
|
||||
.wrap_at(width),
|
||||
.wrap_at(size.width),
|
||||
center,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
242
src/widgets/scroll.rs
Normal file
242
src/widgets/scroll.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::utils::lossy_f64_to_f32;
|
||||
use kludgine::figures::{
|
||||
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::{Animation, AnimationHandle, Spawn, ZeroToOne};
|
||||
use crate::context::{AsEventContext, EventContext};
|
||||
use crate::styles::{
|
||||
ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent,
|
||||
};
|
||||
use crate::value::Dynamic;
|
||||
use crate::widget::{EventHandling, MakeWidget, ManagedWidget, Widget, WidgetInstance, HANDLED};
|
||||
use crate::{ConstraintLimit, Name};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ChildWidget {
|
||||
Instance(WidgetInstance),
|
||||
Managed(ManagedWidget),
|
||||
Mounting,
|
||||
}
|
||||
|
||||
impl ChildWidget {
|
||||
pub fn managed(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget {
|
||||
if matches!(self, ChildWidget::Instance(_)) {
|
||||
let ChildWidget::Instance(instance) = std::mem::replace(self, ChildWidget::Mounting)
|
||||
else {
|
||||
unreachable!("just matched")
|
||||
};
|
||||
*self = ChildWidget::Managed(context.push_child(instance));
|
||||
}
|
||||
let ChildWidget::Managed(managed) = self else {
|
||||
unreachable!("always converted")
|
||||
};
|
||||
managed.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that supports scrolling its contents.
|
||||
#[derive(Debug)]
|
||||
pub struct Scroll {
|
||||
contents: ChildWidget,
|
||||
content_size: Size<Px>,
|
||||
scroll: Point<Px>,
|
||||
max_scroll: 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 {
|
||||
Self {
|
||||
contents: ChildWidget::Instance(contents.make_widget()),
|
||||
content_size: Size::default(),
|
||||
scroll: Point::default(),
|
||||
max_scroll: Point::default(),
|
||||
scrollbar_opacity: Dynamic::default(),
|
||||
scrollbar_opacity_animation: AnimationHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constrain_scroll(&mut self) {
|
||||
self.scroll = self.scroll.max(self.max_scroll).min(Point::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scroll {
|
||||
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hover(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) {
|
||||
self.scrollbar_opacity_animation = Animation::linear(
|
||||
self.scrollbar_opacity.clone(),
|
||||
ZeroToOne::ONE,
|
||||
Duration::from_millis(300),
|
||||
)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
fn unhover(&mut self, _context: &mut EventContext<'_, '_>) {
|
||||
self.scrollbar_opacity_animation = Animation::linear(
|
||||
self.scrollbar_opacity.clone(),
|
||||
ZeroToOne::ZERO,
|
||||
Duration::from_millis(300),
|
||||
)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
context.redraw_when_changed(&self.scrollbar_opacity);
|
||||
self.constrain_scroll();
|
||||
let Some(visible_rect) = context.graphics.visible_rect() else {
|
||||
return;
|
||||
};
|
||||
let visible_bottom_right = visible_rect.into_signed().extent();
|
||||
let styles = context.query_style(&[&ScrollBarThickness]);
|
||||
let bar_width = styles
|
||||
.get_or_default(&ScrollBarThickness)
|
||||
.into_px(context.graphics.scale());
|
||||
|
||||
let max_extents = Size::new(
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.x.into_unsigned()),
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - self.scroll.y.into_unsigned()),
|
||||
);
|
||||
let managed = self.contents.managed(&mut context.as_event_context());
|
||||
self.content_size = context
|
||||
.for_other(&managed)
|
||||
.measure(max_extents)
|
||||
.into_signed();
|
||||
let control_size = context.graphics.region().size;
|
||||
|
||||
let region = Rect::new(
|
||||
self.scroll,
|
||||
self.content_size
|
||||
.min(Size::new(Px::MAX, Px::MAX) - self.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 {
|
||||
context.graphics.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(horizontal_bar.offset, control_size.height - bar_width),
|
||||
Size::new(horizontal_bar.size, bar_width),
|
||||
),
|
||||
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
|
||||
),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
context.graphics.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(visible_bottom_right.x - bar_width, vertical_bar.offset),
|
||||
Size::new(bar_width, vertical_bar.size),
|
||||
),
|
||||
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
|
||||
),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
available_space: Size<crate::ConstraintLimit>,
|
||||
_context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
Size::new(available_space.width.max(), available_space.height.max())
|
||||
}
|
||||
|
||||
fn mouse_wheel(
|
||||
&mut self,
|
||||
_device_id: DeviceId,
|
||||
delta: MouseScrollDelta,
|
||||
_phase: TouchPhase,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let amount = match delta {
|
||||
/* TODO query line height */
|
||||
MouseScrollDelta::LineDelta(x, y) => Point::new(x, y) * 16.0,
|
||||
MouseScrollDelta::PixelDelta(px) => {
|
||||
Point::new(lossy_f64_to_f32(px.x), lossy_f64_to_f32(px.y))
|
||||
}
|
||||
};
|
||||
|
||||
self.scroll += amount.cast();
|
||||
context.set_needs_redraw();
|
||||
|
||||
// TODO make this only returned handled if we actually scrolled.
|
||||
HANDLED
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarInfo {
|
||||
offset: Px,
|
||||
amount_hidden: Px,
|
||||
size: Px,
|
||||
}
|
||||
|
||||
fn scrollbar_region(scroll: Px, content_size: Px, control_size: Px) -> ScrollbarInfo {
|
||||
if content_size > control_size {
|
||||
let amount_hidden = content_size - control_size;
|
||||
let ratio_visible = control_size.into_float() / content_size.into_float();
|
||||
let bar_size = control_size * ratio_visible;
|
||||
let remaining_area = control_size - bar_size;
|
||||
let amount_scrolled = -scroll.into_float() / amount_hidden.into_float();
|
||||
let bar_offset = remaining_area * amount_scrolled;
|
||||
ScrollbarInfo {
|
||||
offset: bar_offset,
|
||||
amount_hidden,
|
||||
size: bar_size,
|
||||
}
|
||||
} else {
|
||||
ScrollbarInfo::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollBarThickness;
|
||||
|
||||
impl ComponentDefinition for ScrollBarThickness {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(9))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for ScrollBarThickness {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::named::<Scroll>("text_size"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentGroup for Scroll {
|
||||
fn name() -> Name {
|
||||
Name::new("Scroll")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ use std::ops::Deref;
|
|||
|
||||
use alot::{LotId, OrderedLots};
|
||||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Point, Rect, Size};
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
||||
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext};
|
||||
use crate::value::{Generation, IntoValue, Value};
|
||||
|
|
@ -117,13 +117,21 @@ impl Widget for Stack {
|
|||
for (index, layout) in self.layout.iter().enumerate() {
|
||||
let child = &self.synced_children[index];
|
||||
if layout.size > 0 {
|
||||
let mut clipped = context.clipped_to(Rect::new(
|
||||
self.layout.orientation.make_point(layout.offset, UPx(0)),
|
||||
self.layout
|
||||
.orientation
|
||||
.make_size(layout.size, self.layout.other),
|
||||
));
|
||||
clipped.for_other(child).redraw();
|
||||
context
|
||||
.for_child(
|
||||
child,
|
||||
Rect::new(
|
||||
self.layout
|
||||
.orientation
|
||||
.make_point(layout.offset, UPx(0))
|
||||
.into_signed(),
|
||||
self.layout
|
||||
.orientation
|
||||
.make_size(layout.size, self.layout.other)
|
||||
.into_signed(),
|
||||
),
|
||||
)
|
||||
.redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,7 +410,7 @@ impl Layout {
|
|||
ConstraintLimit::ClippedAfter(clip_limit) => self.other.min(clip_limit),
|
||||
};
|
||||
|
||||
self.orientation.make_size(available_space, self.other)
|
||||
self.orientation.make_size(offset, self.other)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ use kludgine::figures::Size;
|
|||
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext};
|
||||
use crate::styles::Styles;
|
||||
use crate::widget::{BoxedWidget, ManagedWidget, Widget};
|
||||
use crate::widget::{ManagedWidget, Widget, WidgetInstance};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
||||
#[derive(Debug)]
|
||||
pub struct Style {
|
||||
styles: Styles,
|
||||
child: BoxedWidget,
|
||||
child: WidgetInstance,
|
||||
mounted_child: Option<ManagedWidget>,
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ impl Style {
|
|||
pub fn new(styles: impl Into<Styles>, child: impl Widget) -> Self {
|
||||
Self {
|
||||
styles: styles.into(),
|
||||
child: BoxedWidget::new(child),
|
||||
child: WidgetInstance::new(child),
|
||||
mounted_child: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,8 +65,10 @@ where
|
|||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let focus = self.focus.get();
|
||||
self.layers
|
||||
.map(|layers| tilemap::draw(layers, focus, self.zoom, &mut context.graphics));
|
||||
// TODO this needs to be updated to support being placed in side of a scroll view.
|
||||
self.layers.map(|layers| {
|
||||
tilemap::draw(layers, focus, self.zoom, context.graphics.inner_graphics());
|
||||
});
|
||||
|
||||
if let Some(tick) = &self.tick {
|
||||
tick.rendered(context);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ use crate::context::{EventContext, Exclusive, GraphicsContext, WidgetContext};
|
|||
use crate::graphics::Graphics;
|
||||
use crate::tree::Tree;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::widget::{BoxedWidget, EventHandling, ManagedWidget, Widget, HANDLED, IGNORED};
|
||||
use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::Run;
|
||||
|
||||
|
|
@ -52,13 +52,13 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl Window<BoxedWidget> {
|
||||
impl Window<WidgetInstance> {
|
||||
/// Returns a new instance using `widget` as its contents.
|
||||
pub fn for_widget<W>(widget: W) -> Self
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
Self::new(BoxedWidget::new(widget))
|
||||
Self::new(WidgetInstance::new(widget))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ pub trait WindowBehavior: Sized + 'static {
|
|||
fn initialize(window: &mut RunningWindow<'_>, context: Self::Context) -> Self;
|
||||
|
||||
/// Create the window's root widget. This function is only invoked once.
|
||||
fn make_root(&mut self) -> BoxedWidget;
|
||||
fn make_root(&mut self) -> WidgetInstance;
|
||||
|
||||
/// The window has been requested to close. If this function returns true,
|
||||
/// the window will be closed. Returning false prevents the window from
|
||||
|
|
@ -422,6 +422,8 @@ where
|
|||
) {
|
||||
match event {
|
||||
WindowCommand::Redraw => {
|
||||
// TODO we should attempt to batch redraw events so that we're
|
||||
// not constantly sending them from animations.
|
||||
window.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
src/with_clone.rs
Normal file
39
src/with_clone.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/// Invokes a function with a clone of `self`.
|
||||
pub trait WithClone: Sized {
|
||||
/// The type that results from cloning.
|
||||
type Cloned;
|
||||
|
||||
/// Maps `with` with the results of cloning `self`.
|
||||
fn with_clone<R>(&self, with: impl FnOnce(Self::Cloned) -> R) -> R;
|
||||
}
|
||||
|
||||
macro_rules! impl_with_clone {
|
||||
($($name:ident $field:tt),+) => {
|
||||
impl<'a, $($name: Clone,)+> WithClone for ($(&'a $name,)+)
|
||||
{
|
||||
type Cloned = ($($name,)+);
|
||||
|
||||
fn with_clone<R>(&self, with: impl FnOnce(Self::Cloned) -> R) -> R {
|
||||
with(($(self.$field.clone(),)+))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a, T> WithClone for &'a T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Cloned = T;
|
||||
|
||||
fn with_clone<R>(&self, with: impl FnOnce(Self::Cloned) -> R) -> R {
|
||||
with((*self).clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl_with_clone!(T1 0);
|
||||
impl_with_clone!(T1 0, T2 1);
|
||||
impl_with_clone!(T1 0, T2 1, T3 2);
|
||||
impl_with_clone!(T1 0, T2 1, T3 2, T4 3);
|
||||
impl_with_clone!(T1 0, T2 1, T3 2, T4 3, T5 4);
|
||||
impl_with_clone!(T1 0, T2 1, T3 2, T4 3, T5 4, T6 5);
|
||||
Loading…
Reference in a new issue