feat(core/rust): hold to confirm animation for Model R

pull/2448/head
tychovrahe 2 years ago committed by TychoVrahe
parent 107e22c814
commit 68598f00af

@ -136,6 +136,138 @@ pub fn rect_fill_rounded1(r: Rect, fg_color: Color, bg_color: Color) {
} }
} }
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct TextOverlay<'a> {
area: Rect,
text: &'a str,
font: Font,
}
impl<'a> TextOverlay<'a> {
pub fn new(text: &'a str, font: Font) -> Self {
let area = Rect::zero();
Self { area, text, font }
}
pub fn place(&mut self, baseline: Point) {
let text_width = self.font.text_width(self.text);
let text_height = self.font.text_height();
let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height);
let text_area_end = baseline + Offset::new(text_width / 2, 0);
let area = Rect::new(text_area_start, text_area_end);
self.area = area;
}
pub fn get_pixel(&self, underlying: Color, fg: Color, p: Point) -> Color {
if !self.area.contains(p) {
return underlying;
}
let mut tot_adv = 0;
let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0);
for g in self.text.bytes().filter_map(|c| self.font.get_glyph(c)) {
let char_area = Rect::new(
Point::new(tot_adv + g.bearing_x, g.height - g.bearing_y),
Point::new(tot_adv + g.bearing_x + g.width, g.bearing_y),
);
tot_adv += g.adv;
if !char_area.contains(p_rel) {
continue;
}
let p_inner = p_rel - char_area.top_left();
let overlay_data = g.get_pixel_data(p_inner);
return Color::lerp(underlying, fg, overlay_data as f32 / 15_f32);
}
underlying
}
}
/// Gets a color of a pixel on `p` coordinates of rounded rectangle with corner
/// radius 2
fn rect_rounded2_get_pixel(
p: Offset,
size: Offset,
colortable: [Color; 16],
fill: bool,
line_width: i32,
) -> Color {
let border = (p.x >= 0 && p.x < line_width)
|| ((p.x >= size.x - line_width) && p.x <= (size.x - 1))
|| (p.y >= 0 && p.y < line_width)
|| ((p.y >= size.y - line_width) && p.y <= (size.y - 1));
let corner_lim = 2 * line_width;
let corner_inner = line_width;
let corner_all = ((p.x > size.x - (corner_lim + 1)) || p.x < corner_lim)
&& (p.y < corner_lim || p.y > size.y - (corner_lim + 1));
let corner = corner_all
&& (p.y >= corner_inner)
&& (p.x >= corner_inner)
&& (p.y <= size.y - (corner_inner + 1))
&& (p.x <= size.x - (corner_inner + 1));
let corner_out = corner_all && !corner;
if (border || corner || fill) && !corner_out {
colortable[15]
} else {
colortable[0]
}
}
/// Draws a rounded rectangle with corner radius 2, partially filled
/// according to `fill_from` and `fill_to` arguments.
/// Optionally draws a text inside the rectangle and adjusts its color to match
/// the fill. The coordinates of the text are specified in the TextOverlay
/// struct.
pub fn bar_with_text_and_fill(
area: Rect,
overlay: Option<TextOverlay>,
fg_color: Color,
bg_color: Color,
fill_from: i32,
fill_to: i32,
) {
let r = area.translate(get_offset());
let clamped = r.clamp(constant::screen());
let colortable = get_color_table(fg_color, bg_color);
set_window(clamped);
for y_c in clamped.y0..clamped.y1 {
for x_c in clamped.x0..clamped.x1 {
let p = Point::new(x_c, y_c);
let r_offset = p - r.top_left();
let filled = (r_offset.x >= fill_from
&& fill_from >= 0
&& (r_offset.x <= fill_to || fill_to < fill_from))
|| (r_offset.x < fill_to && fill_to >= 0);
let underlying_color =
rect_rounded2_get_pixel(r_offset, r.size(), colortable, filled, 1);
let final_color = overlay.map_or(underlying_color, |o| {
let text_color = if filled { bg_color } else { fg_color };
o.get_pixel(underlying_color, text_color, p)
});
pixeldata(final_color);
}
}
pixeldata_dirty();
}
// Used on T1 only. // Used on T1 only.
pub fn dotted_line(start: Point, width: i32, color: Color) { pub fn dotted_line(start: Point, width: i32, color: Color) {
for x in (start.x..width).step_by(2) { for x in (start.x..width).step_by(2) {

@ -0,0 +1,90 @@
use crate::{
time::Instant,
ui::{
component::{Component, Event, EventCtx},
event::ButtonEvent,
geometry::{Point, Rect},
model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
},
};
pub enum HoldToConfirmMsg {
Confirmed,
FailedToConfirm,
}
pub struct HoldToConfirm {
area: Rect,
pos: ButtonPos,
loader: Loader,
baseline: Point,
text_width: i32,
}
impl HoldToConfirm {
pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> Self {
let text_width = styles.normal.font.text_width(text.as_ref());
Self {
area: Rect::zero(),
pos,
loader: Loader::new(text, styles),
baseline: Point::zero(),
text_width,
}
}
fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect {
let button_width = self.text_width + 7;
match pos {
ButtonPos::Left => area.split_left(button_width).0,
ButtonPos::Right => area.split_right(button_width).1,
}
}
}
impl Component for HoldToConfirm {
type Msg = HoldToConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let loader_area = self.placement(bounds, self.pos);
self.loader.place(loader_area)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event {
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
self.loader.start_growing(ctx, Instant::now());
}
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
if self.loader.is_animating() {
self.loader.start_shrinking(ctx, Instant::now());
}
}
_ => {}
};
let msg = self.loader.event(ctx, event);
if let Some(LoaderMsg::GrownCompletely) = msg {
return Some(HoldToConfirmMsg::Confirmed);
}
if let Some(LoaderMsg::ShrunkCompletely) = msg {
return Some(HoldToConfirmMsg::FailedToConfirm);
}
None
}
fn paint(&mut self) {
self.loader.paint();
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for HoldToConfirm {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("HoldToConfirm");
self.loader.trace(d);
d.close();
}
}

@ -0,0 +1,200 @@
use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx},
display::{self, Color, Font},
geometry::{Offset, Rect},
},
};
pub enum LoaderMsg {
GrownCompletely,
ShrunkCompletely,
}
enum State {
Initial,
Growing(Animation<u16>),
Shrinking(Animation<u16>),
Grown,
}
pub struct Loader {
area: Rect,
state: State,
growing_duration: Duration,
shrinking_duration: Duration,
text: display::TextOverlay<'static>,
styles: LoaderStyleSheet,
}
impl Loader {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new(text: &'static str, styles: LoaderStyleSheet) -> Self {
let overlay = display::TextOverlay::new(text, styles.normal.font);
Self {
area: Rect::zero(),
state: State::Initial,
growing_duration: Duration::from_millis(1000),
shrinking_duration: Duration::from_millis(500),
text: overlay,
styles,
}
}
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MIN,
display::LOADER_MAX,
self.growing_duration,
now,
);
if let State::Shrinking(shrinking) = &self.state {
anim.seek_to_value(shrinking.value(now));
}
self.state = State::Growing(anim);
// The animation is starting, request an animation frame event.
ctx.request_anim_frame();
// We don't have to wait for the animation frame event with the first paint,
// let's do that now.
ctx.request_paint();
}
pub fn start_shrinking(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MAX,
display::LOADER_MIN,
self.shrinking_duration,
now,
);
if let State::Growing(growing) = &self.state {
anim.seek_to_value(display::LOADER_MAX - growing.value(now));
}
self.state = State::Shrinking(anim);
// The animation should be already progressing at this point, so we don't need
// to request another animation frames, but we should request to get painted
// after this event pass.
ctx.request_paint();
}
pub fn reset(&mut self) {
self.state = State::Initial;
}
pub fn animation(&self) -> Option<&Animation<u16>> {
match &self.state {
State::Initial => None,
State::Grown => None,
State::Growing(a) | State::Shrinking(a) => Some(a),
}
}
pub fn progress(&self, now: Instant) -> Option<u16> {
self.animation().map(|a| a.value(now))
}
pub fn is_animating(&self) -> bool {
self.animation().is_some()
}
pub fn is_completely_grown(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MAX))
}
pub fn is_completely_shrunk(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MIN))
}
pub fn paint_loader(&mut self, style: &LoaderStyle, done: i32) {
let invert_from = ((self.area.width() + 1) * done) / (display::LOADER_MAX as i32);
display::bar_with_text_and_fill(
self.area,
Some(self.text),
style.fg_color,
style.bg_color,
-1,
invert_from,
);
}
}
impl Component for Loader {
type Msg = LoaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
let baseline = bounds.bottom_center() + Offset::new(1, -1);
self.text.place(baseline);
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let now = Instant::now();
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_animating() {
// We have something to paint, so request to be painted in the next pass.
ctx.request_paint();
if self.is_completely_grown(now) {
self.state = State::Grown;
return Some(LoaderMsg::GrownCompletely);
} else if self.is_completely_shrunk(now) {
self.state = State::Initial;
return Some(LoaderMsg::ShrunkCompletely);
} else {
// There is further progress in the animation, request an animation frame event.
ctx.request_anim_frame();
}
}
}
None
}
fn paint(&mut self) {
// TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful
// in automated tests.
// In practice, taking the current instant here is more precise in case some
// other component in the tree takes a long time to draw.
let now = Instant::now();
if let State::Initial = self.state {
self.paint_loader(self.styles.normal, 0);
} else if let State::Grown = self.state {
self.paint_loader(self.styles.normal, display::LOADER_MAX as i32);
} else {
let progress = self.progress(now);
if let Some(done) = progress {
self.paint_loader(self.styles.normal, done as i32);
} else {
self.paint_loader(self.styles.normal, 0);
}
}
}
}
pub struct LoaderStyleSheet {
pub normal: &'static LoaderStyle,
}
pub struct LoaderStyle {
pub font: Font,
pub fg_color: Color,
pub bg_color: Color,
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Loader {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Loader");
d.close();
}
}

