Scroll and Animations

Scroll is only working to the absolute barest of requirements.
This commit is contained in:
Jonathan Johnson 2023-11-01 15:15:14 -07:00
parent 93a9545cc4
commit 79be9a063b
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
20 changed files with 1320 additions and 121 deletions

64
Cargo.lock generated
View file

@ -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",

View file

@ -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
View 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())
}
})
}

View file

@ -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
View 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
View 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
}
}

View file

@ -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 {

View file

@ -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(&region.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,
}

View file

@ -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),

View file

@ -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>>,

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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
View 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")
}
}

View file

@ -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)
}
}

View file

@ -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,
}
}

View file

@ -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);

View file

@ -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
View 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);