use crate::{ time::Duration, ui::{ component::{Component, ComponentExt, Event, EventCtx, GridPlaced, Map, TimerToken}, display::{self, Color, Font}, event::TouchEvent, geometry::{Insets, Offset, Rect}, }, }; use super::theme; pub enum ButtonMsg { Pressed, Released, Clicked, LongPressed, } pub struct Button { area: Rect, content: ButtonContent, styles: ButtonStyleSheet, state: State, long_press: Option, long_timer: Option, } impl Button { /// Standard height in pixels. pub const HEIGHT: i32 = 38; /// Offsets the baseline of the button text either up (negative) or down /// (positive). pub const BASELINE_OFFSET: i32 = -3; pub fn new(content: ButtonContent) -> Self { Self { content, area: Rect::zero(), styles: theme::button_default(), state: State::Initial, long_press: None, long_timer: None, } } pub fn with_text(text: T) -> Self { Self::new(ButtonContent::Text(text)) } pub fn with_icon(image: &'static [u8]) -> Self { Self::new(ButtonContent::Icon(image)) } pub fn empty() -> Self { Self::new(ButtonContent::Empty) } pub fn styled(mut self, styles: ButtonStyleSheet) -> Self { self.styles = styles; self } pub fn with_long_press(mut self, duration: Duration) -> Self { self.long_press = Some(duration); self } pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) { if enabled { self.enable(ctx); } else { self.disable(ctx); } } pub fn initially_enabled(mut self, enabled: bool) -> Self { if !enabled { self.state = State::Disabled; } self } pub fn enable(&mut self, ctx: &mut EventCtx) { self.set(ctx, State::Initial) } pub fn disable(&mut self, ctx: &mut EventCtx) { self.set(ctx, State::Disabled) } pub fn is_enabled(&self) -> bool { matches!( self.state, State::Initial | State::Pressed | State::Released ) } pub fn is_disabled(&self) -> bool { matches!(self.state, State::Disabled) } pub fn set_content(&mut self, ctx: &mut EventCtx, content: ButtonContent) where T: PartialEq, { if self.content != content { self.content = content; ctx.request_paint(); } } pub fn content(&self) -> &ButtonContent { &self.content } pub fn set_stylesheet(&mut self, ctx: &mut EventCtx, styles: ButtonStyleSheet) { if self.styles != styles { self.styles = styles; ctx.request_paint(); } } pub fn style(&self) -> &ButtonStyle { match self.state { State::Initial | State::Released => self.styles.normal, State::Pressed => self.styles.active, State::Disabled => self.styles.disabled, } } pub fn area(&self) -> Rect { self.area } fn set(&mut self, ctx: &mut EventCtx, state: State) { if self.state != state { self.state = state; ctx.request_paint(); } } pub fn paint_background(&self, style: &ButtonStyle) { if style.border_width > 0 { // Paint the border and a smaller background on top of it. display::rect_fill_rounded( self.area, style.border_color, style.background_color, style.border_radius, ); display::rect_fill_rounded( self.area.inset(Insets::uniform(style.border_width)), style.button_color, style.border_color, style.border_radius, ); } else { // We do not need to draw an explicit border in this case, just a // bigger background. display::rect_fill_rounded( self.area, style.button_color, style.background_color, style.border_radius, ); } } pub fn paint_content(&self, style: &ButtonStyle) where T: AsRef, { match &self.content { ButtonContent::Empty => {} ButtonContent::Text(text) => { let text = text.as_ref(); let width = style.font.text_width(text); let height = style.font.text_height(); let start_of_baseline = self.area.center() + Offset::new(-width / 2, height / 2) + Offset::y(Self::BASELINE_OFFSET); display::text( start_of_baseline, text, style.font, style.text_color, style.button_color, ); } ButtonContent::Icon(icon) => { display::icon( self.area.center(), icon, style.text_color, style.button_color, ); } } } } impl Component for Button where T: AsRef, { type Msg = ButtonMsg; fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; self.area } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { match event { Event::Touch(TouchEvent::TouchStart(pos)) => { match self.state { State::Disabled => { // Do nothing. } _ => { // Touch started in our area, transform to `Pressed` state. if self.area.contains(pos) { self.set(ctx, State::Pressed); if let Some(duration) = self.long_press { self.long_timer = Some(ctx.request_timer(duration)); } return Some(ButtonMsg::Pressed); } } } } Event::Touch(TouchEvent::TouchMove(pos)) => { match self.state { State::Released if self.area.contains(pos) => { // Touch entered our area, transform to `Pressed` state. self.set(ctx, State::Pressed); return Some(ButtonMsg::Pressed); } State::Pressed if !self.area.contains(pos) => { // Touch is leaving our area, transform to `Released` state. self.set(ctx, State::Released); return Some(ButtonMsg::Released); } _ => { // Do nothing. } } } Event::Touch(TouchEvent::TouchEnd(pos)) => { match self.state { State::Initial | State::Disabled => { // Do nothing. } State::Pressed if self.area.contains(pos) => { // Touch finished in our area, we got clicked. self.set(ctx, State::Initial); return Some(ButtonMsg::Clicked); } _ => { // Touch finished outside our area. self.set(ctx, State::Initial); self.long_timer = None; } } } Event::Timer(token) => { if self.long_timer == Some(token) { self.long_timer = None; if matches!(self.state, State::Pressed) { self.set(ctx, State::Initial); return Some(ButtonMsg::LongPressed); } } } _ => {} }; None } fn paint(&mut self) { let style = self.style(); self.paint_background(style); self.paint_content(style); } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); } } #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Button where T: AsRef + crate::trace::Trace, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("Button"); match &self.content { ButtonContent::Empty => {} ButtonContent::Text(text) => t.field("text", text), ButtonContent::Icon(_) => t.symbol("icon"), } t.close(); } } #[derive(PartialEq, Eq)] enum State { Initial, Pressed, Released, Disabled, } #[derive(PartialEq, Eq)] pub enum ButtonContent { Empty, Text(T), Icon(&'static [u8]), } #[derive(PartialEq, Eq)] pub struct ButtonStyleSheet { pub normal: &'static ButtonStyle, pub active: &'static ButtonStyle, pub disabled: &'static ButtonStyle, } #[derive(PartialEq, Eq)] pub struct ButtonStyle { pub font: Font, pub text_color: Color, pub button_color: Color, pub background_color: Color, pub border_color: Color, pub border_radius: u8, pub border_width: i32, } impl Button { pub fn cancel_confirm( left: Button, right: Button, right_size_factor: usize, ) -> CancelConfirm< T, impl Fn(ButtonMsg) -> Option, impl Fn(ButtonMsg) -> Option, > where T: AsRef, { let columns = 1 + right_size_factor; ( GridPlaced::new(left) .with_grid(1, columns) .with_spacing(theme::BUTTON_SPACING) .with_row_col(0, 0) .map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Cancelled) }), GridPlaced::new(right) .with_grid(1, columns) .with_spacing(theme::BUTTON_SPACING) .with_from_to((0, 1), (0, right_size_factor)) .map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) }), ) } pub fn cancel_confirm_text( left: Option, right: T, ) -> CancelConfirm< T, impl Fn(ButtonMsg) -> Option, impl Fn(ButtonMsg) -> Option, > where T: AsRef, { let (left, right_size_factor) = if let Some(verb) = left { (Button::with_text(verb), 1) } else { (Button::with_icon(theme::ICON_CANCEL), 2) }; let right = Button::with_text(right).styled(theme::button_confirm()); Self::cancel_confirm(left, right, right_size_factor) } pub fn cancel_info_confirm( confirm: T, info: T, ) -> CancelInfoConfirm< T, impl Fn(ButtonMsg) -> Option, impl Fn(ButtonMsg) -> Option, impl Fn(ButtonMsg) -> Option, > where T: AsRef, { let right = Button::with_text(confirm).styled(theme::button_confirm()); let top = Button::with_text(info); let left = Button::with_icon(theme::ICON_CANCEL); ( GridPlaced::new(left) .with_grid(2, 3) .with_spacing(theme::BUTTON_SPACING) .with_row_col(1, 0) .map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Cancelled) }), GridPlaced::new(top) .with_grid(2, 3) .with_spacing(theme::BUTTON_SPACING) .with_from_to((0, 0), (0, 2)) .map(|msg| (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Info)), GridPlaced::new(right) .with_grid(2, 3) .with_spacing(theme::BUTTON_SPACING) .with_from_to((1, 1), (1, 2)) .map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelInfoConfirmMsg::Confirmed) }), ) } } type CancelConfirm = ( Map>, F0>, Map>, F1>, ); pub enum CancelConfirmMsg { Cancelled, Confirmed, } type CancelInfoConfirm = ( Map>, F0>, Map>, F1>, Map>, F2>, ); pub enum CancelInfoConfirmMsg { Cancelled, Info, Confirmed, }