xilem/masonry/src/widgets/variable_label.rs

283 lines
8.4 KiB
Rust

// Copyright 2019 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! A label with support for animated variable font properties
use std::cmp::Ordering;
use accesskit::{Node, Role};
use smallvec::{SmallVec, smallvec};
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::{Point, Size};
use crate::core::{
AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent,
PropertiesMut, PropertiesRef, QueryCtx, RegisterCtx, StyleProperty, TextEvent, Update,
UpdateCtx, Widget, WidgetId, WidgetMut, WidgetPod,
};
use crate::parley::style::FontWeight;
use crate::widgets::Label;
/// An `f32` value which can move towards a target value at a linear rate over time.
#[derive(Clone, Debug)]
pub struct AnimatedF32 {
/// The value which self will eventually reach.
target: f32,
/// The current value
value: f32,
// TODO: Provide different easing functions, instead of just linear
/// The change in value every millisecond, which will not change over the lifetime of the value.
rate_per_millisecond: f32,
}
impl AnimatedF32 {
/// Create a value which is not changing.
pub fn stable(value: f32) -> Self {
assert!(value.is_finite());
Self {
target: value,
value,
rate_per_millisecond: 0.,
}
}
/// Move this value to the `target` over `over_millis` milliseconds.
/// Might change the current value, if `over_millis` is zero.
///
/// `over_millis` should be non-negative.
///
/// # Panics
///
/// If `target` is not a finite value.
pub fn move_to(&mut self, target: f32, over_millis: f32) {
assert!(target.is_finite());
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) => {
// Since over_millis is positive, we know that this vector is in the direction of the `target`.
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}"),
}
}
/// Advance this animation by `by_millis` milliseconds.
///
/// Returns the status of the animation after this advancement.
pub fn advance(&mut self, by_millis: f32) -> AnimationStatus {
if !self.value.is_finite() {
tracing::error!("Got unexpected non-finite value {}", self.value);
debug_assert!(self.target.is_finite());
self.value = self.target;
}
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
}
}
}
/// The status an animation can be in.
///
/// Generally returned when an animation is advanced, to determine whether.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AnimationStatus {
/// The animation has finished.
Completed,
/// The animation is still running
Ongoing,
}
impl AnimationStatus {
pub fn is_completed(self) -> bool {
matches!(self, Self::Completed)
}
}
/// A widget displaying non-editable text, with a variable [weight](parley::style::FontWeight).
pub struct VariableLabel {
label: WidgetPod<Label>,
weight: AnimatedF32,
}
// --- MARK: BUILDERS ---
impl VariableLabel {
/// Create a new variable label from the given text.
pub fn new(text: impl Into<ArcStr>) -> Self {
Self::from_label_pod(WidgetPod::new(Label::new(text)))
}
/// Create a new variable label from the given label.
///
/// Uses the label's text and style values.
pub fn from_label(label: Label) -> Self {
Self::from_label_pod(WidgetPod::new(label))
}
/// Create a new variable label from the given label wrapped in a [`WidgetPod`].
///
/// Uses the label's text and style values.
pub fn from_label_pod(label: WidgetPod<Label>) -> Self {
Self {
label,
weight: AnimatedF32::stable(FontWeight::NORMAL.value()),
}
}
/// Set the initial font weight for this text.
pub fn with_initial_weight(mut self, weight: f32) -> Self {
self.weight = AnimatedF32::stable(weight);
self
}
}
// --- MARK: WIDGETMUT ---
impl VariableLabel {
/// Get the underlying label for this widget.
pub fn label_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, Label> {
this.ctx.get_mut(&mut this.widget.label)
}
/// Set the text of this label.
pub fn set_text(this: &mut WidgetMut<'_, Self>, new_text: impl Into<ArcStr>) {
Label::set_text(&mut Self::label_mut(this), new_text);
}
/// Set the weight which this font will target.
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();
}
}
// --- MARK: IMPL WIDGET ---
impl Widget for VariableLabel {
fn on_pointer_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &PointerEvent,
) {
}
fn accepts_pointer_interaction(&self) -> bool {
false
}
fn on_text_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &TextEvent,
) {
}
fn on_access_event(
&mut self,
_ctx: &mut EventCtx,
_props: &mut PropertiesMut<'_>,
_event: &AccessEvent,
) {
}
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;
// The ergonomics of child widgets are quite bad - ideally, this wouldn't need a mutate pass, since we
// can set the required invalidation anyway.
ctx.mutate_later(&mut self.label, move |mut label| {
// TODO: Should this be configurable?
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) -> SmallVec<[WidgetId; 16]> {
smallvec![self.label.id()]
}
fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span {
trace_span!("VariableLabel", id = ctx.widget_id().trace())
}
}
// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
// TODO - Add tests
}