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), TextChanged(String),
TextEntered(String), TextEntered(String),
CheckboxChecked(bool), CheckboxChecked(bool),
VariableDrag(f64, f64),
// FIXME - This is a huge hack // FIXME - This is a huge hack
Other(Box<dyn Any + Send>), 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::TextChanged(text) => f.debug_tuple("TextChanged").field(text).finish(),
Self::TextEntered(text) => f.debug_tuple("TextEntered").field(text).finish(), Self::TextEntered(text) => f.debug_tuple("TextEntered").field(text).finish(),
Self::CheckboxChecked(b) => f.debug_tuple("CheckboxChecked").field(b).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(...)"), Self::Other(_) => write!(f, "Other(...)"),
} }
} }

View File

@ -8,13 +8,14 @@ use std::cmp::Ordering;
use accesskit::Role; use accesskit::Role;
use parley::fontique::Weight; use parley::fontique::Weight;
use parley::layout::Alignment; use parley::layout::Alignment;
use parley::style::{FontFamily, FontStack}; use parley::style::{FontFamily, FontSettings, FontStack};
use smallvec::SmallVec; use smallvec::SmallVec;
use tracing::{trace, trace_span, Span}; use tracing::{debug, trace, trace_span, Span};
use vello::kurbo::{Affine, Point, Size}; use vello::kurbo::{Affine, Point, Size};
use vello::peniko::BlendMode; use vello::peniko::BlendMode;
use vello::Scene; use vello::Scene;
use crate::action::Action;
use crate::text::{Hinting, TextBrush, TextLayout, TextStorage}; use crate::text::{Hinting, TextBrush, TextLayout, TextStorage};
use crate::widget::WidgetMut; use crate::widget::WidgetMut;
use crate::{ use crate::{
@ -141,6 +142,7 @@ pub struct VariableLabel {
show_disabled: bool, show_disabled: bool,
brush: TextBrush, brush: TextBrush,
weight: AnimatedF32, weight: AnimatedF32,
width: AnimatedF32,
} }
// --- MARK: BUILDERS --- // --- MARK: BUILDERS ---
@ -153,6 +155,7 @@ impl VariableLabel {
show_disabled: true, show_disabled: true,
brush: crate::theme::TEXT_COLOR.into(), brush: crate::theme::TEXT_COLOR.into(),
weight: AnimatedF32::stable(Weight::NORMAL.value()), 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_paint();
self.ctx.request_anim_frame(); 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 --- // --- MARK: IMPL WIDGET ---
impl Widget for VariableLabel { 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 { 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 // 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) => { PointerEvent::PointerDown(_button, _state) => {
// TODO: Start tracking currently pressed ctx.set_active(true);
// (i.e. don't press)
}
PointerEvent::PointerUp(_button, _state) => {
// TODO: Follow link (if not now dragging ?)
} }
PointerEvent::PointerUp(_button, _state) => ctx.set_active(false),
_ => {} _ => {}
} }
} }
@ -337,9 +350,10 @@ impl Widget for VariableLabel {
} }
LifeCycle::AnimFrame(time) => { LifeCycle::AnimFrame(time) => {
let millis = (*time as f64 / 1_000_000.) as f32; 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(); self.text_layout.invalidate();
if !result.is_completed() { if !result1.is_completed() || !result2.is_completed() {
ctx.request_anim_frame(); ctx.request_anim_frame();
} }
ctx.request_layout(); ctx.request_layout();
@ -370,6 +384,8 @@ impl Widget for VariableLabel {
builder.push_default(&parley::style::StyleProperty::FontWeight(Weight::new( builder.push_default(&parley::style::StyleProperty::FontWeight(Weight::new(
self.weight.value, 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( // builder.push_default(&parley::style::StyleProperty::FontVariations(
// parley::style::FontSettings::List(&[]), // parley::style::FontSettings::List(&[]),
// )); // ));

View File

@ -24,10 +24,13 @@ use xilem_core::fork;
struct Clocks { struct Clocks {
/// The font [weight](Weight) used for the values. /// The font [weight](Weight) used for the values.
weight: f32, weight: f32,
width: f32,
smoothing_ms: f32,
/// The current UTC offset on this machine. /// The current UTC offset on this machine.
local_offset: Result<UtcOffset, IndeterminateOffset>, local_offset: Result<UtcOffset, IndeterminateOffset>,
/// The current time. /// The current time.
now_utc: OffsetDateTime, now_utc: OffsetDateTime,
big: bool,
} }
/// A possible timezone, with an offset from UTC. /// 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. /// Controls for the variable font weight.
fn controls() -> impl WidgetView<Clocks> { fn controls() -> impl WidgetView<Clocks> {
flex(( flex((
button("Increase", |data: &mut Clocks| { // button("Increase", |data: &mut Clocks| {
data.weight = (data.weight + 100.).clamp(1., 1000.); // data.smoothing_ms = 400.0;
}), // data.weight = (data.weight + 100.).clamp(1., 1000.);
button("Decrease", |data: &mut Clocks| { // }),
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| { button("Minimum", |data: &mut Clocks| {
data.smoothing_ms = 400.0;
data.weight = 1.; data.weight = 1.;
}), }),
button("Maximum", |data: &mut Clocks| { button("Maximum", |data: &mut Clocks| {
data.smoothing_ms = 400.0;
data.weight = 1000.; data.weight = 1000.;
}), }),
button("Big", |data: &mut Clocks| data.big = true),
button("Small", |data: &mut Clocks| data.big = false),
)) ))
.direction(Axis::Horizontal) .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. /// 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> { fn view(&self, data: &mut Clocks) -> impl WidgetView<Clocks> {
let date_time_in_self = data.now_utc.to_offset(self.offset); 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(( sized_box(flex((
flex(( flex((
prose(self.region), prose(self.region),
@ -136,11 +150,17 @@ impl TimeZone {
.format(format_description!("[hour repr:24]:[minute]:[second]")) .format(format_description!("[hour repr:24]:[minute]:[second]"))
.unwrap() .unwrap()
.to_string(), .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. // Use the roboto flex we have just loaded.
.with_font(FontStack::List(&[FontFamily::Named("Roboto Flex")])) .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), FlexSpacer::Flex(1.0),
(data.local_now().date() != date_time_in_self.date()).then(|| { (data.local_now().date() != date_time_in_self.date()).then(|| {
label( label(
@ -153,7 +173,7 @@ impl TimeZone {
.direction(Axis::Horizontal), .direction(Axis::Horizontal),
))) )))
.expand_width() .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> { fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
let data = Clocks { let data = Clocks {
weight: Weight::BLACK.value(), weight: Weight::BLACK.value(),
width: 100.0,
smoothing_ms: 400.0,
// TODO: We can't get this on Android, because // TODO: We can't get this on Android, because
local_offset: UtcOffset::current_local_offset(), local_offset: UtcOffset::current_local_offset(),
now_utc: OffsetDateTime::now_utc(), now_utc: OffsetDateTime::now_utc(),
big: false,
}; };
// Load Roboto Flex so that it can be used at runtime. // 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}; use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};
/// A view for displaying non-editable text, with a variable [weight](masonry::parley::style::FontWeight). /// 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 { VariableLabel {
label: label.into(), label: label.into(),
text_brush: Color::WHITE.into(), text_brush: Color::WHITE.into(),
alignment: TextAlignment::default(), alignment: TextAlignment::default(),
text_size: masonry::theme::TEXT_SIZE_NORMAL as f32, text_size: masonry::theme::TEXT_SIZE_NORMAL as f32,
target_weight: Weight::NORMAL, target_weight: Weight::NORMAL,
target_width: 100.0,
over_millis: 0., over_millis: 0.,
font: FontStack::Single(FontFamily::Generic(GenericFamily::SystemUi)), 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, label: ArcStr,
text_brush: TextBrush, text_brush: TextBrush,
alignment: TextAlignment, alignment: TextAlignment,
text_size: f32, text_size: f32,
target_width: f32,
target_weight: Weight, target_weight: Weight,
over_millis: f32, over_millis: f32,
font: FontStack<'static>, font: FontStack<'static>,
// TODO: add more attributes of `masonry::widget::Label` // TODO: add more attributes of `masonry::widget::Label`
callback: F,
} }
impl VariableLabel { impl<F> VariableLabel<F> {
#[doc(alias = "color")] #[doc(alias = "color")]
pub fn brush(mut self, brush: impl Into<TextBrush>) -> Self { pub fn brush(mut self, brush: impl Into<TextBrush>) -> Self {
self.text_brush = brush.into(); self.text_brush = brush.into();
@ -76,6 +84,12 @@ impl VariableLabel {
self 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. /// Set the [font stack](FontStack) this label will use.
/// ///
/// A font stack allows for providing fallbacks. If there is no matching font /// A font stack allows for providing fallbacks. If there is no matching font
@ -95,13 +109,17 @@ impl VariableLabel {
} }
} }
impl ViewMarker for VariableLabel {} impl<F> ViewMarker for VariableLabel<F> {}
impl<State, Action> View<State, Action, ViewCtx> for VariableLabel { 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 Element = Pod<widget::VariableLabel>;
type ViewState = (); type ViewState = ();
fn build(&self, _ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
let widget_pod = Pod::new( ctx.with_leaf_action_widget(|_| {
Pod::new(
widget::VariableLabel::new(self.label.clone()) widget::VariableLabel::new(self.label.clone())
.with_text_brush(self.text_brush.clone()) .with_text_brush(self.text_brush.clone())
.with_line_break_mode(widget::LineBreaking::WordWrap) .with_line_break_mode(widget::LineBreaking::WordWrap)
@ -109,8 +127,8 @@ impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
.with_font(self.font) .with_font(self.font)
.with_text_size(self.text_size) .with_text_size(self.text_size)
.with_initial_weight(self.target_weight.value()), .with_initial_weight(self.target_weight.value()),
); )
(widget_pod, ()) })
} }
fn rebuild<'el>( 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); element.set_target_weight(self.target_weight.value(), self.over_millis);
ctx.mark_changed(); 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. // 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; let fonts_eq = fonts_eq_fastpath(prev.font, self.font) || prev.font == self.font;
if !fonts_eq { if !fonts_eq {
@ -156,12 +178,24 @@ impl<State, Action> View<State, Action, ViewCtx> for VariableLabel {
(): &mut Self::ViewState, (): &mut Self::ViewState,
_id_path: &[ViewId], _id_path: &[ViewId],
message: xilem_core::DynMessage, message: xilem_core::DynMessage,
_app_state: &mut State, app_state: &mut State,
) -> crate::MessageResult<Action> { ) -> crate::MessageResult<Action> {
tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug"); 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) MessageResult::Stale(message)
} }
} }
}
}
/// Because all the `FontStack`s we use are 'static, we expect the value to never change. /// Because all the `FontStack`s we use are 'static, we expect the value to never change.
/// ///