First cut at touch-based variable demo

Wires up touch on timezone demo to weight and width axes.

Hacky but effective.
This commit is contained in:
Raph Levien 2024-09-09 12:22:54 -07:00
parent 75d515617a
commit 6e0154c398
4 changed files with 114 additions and 39 deletions

View File

@ -18,6 +18,7 @@ pub enum Action {
TextChanged(String),
TextEntered(String),
CheckboxChecked(bool),
VariableDrag(f64, f64),
// FIXME - This is a huge hack
Other(Box<dyn Any + Send>),
}
@ -43,6 +44,7 @@ impl std::fmt::Debug for Action {
Self::TextChanged(text) => f.debug_tuple("TextChanged").field(text).finish(),
Self::TextEntered(text) => f.debug_tuple("TextEntered").field(text).finish(),
Self::CheckboxChecked(b) => f.debug_tuple("CheckboxChecked").field(b).finish(),
Self::VariableDrag(x, y) => f.debug_tuple("VariableDrag").field(x).field(y).finish(),
Self::Other(_) => write!(f, "Other(...)"),
}
}

View File

@ -8,13 +8,14 @@ use std::cmp::Ordering;
use accesskit::Role;
use parley::fontique::Weight;
use parley::layout::Alignment;
use parley::style::{FontFamily, FontStack};
use parley::style::{FontFamily, FontSettings, FontStack};
use smallvec::SmallVec;
use tracing::{trace, trace_span, Span};
use tracing::{debug, trace, trace_span, Span};
use vello::kurbo::{Affine, Point, Size};
use vello::peniko::BlendMode;
use vello::Scene;
use crate::action::Action;
use crate::text::{Hinting, TextBrush, TextLayout, TextStorage};
use crate::widget::WidgetMut;
use crate::{
@ -141,6 +142,7 @@ pub struct VariableLabel {
show_disabled: bool,
brush: TextBrush,
weight: AnimatedF32,
width: AnimatedF32,
}
// --- MARK: BUILDERS ---
@ -153,6 +155,7 @@ impl VariableLabel {
show_disabled: true,
brush: crate::theme::TEXT_COLOR.into(),
weight: AnimatedF32::stable(Weight::NORMAL.value()),
width: AnimatedF32::stable(100.0),
}
}
@ -278,22 +281,32 @@ impl WidgetMut<'_, VariableLabel> {
self.ctx.request_paint();
self.ctx.request_anim_frame();
}
/// Set the weight which this font will target.
pub fn set_target_width(&mut self, target: f32, over_millis: f32) {
self.widget.width.move_to(target, over_millis);
self.ctx.request_layout();
self.ctx.request_paint();
self.ctx.request_anim_frame();
}
}
// --- MARK: IMPL WIDGET ---
impl Widget for VariableLabel {
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, event: &PointerEvent) {
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
match event {
PointerEvent::PointerMove(_point) => {
PointerEvent::PointerMove(point) => {
let mouse_pos =
Point::new(point.position.x, point.position.y) - ctx.window_origin().to_vec2();
// TODO: Set cursor if over link
if ctx.is_active() {
debug!("got pointer move event in variable label {point:?}");
ctx.submit_action(Action::VariableDrag(mouse_pos.x, mouse_pos.y));
}
}
PointerEvent::PointerDown(_button, _state) => {
// TODO: Start tracking currently pressed
// (i.e. don't press)
}
PointerEvent::PointerUp(_button, _state) => {
// TODO: Follow link (if not now dragging ?)
ctx.set_active(true);
}
PointerEvent::PointerUp(_button, _state) => ctx.set_active(false),
_ => {}
}
}
@ -337,9 +350,10 @@ impl Widget for VariableLabel {
}
LifeCycle::AnimFrame(time) => {
let millis = (*time as f64 / 1_000_000.) as f32;
let result = self.weight.advance(millis);
let result1 = self.weight.advance(millis);
let result2 = self.width.advance(millis);
self.text_layout.invalidate();
if !result.is_completed() {
if !result1.is_completed() || !result2.is_completed() {
ctx.request_anim_frame();
}
ctx.request_layout();
@ -370,6 +384,8 @@ impl Widget for VariableLabel {
builder.push_default(&parley::style::StyleProperty::FontWeight(Weight::new(
self.weight.value,
)));
let wdth_setting = ("wdth", self.width.value).into();
builder.push_default(&parley::style::StyleProperty::FontVariations(FontSettings::List(&[wdth_setting])));
// builder.push_default(&parley::style::StyleProperty::FontVariations(
// parley::style::FontSettings::List(&[]),
// ));

View File

@ -24,10 +24,13 @@ use xilem_core::fork;
struct Clocks {
/// The font [weight](Weight) used for the values.
weight: f32,
width: f32,
smoothing_ms: f32,
/// The current UTC offset on this machine.
local_offset: Result<UtcOffset, IndeterminateOffset>,
/// The current time.
now_utc: OffsetDateTime,
big: bool,
}
/// A possible timezone, with an offset from UTC.
@ -94,18 +97,24 @@ fn local_time(data: &mut Clocks) -> impl WidgetView<Clocks> {
/// Controls for the variable font weight.
fn controls() -> impl WidgetView<Clocks> {
flex((
button("Increase", |data: &mut Clocks| {
data.weight = (data.weight + 100.).clamp(1., 1000.);
}),
button("Decrease", |data: &mut Clocks| {
data.weight = (data.weight - 100.).clamp(1., 1000.);
}),
// button("Increase", |data: &mut Clocks| {
// data.smoothing_ms = 400.0;
// data.weight = (data.weight + 100.).clamp(1., 1000.);
// }),
// button("Decrease", |data: &mut Clocks| {
// data.smoothing_ms = 400.0;
// data.weight = (data.weight - 100.).clamp(1., 1000.);
// }),
button("Minimum", |data: &mut Clocks| {
data.smoothing_ms = 400.0;
data.weight = 1.;
}),
button("Maximum", |data: &mut Clocks| {
data.smoothing_ms = 400.0;
data.weight = 1000.;
}),
button("Big", |data: &mut Clocks| data.big = true),
button("Small", |data: &mut Clocks| data.big = false),
))
.direction(Axis::Horizontal)
}
@ -114,6 +123,11 @@ impl TimeZone {
/// Display this timezone as a row, designed to be shown in a list of time zones.
fn view(&self, data: &mut Clocks) -> impl WidgetView<Clocks> {
let date_time_in_self = data.now_utc.to_offset(self.offset);
let text_size = if data.big {
85.0
} else {
48.0
};
sized_box(flex((
flex((
prose(self.region),
@ -136,11 +150,17 @@ impl TimeZone {
.format(format_description!("[hour repr:24]:[minute]:[second]"))
.unwrap()
.to_string(),
|state: &mut Clocks, x, y| {
state.width = (x * 0.5).clamp(25., 151.) as f32;
state.weight = (y * 15.0).clamp(1., 1000.) as f32;
state.smoothing_ms = 10.0;
}
)
.text_size(48.)
.text_size(text_size)
// Use the roboto flex we have just loaded.
.with_font(FontStack::List(&[FontFamily::Named("Roboto Flex")]))
.target_weight(data.weight, 400.),
.target_weight(data.weight, data.smoothing_ms)
.target_width(data.width),
FlexSpacer::Flex(1.0),
(data.local_now().date() != date_time_in_self.date()).then(|| {
label(
@ -153,7 +173,7 @@ impl TimeZone {
.direction(Axis::Horizontal),
)))
.expand_width()
.height(72.)
.height(text_size as f64 + 24.0)
}
}
@ -185,9 +205,12 @@ const ROBOTO_FLEX: &[u8] = include_bytes!(concat!(
fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
let data = Clocks {
weight: Weight::BLACK.value(),
width: 100.0,
smoothing_ms: 400.0,
// TODO: We can't get this on Android, because
local_offset: UtcOffset::current_local_offset(),
now_utc: OffsetDateTime::now_utc(),
big: false,
};
// Load Roboto Flex so that it can be used at runtime.

View File

@ -14,31 +14,39 @@ use xilem_core::{Mut, ViewMarker};
use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
/// A view for displaying non-editable text, with a variable [weight](masonry::parley::style::FontWeight).
pub fn variable_label(label: impl Into<ArcStr>) -> VariableLabel {
pub fn variable_label<State, Action>(
label: impl Into<ArcStr>,
callback: impl Fn(&mut State, f64, f64) -> Action + Send + 'static,
) -> VariableLabel<impl for<'a> Fn(&'a mut State, f64, f64) -> MessageResult<Action> + Send + 'static>
{
VariableLabel {
label: label.into(),
text_brush: Color::WHITE.into(),
alignment: TextAlignment::default(),
text_size: masonry::theme::TEXT_SIZE_NORMAL as f32,
target_weight: Weight::NORMAL,
target_width: 100.0,
over_millis: 0.,
font: FontStack::Single(FontFamily::Generic(GenericFamily::SystemUi)),
callback: move |state: &mut State, x, y| MessageResult::Action(callback(state, x, y)),
}
}
pub struct VariableLabel {
pub struct VariableLabel<F> {
label: ArcStr,
text_brush: TextBrush,
alignment: TextAlignment,
text_size: f32,
target_width: f32,
target_weight: Weight,
over_millis: f32,
font: FontStack<'static>,
// TODO: add more attributes of `masonry::widget::Label`
callback: F,
}
impl VariableLabel {
impl<F> VariableLabel<F> {
#[doc(alias = "color")]
pub fn brush(mut self, brush: impl Into<TextBrush>) -> Self {
self.text_brush = brush.into();
@ -76,6 +84,12 @@ impl VariableLabel {
self
}
pub fn target_width(mut self, width: f32) -> Self {
assert!(width.is_finite(), "Invalid target width {width}.");
self.target_width = width;
self
}
/// Set the [font stack](FontStack) this label will use.
///
/// A font stack allows for providing fallbacks. If there is no matching font
@ -95,22 +109,26 @@ impl VariableLabel {
}
}
impl ViewMarker for VariableLabel {}
impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
impl<F> ViewMarker for VariableLabel<F> {}
impl<F, State, Action> View<State, Action, ViewCtx> for VariableLabel<F>
where
F: Fn(&mut State, f64, f64) -> MessageResult<Action> + Send + Sync + 'static,
{
type Element = Pod<widget::VariableLabel>;
type ViewState = ();
fn build(&self, _ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
let widget_pod = Pod::new(
widget::VariableLabel::new(self.label.clone())
.with_text_brush(self.text_brush.clone())
.with_line_break_mode(widget::LineBreaking::WordWrap)
.with_text_alignment(self.alignment)
.with_font(self.font)
.with_text_size(self.text_size)
.with_initial_weight(self.target_weight.value()),
);
(widget_pod, ())
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
ctx.with_leaf_action_widget(|_| {
Pod::new(
widget::VariableLabel::new(self.label.clone())
.with_text_brush(self.text_brush.clone())
.with_line_break_mode(widget::LineBreaking::WordWrap)
.with_text_alignment(self.alignment)
.with_font(self.font)
.with_text_size(self.text_size)
.with_initial_weight(self.target_weight.value()),
)
})
}
fn rebuild<'el>(
@ -140,6 +158,10 @@ impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
element.set_target_weight(self.target_weight.value(), self.over_millis);
ctx.mark_changed();
}
if prev.target_width != self.target_width {
element.set_target_width(self.target_width, self.over_millis);
ctx.mark_changed();
}
// First perform a fast filter, then perform a full comparison if that suggests a possible change.
let fonts_eq = fonts_eq_fastpath(prev.font, self.font) || prev.font == self.font;
if !fonts_eq {
@ -156,10 +178,22 @@ impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
(): &mut Self::ViewState,
_id_path: &[ViewId],
message: xilem_core::DynMessage,
_app_state: &mut State,
app_state: &mut State,
) -> crate::MessageResult<Action> {
tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug");
MessageResult::Stale(message)
match message.downcast::<masonry::Action>() {
Ok(action) => {
if let masonry::Action::VariableDrag(x, y) = *action {
(self.callback)(app_state, x, y)
} else {
tracing::error!("Wrong action type in VariableLabel::message: {action:?}");
MessageResult::Stale(action)
}
}
Err(message) => {
tracing::error!("Wrong message type in VariableLabel::message: {message:?}");
MessageResult::Stale(message)
}
}
}
}