#![expect(unreachable_pub, reason = "We don't expose the animation types yet")]
use std::cmp::Ordering;
use accesskit::{Node, Role};
use masonry_core::core::NoAction;
use parley::style::FontWeight;
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Point, Size};
use crate::core::{
AccessCtx, ArcStr, BoxConstraints, ChildrenIds, LayoutCtx, NewWidget, PaintCtx, PropertiesMut,
PropertiesRef, RegisterCtx, StyleProperty, Update, UpdateCtx, Widget, WidgetId, WidgetMut,
WidgetPod,
};
use crate::widgets::Label;
#[derive(Clone, Debug)]
pub struct AnimatedF32 {
target: f32,
value: f32,
rate_per_millisecond: f32,
}
impl AnimatedF32 {
pub fn stable(value: f32) -> Self {
assert!(value.is_finite(), "invalid animated value");
Self {
target: value,
value,
rate_per_millisecond: 0.,
}
}
pub fn move_to(&mut self, target: f32, over_millis: f32) {
assert!(target.is_finite(), "invalid target value");
assert!(over_millis.is_finite(), "invalid delay value");
self.target = target;
match over_millis.partial_cmp(&0.) {
Some(Ordering::Equal) => self.value = target,
Some(Ordering::Less) => {
tracing::warn!("move_to: provided negative time step {over_millis}");
self.value = target;
}
Some(Ordering::Greater) => {
self.rate_per_millisecond = (self.target - self.value) / over_millis;
debug_assert!(
self.rate_per_millisecond.is_finite(),
"Calculated invalid rate despite valid inputs. Current value is {}",
self.value
);
}
None => panic!("Provided invalid time step {over_millis}"),
}
}
pub fn advance(&mut self, by_millis: f32) -> AnimationStatus {
assert!(by_millis.is_finite(), "invalid timestep value");
let original_side = self
.value
.partial_cmp(&self.target)
.expect("Target and value are not NaN.");
self.value += self.rate_per_millisecond * by_millis;
let other_side = self
.value
.partial_cmp(&self.target)
.expect("Target and value are not NaN.");
if other_side.is_eq() || original_side != other_side {
self.value = self.target;
self.rate_per_millisecond = 0.;
AnimationStatus::Completed
} else {
AnimationStatus::Ongoing
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AnimationStatus {
Completed,
Ongoing,
}
impl AnimationStatus {
pub fn is_completed(self) -> bool {
matches!(self, Self::Completed)
}
}
pub struct VariableLabel {
label: WidgetPod<Label>,
weight: AnimatedF32,
}
impl VariableLabel {
pub fn new(text: impl Into<ArcStr>) -> Self {
Self::from_label(NewWidget::new(Label::new(text)))
}
pub fn from_label(label: NewWidget<Label>) -> Self {
Self {
label: label.to_pod(),
weight: AnimatedF32::stable(FontWeight::NORMAL.value()),
}
}
pub fn with_initial_weight(mut self, weight: f32) -> Self {
self.weight = AnimatedF32::stable(weight);
self
}
}
impl VariableLabel {
pub fn label_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, Label> {
this.ctx.get_mut(&mut this.widget.label)
}
pub fn set_text(this: &mut WidgetMut<'_, Self>, new_text: impl Into<ArcStr>) {
Label::set_text(&mut Self::label_mut(this), new_text);
}
pub fn set_target_weight(this: &mut WidgetMut<'_, Self>, target: f32, over_millis: f32) {
this.widget.weight.move_to(target, over_millis);
this.ctx.request_layout();
this.ctx.request_anim_frame();
}
}
impl Widget for VariableLabel {
type Action = NoAction;
fn accepts_pointer_interaction(&self) -> bool {
false
}
fn update(
&mut self,
_ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &Update,
) {
}
fn register_children(&mut self, ctx: &mut RegisterCtx<'_>) {
ctx.register_child(&mut self.label);
}
fn on_anim_frame(
&mut self,
ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
interval: u64,
) {
let millis = (interval as f64 / 1_000_000.) as f32;
let result = self.weight.advance(millis);
let new_weight = self.weight.value;
ctx.mutate_later(&mut self.label, move |mut label| {
if result.is_completed() {
Label::set_hint(&mut label, true);
} else {
Label::set_hint(&mut label, false);
}
Label::insert_style(
&mut label,
StyleProperty::FontWeight(FontWeight::new(new_weight)),
);
});
if !result.is_completed() {
ctx.request_anim_frame();
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx<'_>,
_props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
let size = ctx.run_layout(&mut self.label, bc);
ctx.place_child(&mut self.label, Point::ORIGIN);
size
}
fn paint(&mut self, _ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, _scene: &mut Scene) {}
fn accessibility_role(&self) -> Role {
Role::GenericContainer
}
fn accessibility(
&mut self,
_ctx: &mut AccessCtx<'_>,
_props: &PropertiesRef<'_>,
_node: &mut Node,
) {
}
fn children_ids(&self) -> ChildrenIds {
ChildrenIds::from_slice(&[self.label.id()])
}
fn make_trace_span(&self, id: WidgetId) -> Span {
trace_span!("VariableLabel", id = id.trace())
}
}
#[cfg(test)]
mod tests {
}