Tuple animations

This commit is contained in:
Jonathan Johnson 2023-11-02 07:48:30 -07:00
parent 64f46a46e2
commit ed31805693
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
10 changed files with 310 additions and 101 deletions

12
Cargo.lock generated
View file

@ -383,9 +383,9 @@ dependencies = [
[[package]]
name = "cursor-icon"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740bb192a8e2d1350119916954f4409ee7f62f149b536911eeb78ba5a20526bf"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]]
name = "d3d12"
@ -2428,18 +2428,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697"
[[package]]
name = "zerocopy"
version = "0.7.21"
version = "0.7.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686b7e407015242119c33dab17b8f61ba6843534de936d94368856528eae4dcc"
checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.21"
version = "0.7.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806"
checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,6 +1,6 @@
use std::time::Duration;
use gooey::animation::{Animation, AnimationHandle, Spawn};
use gooey::animation::{AnimationHandle, AnimationTarget, Spawn};
use gooey::value::Dynamic;
use gooey::widgets::{Button, Label, Stack};
use gooey::{widgets, Run, WithClone};
@ -24,7 +24,12 @@ fn animate_to(
) -> impl FnMut(()) {
(animation, value).with_clone(|(animation, value)| {
move |_| {
animation.set(Animation::linear(value.clone(), target, Duration::from_secs(1)).spawn())
animation.set(
value
.transition_to(target)
.over(Duration::from_secs(1))
.spawn(),
)
}
})
}

View file

@ -11,6 +11,7 @@ use alot::{LotId, Lots};
use kempt::Set;
use kludgine::Color;
use crate::impl_all_tuples;
use crate::value::Dynamic;
static ANIMATIONS: Mutex<Animating> = Mutex::new(Animating::new());
@ -106,61 +107,137 @@ pub trait Animate: Send + Sync {
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration>;
}
/// A pending transition for a [`Dynamic`] to a new value.
pub struct DynamicTransition<T> {
/// The dynamic value to change.
pub dynamic: Dynamic<T>,
/// The final value to store in the [`Dynamic`].
pub new_value: T,
}
impl<T> AnimationTarget for DynamicTransition<T>
where
T: LinearInterpolate + Clone + Send + Sync,
{
type Running = TransitioningDynamic<T>;
fn begin(self) -> Self::Running {
self.into()
}
}
/// A [`DynamicTransition`] that has begun its transition.
pub struct TransitioningDynamic<T> {
change: DynamicTransition<T>,
start: T,
}
impl<T> From<DynamicTransition<T>> for TransitioningDynamic<T>
where
T: Clone,
{
fn from(change: DynamicTransition<T>) -> Self {
Self {
start: change.dynamic.get(),
change,
}
}
}
impl<T> AnimateTarget for TransitioningDynamic<T>
where
T: LinearInterpolate + Clone + Send + Sync,
{
fn update(&self, percent: f32) {
self.change
.dynamic
.set(self.start.lerp(&self.change.new_value, percent));
}
fn finish(&self) {
self.change.dynamic.set(self.change.new_value.clone());
}
}
/// 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,
pub struct Animation<Target, Easing = Linear>
where
Target: AnimationTarget,
{
value: Target,
duration: Duration,
_easing: PhantomData<Easing>,
}
impl<T> Animation<T, Linear>
where
T: LinearInterpolate + Clone + Send + Sync + 'static,
T: AnimationTarget,
{
/// 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 {
fn new(value: T, duration: Duration) -> Self {
Self {
value,
end: end_value,
duration,
_easing: PhantomData,
}
}
/// Returns this animation with a different easing function.
pub fn with_easing<Easing: self::Easing>(self) -> Animation<T, Easing> {
Animation {
value: self.value,
duration: self.duration,
_easing: PhantomData,
}
}
}
impl<T, Easing> IntoAnimate for Animation<T, Easing>
where
T: LinearInterpolate + Clone + Send + Sync + 'static,
T: AnimationTarget,
Easing: self::Easing,
{
type Animate = RunningAnimation<T, Easing>;
type Animate = RunningAnimation<T::Running, Easing>;
fn into_animate(self) -> Self::Animate {
RunningAnimation {
start: self.value.get(),
animation: self,
target: self.value.begin(),
duration: self.duration,
elapsed: Duration::ZERO,
_easing: PhantomData,
}
}
}
/// A target for a timed [`Animation`].
pub trait AnimationTarget: Sized + Send + Sync {
/// The type that can linearly interpolate this target.
type Running: AnimateTarget;
/// Record the current value of the target, and return a type that can
/// linearly interpolate between the current value and the desired value.
fn begin(self) -> Self::Running;
/// Returns a pending animation that linearly transitions `self` over
/// `duration`.
///
/// A different [`Easing`] can be used by calling
/// [`with_easing`](Animation::with_easing) on the result of this function.
fn over(self, duration: Duration) -> Animation<Self, Linear> {
Animation::new(self, duration)
}
}
/// The target of an [`Animate`] implementor.
pub trait AnimateTarget: Send + Sync {
/// Updates the target with linear interpolation.
fn update(&self, percent: f32);
/// Sets the target to the desired completion state.
fn finish(&self);
}
/// A type that can convert into `Box<dyn Animate>`.
pub trait BoxAnimate {
/// Returns the boxed animation.
@ -177,11 +254,35 @@ pub trait IntoAnimate: Sized + Send + Sync {
/// Returns an combined animation that performs `self` and `other` in
/// sequence.
fn chain<Other: IntoAnimate>(self, other: Other) -> Chain<Self, Other> {
fn and_then<Other: IntoAnimate>(self, other: Other) -> Chain<Self, Other> {
Chain::new(self, other)
}
}
macro_rules! impl_tuple_animate {
($($type:ident $field:tt),+) => {
impl<$($type),+> AnimationTarget for ($($type,)+) where $($type: AnimationTarget),+ {
type Running = ($(<$type>::Running,)+);
fn begin(self) -> Self::Running {
($(self.$field.begin(),)+)
}
}
impl<$($type),+> AnimateTarget for ($($type,)+) where $($type: AnimateTarget),+ {
fn update(&self, percent: f32) {
$(self.$field.update(percent);)+
}
fn finish(&self) {
$(self.$field.finish();)+
}
}
}
}
impl_all_tuples!(impl_tuple_animate);
impl<T> BoxAnimate for T
where
T: IntoAnimate + 'static,
@ -219,22 +320,20 @@ impl Spawn for Box<dyn Animate> {
impl<T, Easing> Animate for RunningAnimation<T, Easing>
where
T: LinearInterpolate + Clone + Send + Sync,
T: AnimateTarget,
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());
if let Some(remaining_elapsed) = self.elapsed.checked_sub(self.duration) {
self.target.finish();
ControlFlow::Break(remaining_elapsed)
} else {
let progress = Easing::ease(ZeroToOne::new(
self.elapsed.as_secs_f32() / self.animation.duration.as_secs_f32(),
self.elapsed.as_secs_f32() / self.duration.as_secs_f32(),
));
self.animation
.value
.set(self.start.lerp(&self.animation.end, progress));
self.target.update(progress);
ControlFlow::Continue(())
}
}
@ -248,9 +347,10 @@ where
/// created: [`IntoAnimate::into_animate`]. [`Easing`] is used to customize how
/// interpolation is performed.
pub struct RunningAnimation<T, Easing> {
animation: Animation<T, Easing>,
start: T,
target: T,
duration: Duration,
elapsed: Duration,
_easing: PhantomData<Easing>,
}
/// A handle to a spawned animation. When dropped, the associated animation will

View file

@ -1,5 +1,7 @@
//! Types that provide access to the Gooey runtime.
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use kludgine::app::winit::event::{
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
@ -14,7 +16,7 @@ use crate::styles::components::HighlightColor;
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles};
use crate::value::Dynamic;
use crate::widget::{EventHandling, ManagedWidget, WidgetInstance};
use crate::window::RunningWindow;
use crate::window::{sealed, RunningWindow};
use crate::ConstraintLimit;
/// A context to an event function.
@ -378,6 +380,7 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, '
/// specific widget.
pub struct WidgetContext<'context, 'window> {
current_node: &'context ManagedWidget,
redraw_status: &'context RedrawStatus,
window: &'context mut RunningWindow<'window>,
pending_state: PendingState<'context>,
}
@ -385,10 +388,12 @@ pub struct WidgetContext<'context, 'window> {
impl<'context, 'window> WidgetContext<'context, 'window> {
pub(crate) fn new(
current_node: &'context ManagedWidget,
redraw_status: &'context RedrawStatus,
window: &'context mut RunningWindow<'window>,
) -> Self {
Self {
current_node,
redraw_status,
window,
pending_state: PendingState::Owned(PendingWidgetState {
focus: current_node
@ -407,6 +412,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
pub fn borrowed(&mut self) -> WidgetContext<'_, 'window> {
WidgetContext {
current_node: self.current_node,
redraw_status: self.redraw_status,
window: &mut *self.window,
pending_state: self.pending_state.borrowed(),
}
@ -419,6 +425,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
) -> WidgetContext<'child, 'window> {
WidgetContext {
current_node: widget,
redraw_status: self.redraw_status,
window: &mut *self.window,
pending_state: self.pending_state.borrowed(),
}
@ -430,7 +437,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
/// Ensures that this widget will be redrawn when `value` has been updated.
pub fn redraw_when_changed<T>(&self, value: &Dynamic<T>) {
value.redraw_when_changed(self.window.handle());
value.redraw_when_changed(self.handle());
}
/// Returns the region that this widget last rendered at.
@ -576,6 +583,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
) -> Component::ComponentType {
self.current_node.tree.query_style(self.current_node, query)
}
pub(crate) fn handle(&self) -> WindowHandle {
WindowHandle {
kludgine: self.window.handle(),
redraw_status: self.redraw_status.clone(),
}
}
}
pub(crate) struct WindowHandle {
kludgine: kludgine::app::WindowHandle<sealed::WindowCommand>,
redraw_status: RedrawStatus,
}
impl WindowHandle {
pub fn redraw(&self) {
if self.redraw_status.should_send_refresh() {
let _result = self.kludgine.send(sealed::WindowCommand::Redraw);
}
}
}
impl dyn AsEventContext<'_> {}
@ -637,3 +664,20 @@ impl DerefMut for PendingState<'_> {
}
}
}
#[derive(Default, Clone)]
pub(crate) struct RedrawStatus {
refresh_sent: Arc<AtomicBool>,
}
impl RedrawStatus {
pub fn should_send_refresh(&self) -> bool {
self.refresh_sent
.compare_exchange(false, true, Ordering::Release, Ordering::Acquire)
.is_ok()
}
pub fn refresh_received(&self) {
self.refresh_sent.store(false, Ordering::Release);
}
}

View file

@ -100,3 +100,16 @@ macro_rules! styles {
$crate::styles!($($component => $value),*)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! impl_all_tuples {
($macro_name:ident) => {
$macro_name!(T0 0);
$macro_name!(T0 0, T1 1);
$macro_name!(T0 0, T1 1, T2 2);
$macro_name!(T0 0, T1 1, T2 2, T3 3);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4);
$macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5);
}
}

View file

@ -4,10 +4,8 @@ use std::fmt::Debug;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use kludgine::app::WindowHandle;
use crate::context::WidgetContext;
use crate::window::sealed::WindowCommand;
use crate::animation::{DynamicTransition, LinearInterpolate};
use crate::context::{WidgetContext, WindowHandle};
/// An instance of a value that provides APIs to observe and react to its
/// contents.
@ -90,7 +88,7 @@ impl<T> Dynamic<T> {
with_clone(self.clone())
}
pub(crate) fn redraw_when_changed(&self, window: WindowHandle<WindowCommand>) {
pub(crate) fn redraw_when_changed(&self, window: WindowHandle) {
self.0.redraw_when_changed(window);
}
@ -136,6 +134,17 @@ impl<T> Dynamic<T> {
pub fn generation(&self) -> Generation {
self.state().wrapped.generation
}
/// Returns a pending transition for this value to `new_value`.
pub fn transition_to(&self, new_value: T) -> DynamicTransition<T>
where
T: LinearInterpolate + Clone + Send + Sync,
{
DynamicTransition {
dynamic: self.clone(),
new_value,
}
}
}
impl<T> Default for Dynamic<T>
@ -185,7 +194,7 @@ impl<T> DynamicData<T> {
.map_or_else(PoisonError::into_inner, |g| g)
}
pub fn redraw_when_changed(&self, window: WindowHandle<WindowCommand>) {
pub fn redraw_when_changed(&self, window: WindowHandle) {
let mut state = self.state();
state.windows.push(window);
}
@ -211,7 +220,7 @@ impl<T> DynamicData<T> {
callback.update(&state.wrapped);
}
for window in state.windows.drain(..) {
let _result = window.send(WindowCommand::Redraw);
window.redraw();
}
result
};
@ -251,7 +260,7 @@ impl<T> DynamicData<T> {
struct State<T> {
wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>,
windows: Vec<WindowHandle<WindowCommand>>,
windows: Vec<WindowHandle>,
readers: usize,
}

View file

@ -11,7 +11,7 @@ use kludgine::shapes::Shape;
use kludgine::text::Text;
use kludgine::Color;
use crate::animation::{Animation, AnimationHandle, Spawn};
use crate::animation::{AnimationHandle, AnimationTarget, Spawn};
use crate::context::{EventContext, GraphicsContext, WidgetContext};
use crate::names::Name;
use crate::styles::components::{HighlightColor, IntrinsicPadding, TextColor};
@ -77,12 +77,10 @@ impl Button {
match (immediate, &self.background_color) {
(false, Some(dynamic)) => {
self.background_color_animation = Animation::linear(
dynamic.clone(),
background_color,
Duration::from_millis(150),
)
.spawn();
self.background_color_animation = dynamic
.transition_to(background_color)
.over(Duration::from_millis(150))
.spawn();
}
(true, Some(dynamic)) => {
dynamic.set(background_color);

View file

@ -11,7 +11,7 @@ use kludgine::figures::{
use kludgine::shapes::Shape;
use kludgine::Color;
use crate::animation::{Animation, AnimationHandle, IntoAnimate, Spawn, ZeroToOne};
use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne};
use crate::context::{AsEventContext, EventContext};
use crate::styles::{
ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent,
@ -78,27 +78,25 @@ impl Widget for Scroll {
}
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),
)
.chain(Duration::from_secs(1))
.chain(Animation::linear(
self.scrollbar_opacity.clone(),
ZeroToOne::ZERO,
Duration::from_millis(300),
))
.spawn();
self.scrollbar_opacity_animation = self
.scrollbar_opacity
.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(300))
.and_then(Duration::from_secs(1))
.and_then(
self.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(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();
self.scrollbar_opacity_animation = self
.scrollbar_opacity
.transition_to(ZeroToOne::ZERO)
.over(Duration::from_millis(300))
.spawn();
}
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {

View file

@ -3,7 +3,11 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::path::Path;
use std::string::ToString;
use std::sync::OnceLock;
use kludgine::app::winit::dpi::PhysicalPosition;
use kludgine::app::winit::event::{
@ -16,7 +20,7 @@ use kludgine::figures::Point;
use kludgine::render::Drawing;
use kludgine::Kludgine;
use crate::context::{EventContext, Exclusive, GraphicsContext, WidgetContext};
use crate::context::{EventContext, Exclusive, GraphicsContext, RedrawStatus, WidgetContext};
use crate::graphics::Graphics;
use crate::tree::Tree;
use crate::utils::ModifiersExt;
@ -69,9 +73,24 @@ where
/// Returns a new instance using `context` to initialize the window upon
/// opening.
pub fn new(context: Behavior::Context) -> Self {
static EXECUTABLE_NAME: OnceLock<String> = OnceLock::new();
let title = EXECUTABLE_NAME
.get_or_init(|| {
std::env::args_os()
.next()
.and_then(|path| {
Path::new(&path)
.file_name()
.and_then(OsStr::to_str)
.map(ToString::to_string)
})
.unwrap_or_else(|| String::from("Gooey App"))
})
.clone();
Self {
attributes: WindowAttributes {
title: String::from("Gooey App"),
title,
..WindowAttributes::default()
},
context,
@ -132,6 +151,7 @@ struct GooeyWindow<T> {
contents: Drawing,
should_close: bool,
mouse_state: MouseState,
redraw_status: RedrawStatus,
}
impl<T> GooeyWindow<T>
@ -169,15 +189,17 @@ where
widget: None,
devices: HashMap::default(),
},
redraw_status: RedrawStatus::default(),
}
}
fn prepare(&mut self, mut window: RunningWindow<'_>, graphics: &mut kludgine::Graphics<'_>) {
self.redraw_status.refresh_received();
graphics.reset_text_attributes();
self.root.tree.reset_render_order();
let graphics = self.contents.new_frame(graphics);
GraphicsContext {
widget: WidgetContext::new(&self.root, &mut window),
widget: WidgetContext::new(&self.root, &self.redraw_status, &mut window),
graphics: Exclusive::Owned(Graphics::new(graphics)),
}
.redraw();
@ -248,7 +270,10 @@ where
) {
let target = self.root.tree.focused_widget().unwrap_or(self.root.id);
let target = self.root.tree.widget(target);
let mut target = EventContext::new(WidgetContext::new(&target, &mut window), kludgine);
let mut target = EventContext::new(
WidgetContext::new(&target, &self.redraw_status, &mut window),
kludgine,
);
let handled = recursively_handle_event(&mut target, |widget| {
widget.keyboard_input(device_id, input.clone(), is_synthetic)
@ -281,7 +306,10 @@ where
let widget = self.root.tree.hovered_widget().unwrap_or(self.root.id);
let widget = self.root.tree.widget(widget);
let mut widget = EventContext::new(WidgetContext::new(&widget, &mut window), kludgine);
let mut widget = EventContext::new(
WidgetContext::new(&widget, &self.redraw_status, &mut window),
kludgine,
);
recursively_handle_event(&mut widget, |widget| {
widget.mouse_wheel(device_id, delta, phase)
});
@ -292,7 +320,10 @@ where
fn ime(&mut self, mut window: RunningWindow<'_>, kludgine: &mut Kludgine, ime: Ime) {
let target = self.root.tree.focused_widget().unwrap_or(self.root.id);
let target = self.root.tree.widget(target);
let mut target = EventContext::new(WidgetContext::new(&target, &mut window), kludgine);
let mut target = EventContext::new(
WidgetContext::new(&target, &self.redraw_status, &mut window),
kludgine,
);
let _handled =
recursively_handle_event(&mut target, |widget| widget.ime(ime.clone())).is_some();
@ -311,15 +342,19 @@ where
if let Some(state) = self.mouse_state.devices.get(&device_id) {
// Mouse Drag
for (button, handler) in state {
let mut context =
EventContext::new(WidgetContext::new(handler, &mut window), kludgine);
let mut context = EventContext::new(
WidgetContext::new(handler, &self.redraw_status, &mut window),
kludgine,
);
let last_rendered_at = context.last_rendered_at().expect("passed hit test");
context.mouse_drag(location - last_rendered_at.origin, device_id, *button);
}
} else {
// Hover
let mut context =
EventContext::new(WidgetContext::new(&self.root, &mut window), kludgine);
let mut context = EventContext::new(
WidgetContext::new(&self.root, &self.redraw_status, &mut window),
kludgine,
);
self.mouse_state.widget = None;
for widget in self.root.tree.widgets_at_point(location) {
let mut widget_context = context.for_other(&widget);
@ -350,8 +385,10 @@ where
_device_id: DeviceId,
) {
if self.mouse_state.widget.take().is_some() {
let mut context =
EventContext::new(WidgetContext::new(&self.root, &mut window), kludgine);
let mut context = EventContext::new(
WidgetContext::new(&self.root, &self.redraw_status, &mut window),
kludgine,
);
context.clear_hover();
}
}
@ -366,14 +403,20 @@ where
) {
match state {
ElementState::Pressed => {
EventContext::new(WidgetContext::new(&self.root, &mut window), kludgine)
.clear_focus();
EventContext::new(
WidgetContext::new(&self.root, &self.redraw_status, &mut window),
kludgine,
)
.clear_focus();
if let (ElementState::Pressed, Some(location), Some(hovered)) =
(state, &self.mouse_state.location, &self.mouse_state.widget)
{
if let Some(handler) = recursively_handle_event(
&mut EventContext::new(WidgetContext::new(hovered, &mut window), kludgine),
&mut EventContext::new(
WidgetContext::new(hovered, &self.redraw_status, &mut window),
kludgine,
),
|context| {
let relative = *location
- context.last_rendered_at().expect("passed hit test").origin;
@ -399,8 +442,10 @@ where
self.mouse_state.devices.remove(&device_id);
}
let mut context =
EventContext::new(WidgetContext::new(&handler, &mut window), kludgine);
let mut context = EventContext::new(
WidgetContext::new(&handler, &self.redraw_status, &mut window),
kludgine,
);
let relative = if let (Some(last_rendered), Some(location)) =
(context.last_rendered_at(), self.mouse_state.location)

View file

@ -1,3 +1,5 @@
use crate::impl_all_tuples;
/// Invokes a function with a clone of `self`.
pub trait WithClone: Sized {
/// The type that results from cloning.
@ -31,9 +33,4 @@ where
}
}
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);
impl_all_tuples!(impl_with_clone);