@ -1,11 +1,15 @@
mod button; mod button;
mod confirm;
mod dialog; mod dialog;
mod frame; mod frame;
mod loader;
mod page; mod page;
use super::theme; use super::theme;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogMsg}; pub use dialog::{Dialog, DialogMsg};
pub use frame::Frame; pub use frame::Frame;
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::ButtonPage; pub use page::ButtonPage;

@ -1,6 +1,7 @@
use crate::ui::{ use crate::ui::{
component::text::layout::DefaultTextTheme, component::text::layout::DefaultTextTheme,
display::{Color, Font}, display::{Color, Font},
model_tr::component::{LoaderStyle, LoaderStyleSheet},
}; };
use super::component::{ButtonStyle, ButtonStyleSheet}; use super::component::{ButtonStyle, ButtonStyleSheet};
@ -48,6 +49,16 @@ pub fn button_cancel() -> ButtonStyleSheet {
} }
} }
pub fn loader_default() -> LoaderStyleSheet {
LoaderStyleSheet {
normal: &LoaderStyle {
font: FONT_NORMAL,
fg_color: FG,
bg_color: BG,
},
}
}
pub struct TRDefaultText; pub struct TRDefaultText;
impl DefaultTextTheme for TRDefaultText { impl DefaultTextTheme for TRDefaultText {

Loading…
Cancel
Save