mirror of https://github.com/linebender/xilem
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:
parent
75d515617a
commit
6e0154c398
|
@ -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(...)"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(&[]),
|
||||
// ));
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue