parent
107e22c814
commit
68598f00af
@ -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 confirm;
|
||||
mod dialog;
|
||||
mod frame;
|
||||
mod loader;
|
||||
mod page;
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
|
||||
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
pub use dialog::{Dialog, DialogMsg};
|
||||
pub use frame::Frame;
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||
pub use page::ButtonPage;
|
||||
|
Loading…
Reference in new issue