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),
|
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(...)"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(&[]),
|
||||||
// ));
|
// ));
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue