You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1042 lines
31 KiB
1042 lines
31 KiB
use crate::{
|
|
strutil::TString,
|
|
time::Duration,
|
|
ui::{
|
|
component::{Component, Event, EventCtx, Never},
|
|
constant,
|
|
display::{self, Color, Font, Icon},
|
|
event::PhysicalButton,
|
|
geometry::{Alignment2D, Offset, Point, Rect},
|
|
shape,
|
|
shape::Renderer,
|
|
},
|
|
};
|
|
|
|
use super::{loader::DEFAULT_DURATION_MS, theme};
|
|
|
|
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
|
|
|
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
|
pub enum ButtonPos {
|
|
Left,
|
|
Middle,
|
|
Right,
|
|
}
|
|
|
|
impl From<PhysicalButton> for ButtonPos {
|
|
fn from(btn: PhysicalButton) -> Self {
|
|
match btn {
|
|
PhysicalButton::Left => ButtonPos::Left,
|
|
PhysicalButton::Right => ButtonPos::Right,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Button {
|
|
bounds: Rect,
|
|
pos: ButtonPos,
|
|
content: ButtonContent,
|
|
styles: ButtonStyleSheet,
|
|
state: State,
|
|
}
|
|
|
|
impl Button {
|
|
pub fn new(pos: ButtonPos, content: ButtonContent, styles: ButtonStyleSheet) -> Self {
|
|
Self {
|
|
pos,
|
|
content,
|
|
styles,
|
|
bounds: Rect::zero(),
|
|
state: State::Released,
|
|
}
|
|
}
|
|
|
|
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails) -> Self {
|
|
// Deciding between text and icon
|
|
let style = btn_details.style();
|
|
match btn_details.content {
|
|
ButtonContent::Text(text) => Self::with_text(pos, text, style),
|
|
ButtonContent::Icon(icon) => Self::with_icon(pos, icon, style),
|
|
}
|
|
}
|
|
|
|
pub fn with_text(pos: ButtonPos, text: TString<'static>, styles: ButtonStyleSheet) -> Self {
|
|
Self::new(pos, ButtonContent::Text(text), styles)
|
|
}
|
|
|
|
pub fn with_icon(pos: ButtonPos, image: Icon, styles: ButtonStyleSheet) -> Self {
|
|
Self::new(pos, ButtonContent::Icon(image), styles)
|
|
}
|
|
|
|
pub fn content(&self) -> &ButtonContent {
|
|
&self.content
|
|
}
|
|
|
|
fn style(&self) -> &ButtonStyle {
|
|
match self.state {
|
|
State::Released => &self.styles.normal,
|
|
State::Pressed => &self.styles.active,
|
|
}
|
|
}
|
|
|
|
/// Changing the icon content of the button.
|
|
pub fn set_icon(&mut self, image: Icon) {
|
|
self.content = ButtonContent::Icon(image);
|
|
}
|
|
|
|
/// Changing the text content of the button.
|
|
pub fn set_text(&mut self, text: TString<'static>) {
|
|
self.content = ButtonContent::Text(text);
|
|
}
|
|
|
|
/// Changing the style of the button.
|
|
pub fn set_style(&mut self, styles: ButtonStyleSheet) {
|
|
self.styles = styles;
|
|
}
|
|
|
|
// Setting the visual state of the button.
|
|
fn set(&mut self, ctx: &mut EventCtx, state: State) {
|
|
if self.state != state {
|
|
self.state = state;
|
|
ctx.request_paint();
|
|
}
|
|
}
|
|
|
|
// Setting the visual state of the button.
|
|
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
|
let new_state = if is_pressed {
|
|
State::Pressed
|
|
} else {
|
|
State::Released
|
|
};
|
|
self.set(ctx, new_state);
|
|
}
|
|
|
|
/// Return the full area of the button according
|
|
/// to its current style, content and position.
|
|
fn get_current_area(&self) -> Rect {
|
|
let style = self.style();
|
|
|
|
// Button width may be forced. Otherwise calculate it.
|
|
let button_width = if let Some(width) = style.fixed_width {
|
|
width
|
|
} else {
|
|
match &self.content {
|
|
ButtonContent::Text(text) => {
|
|
let text_width = text.map(|t| style.font.visible_text_width(t));
|
|
if style.with_outline {
|
|
text_width + 2 * theme::BUTTON_OUTLINE
|
|
} else if style.with_arms {
|
|
text_width + 2 * theme::ARMS_MARGIN
|
|
} else {
|
|
text_width
|
|
}
|
|
}
|
|
ButtonContent::Icon(icon) => {
|
|
// When Icon does not have outline, hardcode its width
|
|
if style.with_outline {
|
|
icon.toif.width() + 2 * theme::BUTTON_OUTLINE
|
|
} else {
|
|
theme::BUTTON_ICON_WIDTH
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Arms should connect to the center, therefore decreasing the height
|
|
let button_height = if style.with_arms {
|
|
theme::BUTTON_HEIGHT - 2
|
|
} else {
|
|
theme::BUTTON_HEIGHT
|
|
};
|
|
let button_bounds = self.bounds.split_bottom(button_height).1;
|
|
match self.pos {
|
|
ButtonPos::Left => button_bounds.split_left(button_width).0,
|
|
ButtonPos::Right => button_bounds.split_right(button_width).1,
|
|
ButtonPos::Middle => button_bounds.split_center(button_width).1,
|
|
}
|
|
}
|
|
|
|
/// Determine baseline point for the text.
|
|
fn get_text_baseline(&self, style: &ButtonStyle) -> Point {
|
|
// Arms and outline require the text to be elevated.
|
|
// Moving text to the right and elevating it for arms and outline.
|
|
let (mut offset_x, offset_y) = if style.with_outline {
|
|
(theme::BUTTON_OUTLINE, theme::BUTTON_OUTLINE)
|
|
} else if style.with_arms {
|
|
(theme::ARMS_MARGIN, theme::ARMS_MARGIN)
|
|
} else {
|
|
(0, 0)
|
|
};
|
|
|
|
// Centering the text in case of fixed width.
|
|
if let ButtonContent::Text(text) = &self.content {
|
|
if let Some(fixed_width) = style.fixed_width {
|
|
let diff = fixed_width - text.map(|t| style.font.visible_text_width(t));
|
|
offset_x = diff / 2;
|
|
}
|
|
}
|
|
|
|
self.get_current_area().bottom_left() + Offset::new(offset_x, -offset_y)
|
|
}
|
|
}
|
|
|
|
impl Component for Button {
|
|
type Msg = Never;
|
|
|
|
fn place(&mut self, bounds: Rect) -> Rect {
|
|
self.bounds = bounds;
|
|
self.get_current_area()
|
|
}
|
|
|
|
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
|
// Events are handled by `ButtonController`
|
|
None
|
|
}
|
|
|
|
fn paint(&mut self) {
|
|
let style = self.style();
|
|
let fg_color = style.text_color;
|
|
let bg_color = fg_color.negate();
|
|
let area = self.get_current_area();
|
|
let inversed_colors = bg_color != theme::BG;
|
|
|
|
// Filling the background (with 2-pixel rounding when applicable)
|
|
if inversed_colors {
|
|
display::rect_outline_rounded(area, bg_color, fg_color, 2);
|
|
display::rect_fill(area.shrink(1), bg_color);
|
|
} else if style.with_outline {
|
|
display::rect_outline_rounded(area, fg_color, bg_color, 2);
|
|
} else {
|
|
display::rect_fill(area, bg_color);
|
|
}
|
|
|
|
// Optionally display "arms" at both sides of content - always in FG and BG
|
|
// colors (they are not inverted).
|
|
if style.with_arms {
|
|
theme::ICON_ARM_LEFT.draw(
|
|
area.left_center(),
|
|
Alignment2D::TOP_RIGHT,
|
|
theme::FG,
|
|
theme::BG,
|
|
);
|
|
theme::ICON_ARM_RIGHT.draw(
|
|
area.right_center(),
|
|
Alignment2D::TOP_LEFT,
|
|
theme::FG,
|
|
theme::BG,
|
|
);
|
|
}
|
|
|
|
// Painting the content
|
|
match &self.content {
|
|
ButtonContent::Text(text) => text.map(|t| {
|
|
display::text_left(
|
|
self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)),
|
|
t,
|
|
style.font,
|
|
fg_color,
|
|
bg_color,
|
|
);
|
|
}),
|
|
ButtonContent::Icon(icon) => {
|
|
// Allowing for possible offset of the area from current style
|
|
let icon_area = area.translate(style.offset);
|
|
if style.with_outline {
|
|
icon.draw(icon_area.center(), Alignment2D::CENTER, fg_color, bg_color);
|
|
} else {
|
|
// Positioning the icon in the corresponding corner/center
|
|
match self.pos {
|
|
ButtonPos::Left => icon.draw(
|
|
icon_area.bottom_left(),
|
|
Alignment2D::BOTTOM_LEFT,
|
|
fg_color,
|
|
bg_color,
|
|
),
|
|
ButtonPos::Right => icon.draw(
|
|
icon_area.bottom_right(),
|
|
Alignment2D::BOTTOM_RIGHT,
|
|
fg_color,
|
|
bg_color,
|
|
),
|
|
ButtonPos::Middle => {
|
|
icon.draw(icon_area.center(), Alignment2D::CENTER, fg_color, bg_color)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
|
let style = self.style();
|
|
let fg_color = style.text_color;
|
|
let bg_color = fg_color.negate();
|
|
let area = self.get_current_area();
|
|
let inversed_colors = bg_color != theme::BG;
|
|
|
|
// Filling the background (with 2-pixel rounding when applicable)
|
|
if inversed_colors {
|
|
shape::Bar::new(area)
|
|
.with_radius(3)
|
|
.with_bg(bg_color)
|
|
.render(target);
|
|
} else if style.with_outline {
|
|
shape::Bar::new(area)
|
|
.with_radius(3)
|
|
.with_fg(fg_color)
|
|
.render(target);
|
|
} else {
|
|
shape::Bar::new(area).with_bg(bg_color).render(target);
|
|
}
|
|
|
|
// Optionally display "arms" at both sides of content - always in FG and BG
|
|
// colors (they are not inverted).
|
|
if style.with_arms {
|
|
shape::ToifImage::new(area.left_center(), theme::ICON_ARM_LEFT.toif)
|
|
.with_align(Alignment2D::TOP_RIGHT)
|
|
.with_fg(theme::FG)
|
|
.render(target);
|
|
|
|
shape::ToifImage::new(area.right_center(), theme::ICON_ARM_RIGHT.toif)
|
|
.with_align(Alignment2D::TOP_LEFT)
|
|
.with_fg(theme::FG)
|
|
.render(target);
|
|
}
|
|
|
|
// Painting the content
|
|
match &self.content {
|
|
ButtonContent::Text(text) => text.map(|t| {
|
|
shape::Text::new(
|
|
self.get_text_baseline(style) - Offset::x(style.font.start_x_bearing(t)),
|
|
t,
|
|
)
|
|
.with_font(style.font)
|
|
.with_fg(fg_color)
|
|
.render(target);
|
|
}),
|
|
ButtonContent::Icon(icon) => {
|
|
// Allowing for possible offset of the area from current style
|
|
let icon_area = area.translate(style.offset);
|
|
if style.with_outline {
|
|
shape::ToifImage::new(icon_area.center(), icon.toif)
|
|
.with_align(Alignment2D::CENTER)
|
|
.with_fg(fg_color)
|
|
.render(target);
|
|
} else {
|
|
// Positioning the icon in the corresponding corner/center
|
|
match self.pos {
|
|
ButtonPos::Left => {
|
|
shape::ToifImage::new(icon_area.bottom_left(), icon.toif)
|
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
|
.with_fg(fg_color)
|
|
.render(target)
|
|
}
|
|
|
|
ButtonPos::Right => {
|
|
shape::ToifImage::new(icon_area.bottom_right(), icon.toif)
|
|
.with_align(Alignment2D::BOTTOM_RIGHT)
|
|
.with_fg(fg_color)
|
|
.render(target)
|
|
}
|
|
|
|
ButtonPos::Middle => shape::ToifImage::new(icon_area.center(), icon.toif)
|
|
.with_align(Alignment2D::CENTER)
|
|
.with_fg(fg_color)
|
|
.render(target),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
enum State {
|
|
Released,
|
|
Pressed,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum ButtonContent {
|
|
Text(TString<'static>),
|
|
Icon(Icon),
|
|
}
|
|
|
|
pub struct ButtonStyleSheet {
|
|
pub normal: ButtonStyle,
|
|
pub active: ButtonStyle,
|
|
}
|
|
|
|
pub struct ButtonStyle {
|
|
pub font: Font,
|
|
pub text_color: Color,
|
|
pub with_outline: bool,
|
|
pub with_arms: bool,
|
|
pub fixed_width: Option<i16>,
|
|
pub offset: Offset,
|
|
}
|
|
|
|
impl ButtonStyleSheet {
|
|
pub fn new(
|
|
normal_color: Color,
|
|
active_color: Color,
|
|
with_outline: bool,
|
|
with_arms: bool,
|
|
fixed_width: Option<i16>,
|
|
offset: Offset,
|
|
) -> Self {
|
|
Self {
|
|
normal: ButtonStyle {
|
|
font: theme::FONT_BUTTON,
|
|
text_color: normal_color,
|
|
with_outline,
|
|
with_arms,
|
|
fixed_width,
|
|
offset,
|
|
},
|
|
active: ButtonStyle {
|
|
font: theme::FONT_BUTTON,
|
|
text_color: active_color,
|
|
with_outline,
|
|
with_arms,
|
|
fixed_width,
|
|
offset,
|
|
},
|
|
}
|
|
}
|
|
|
|
// White text in normal mode.
|
|
pub fn default(
|
|
with_outline: bool,
|
|
with_arms: bool,
|
|
fixed_width: Option<i16>,
|
|
offset: Offset,
|
|
) -> Self {
|
|
Self::new(
|
|
theme::FG,
|
|
theme::BG,
|
|
with_outline,
|
|
with_arms,
|
|
fixed_width,
|
|
offset,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Describing the button on the screen - only visuals.
|
|
#[derive(Clone)]
|
|
pub struct ButtonDetails {
|
|
pub content: ButtonContent,
|
|
pub duration: Option<Duration>,
|
|
with_outline: bool,
|
|
with_arms: bool,
|
|
fixed_width: Option<i16>,
|
|
offset: Offset,
|
|
pub send_long_press: bool,
|
|
}
|
|
|
|
impl ButtonDetails {
|
|
/// Text button.
|
|
pub fn text(text: TString<'static>) -> Self {
|
|
Self {
|
|
content: ButtonContent::Text(text),
|
|
duration: None,
|
|
with_outline: true,
|
|
with_arms: false,
|
|
fixed_width: None,
|
|
offset: Offset::zero(),
|
|
send_long_press: false,
|
|
}
|
|
}
|
|
|
|
/// Icon button.
|
|
pub fn icon(icon: Icon) -> Self {
|
|
Self {
|
|
content: ButtonContent::Icon(icon),
|
|
duration: None,
|
|
with_outline: false,
|
|
with_arms: false,
|
|
fixed_width: None,
|
|
offset: Offset::zero(),
|
|
send_long_press: false,
|
|
}
|
|
}
|
|
|
|
/// Resolves text and finds possible icon names.
|
|
pub fn from_text_possible_icon(text: TString<'static>) -> Self {
|
|
text.map(|t| match t {
|
|
"" => Self::cancel_icon(),
|
|
"<" => Self::left_arrow_icon(),
|
|
"^" => Self::up_arrow_icon(),
|
|
_ => Self::text(text),
|
|
})
|
|
}
|
|
|
|
/// Text with arms signalling double press.
|
|
pub fn armed_text(text: TString<'static>) -> Self {
|
|
Self::text(text).with_arms()
|
|
}
|
|
|
|
/// Cross-style-icon cancel button with no outline.
|
|
pub fn cancel_icon() -> Self {
|
|
Self::icon(theme::ICON_CANCEL).with_offset(Offset::new(3, -3))
|
|
}
|
|
|
|
/// Left arrow to signal going back. No outline.
|
|
pub fn left_arrow_icon() -> Self {
|
|
Self::icon(theme::ICON_ARROW_LEFT).with_offset(Offset::new(4, -3))
|
|
}
|
|
|
|
/// Right arrow to signal going forward. No outline.
|
|
pub fn right_arrow_icon() -> Self {
|
|
Self::icon(theme::ICON_ARROW_RIGHT).with_offset(Offset::new(-4, -3))
|
|
}
|
|
|
|
/// Up arrow to signal paginating back. No outline. Offsetted little right
|
|
/// to not be on the boundary.
|
|
pub fn up_arrow_icon() -> Self {
|
|
Self::icon(theme::ICON_ARROW_UP).with_offset(Offset::new(3, -4))
|
|
}
|
|
|
|
/// Down arrow to signal paginating forward. Takes half the screen's width
|
|
pub fn down_arrow_icon_wide() -> Self {
|
|
Self::icon(theme::ICON_ARROW_DOWN)
|
|
.with_outline(true)
|
|
.with_fixed_width(HALF_SCREEN_BUTTON_WIDTH)
|
|
}
|
|
|
|
/// Up arrow to signal paginating back. Takes half the screen's width
|
|
pub fn up_arrow_icon_wide() -> Self {
|
|
Self::icon(theme::ICON_ARROW_UP)
|
|
.with_outline(true)
|
|
.with_fixed_width(HALF_SCREEN_BUTTON_WIDTH)
|
|
}
|
|
|
|
/// Possible outline around the button.
|
|
pub fn with_outline(mut self, outline: bool) -> Self {
|
|
self.with_outline = outline;
|
|
self
|
|
}
|
|
|
|
/// Positioning the icon precisely where we want.
|
|
/// Buttons are by default placed exactly in the corners (left/right)
|
|
/// or in the center in case of center button. The offset can change it.
|
|
pub fn with_offset(mut self, offset: Offset) -> Self {
|
|
self.offset = offset;
|
|
self
|
|
}
|
|
|
|
/// Left and right "arms" around the button.
|
|
/// Automatically disabling the outline.
|
|
pub fn with_arms(mut self) -> Self {
|
|
self.with_arms = true;
|
|
self.with_outline = false;
|
|
self
|
|
}
|
|
|
|
/// Default duration of the hold-to-confirm - 1 second.
|
|
pub fn with_default_duration(mut self) -> Self {
|
|
self.duration = Some(Duration::from_millis(DEFAULT_DURATION_MS));
|
|
self
|
|
}
|
|
|
|
/// Specific duration of the hold-to-confirm.
|
|
pub fn with_duration(mut self, duration: Duration) -> Self {
|
|
self.duration = Some(duration);
|
|
self
|
|
}
|
|
|
|
/// Specifying the width of the button.
|
|
pub fn with_fixed_width(mut self, width: i16) -> Self {
|
|
self.fixed_width = Some(width);
|
|
self
|
|
}
|
|
|
|
/// Button style that should be applied.
|
|
pub fn style(&self) -> ButtonStyleSheet {
|
|
ButtonStyleSheet::default(
|
|
self.with_outline,
|
|
self.with_arms,
|
|
self.fixed_width,
|
|
self.offset,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Holding the button details for all three possible buttons.
|
|
#[derive(Clone)]
|
|
pub struct ButtonLayout {
|
|
pub btn_left: Option<ButtonDetails>,
|
|
pub btn_middle: Option<ButtonDetails>,
|
|
pub btn_right: Option<ButtonDetails>,
|
|
}
|
|
|
|
impl ButtonLayout {
|
|
pub fn new(
|
|
btn_left: Option<ButtonDetails>,
|
|
btn_middle: Option<ButtonDetails>,
|
|
btn_right: Option<ButtonDetails>,
|
|
) -> Self {
|
|
Self {
|
|
btn_left,
|
|
btn_middle,
|
|
btn_right,
|
|
}
|
|
}
|
|
|
|
/// Empty layout for when we cannot yet tell which buttons
|
|
/// should be on the screen.
|
|
pub fn empty() -> Self {
|
|
Self::new(None, None, None)
|
|
}
|
|
|
|
/// Arrows at sides, armed text in the middle.
|
|
pub fn arrow_armed_arrow(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::left_arrow_icon()),
|
|
Some(ButtonDetails::armed_text(text)),
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Left cancel, armed text and next right arrow.
|
|
pub fn cancel_armed_arrow(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
Some(ButtonDetails::armed_text(text)),
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Middle armed text and next right arrow.
|
|
pub fn none_armed_arrow(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
None,
|
|
Some(ButtonDetails::armed_text(text)),
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Left text, armed text and right info icon/text.
|
|
pub fn text_armed_info(left: TString<'static>, middle: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::from_text_possible_icon(left)),
|
|
Some(ButtonDetails::armed_text(middle)),
|
|
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
|
|
)
|
|
}
|
|
|
|
/// Left cancel, armed text and right info icon/text.
|
|
pub fn cancel_armed_info(middle: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
Some(ButtonDetails::armed_text(middle)),
|
|
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
|
|
)
|
|
}
|
|
|
|
/// Left cancel, armed text and blank on right.
|
|
pub fn cancel_armed_none(middle: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
Some(ButtonDetails::armed_text(middle)),
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Left back arrow and middle armed text.
|
|
pub fn arrow_armed_none(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::left_arrow_icon()),
|
|
Some(ButtonDetails::armed_text(text)),
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Left and right texts.
|
|
pub fn text_none_text(left: TString<'static>, right: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::from_text_possible_icon(left)),
|
|
None,
|
|
Some(ButtonDetails::from_text_possible_icon(right)),
|
|
)
|
|
}
|
|
|
|
/// Left text and right arrow.
|
|
pub fn text_none_arrow(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
None,
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Left text and WIDE right arrow.
|
|
pub fn text_none_arrow_wide(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
None,
|
|
Some(ButtonDetails::down_arrow_icon_wide()),
|
|
)
|
|
}
|
|
|
|
/// Only right text.
|
|
pub fn none_none_text(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
None,
|
|
None,
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
)
|
|
}
|
|
|
|
/// Left and right arrow icons for navigation.
|
|
pub fn arrow_none_arrow() -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::left_arrow_icon()),
|
|
None,
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Left arrow and right text.
|
|
pub fn arrow_none_text(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::left_arrow_icon()),
|
|
None,
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
)
|
|
}
|
|
|
|
/// Up arrow left and right text.
|
|
pub fn up_arrow_none_text(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::up_arrow_icon()),
|
|
None,
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
)
|
|
}
|
|
|
|
/// Cancel cross on left and right arrow.
|
|
pub fn cancel_none_arrow() -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
None,
|
|
Some(ButtonDetails::right_arrow_icon()),
|
|
)
|
|
}
|
|
|
|
/// Cancel cross on left and right arrow facing down.
|
|
pub fn cancel_none_arrow_wide() -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
None,
|
|
Some(ButtonDetails::down_arrow_icon_wide()),
|
|
)
|
|
}
|
|
|
|
/// Up arrow on left and right arrow facing down.
|
|
pub fn up_arrow_none_arrow_wide() -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::up_arrow_icon()),
|
|
None,
|
|
Some(ButtonDetails::down_arrow_icon_wide()),
|
|
)
|
|
}
|
|
|
|
/// Up arrow on left, middle text and info on the right.
|
|
pub fn up_arrow_armed_info(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::up_arrow_icon()),
|
|
Some(ButtonDetails::armed_text(text)),
|
|
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
|
|
)
|
|
}
|
|
|
|
/// Cancel cross on left and right arrow facing down.
|
|
pub fn cancel_none_arrow_down() -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
None,
|
|
Some(ButtonDetails::down_arrow_icon_wide()),
|
|
)
|
|
}
|
|
|
|
/// Cancel cross on left and text on the right.
|
|
pub fn cancel_none_text(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
None,
|
|
Some(ButtonDetails::from_text_possible_icon(text)),
|
|
)
|
|
}
|
|
|
|
/// Cancel cross on left and hold-to-confirm text on the right.
|
|
pub fn cancel_none_htc(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::cancel_icon()),
|
|
None,
|
|
Some(ButtonDetails::text(text).with_default_duration()),
|
|
)
|
|
}
|
|
|
|
/// Arrow back on left and hold-to-confirm text on the right.
|
|
pub fn arrow_none_htc(text: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::left_arrow_icon()),
|
|
None,
|
|
Some(ButtonDetails::text(text).with_default_duration()),
|
|
)
|
|
}
|
|
|
|
/// Only armed text in the middle.
|
|
pub fn none_armed_none(text: TString<'static>) -> Self {
|
|
Self::new(None, Some(ButtonDetails::armed_text(text)), None)
|
|
}
|
|
|
|
/// HTC on both sides.
|
|
pub fn htc_none_htc(left: TString<'static>, right: TString<'static>) -> Self {
|
|
Self::new(
|
|
Some(ButtonDetails::text(left).with_default_duration()),
|
|
None,
|
|
Some(ButtonDetails::text(right).with_default_duration()),
|
|
)
|
|
}
|
|
|
|
/// Only left arrow.
|
|
pub fn arrow_none_none() -> Self {
|
|
Self::new(Some(ButtonDetails::left_arrow_icon()), None, None)
|
|
}
|
|
|
|
/// Only right arrow facing down.
|
|
pub fn none_none_arrow_wide() -> Self {
|
|
Self::new(None, None, Some(ButtonDetails::down_arrow_icon_wide()))
|
|
}
|
|
}
|
|
|
|
/// What happens when a button is triggered.
|
|
/// Theoretically any action can be connected
|
|
/// with any button.
|
|
#[derive(Clone, PartialEq, Eq, Copy)]
|
|
pub enum ButtonAction {
|
|
/// Go to the next page of this flow
|
|
NextPage,
|
|
/// Go to the previous page of this flow
|
|
PrevPage,
|
|
/// Go to the first page of this flow
|
|
FirstPage,
|
|
/// Go to the last page of this flow
|
|
LastPage,
|
|
/// Cancel the whole layout - send Msg::Cancelled
|
|
Cancel,
|
|
/// Confirm the whole layout - send Msg::Confirmed
|
|
Confirm,
|
|
/// Send INFO message from layout - send Msg::Info
|
|
Info,
|
|
}
|
|
|
|
/// Storing actions for all three possible buttons.
|
|
#[derive(Clone, Copy)]
|
|
pub struct ButtonActions {
|
|
pub left: Option<ButtonAction>,
|
|
pub middle: Option<ButtonAction>,
|
|
pub right: Option<ButtonAction>,
|
|
}
|
|
|
|
impl ButtonActions {
|
|
pub const fn new(
|
|
left: Option<ButtonAction>,
|
|
middle: Option<ButtonAction>,
|
|
right: Option<ButtonAction>,
|
|
) -> Self {
|
|
Self {
|
|
left,
|
|
middle,
|
|
right,
|
|
}
|
|
}
|
|
|
|
/// Going back with left, going further with right
|
|
pub fn prev_none_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::PrevPage),
|
|
None,
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Going back with left, going further with middle
|
|
pub fn prev_next_none() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::PrevPage),
|
|
Some(ButtonAction::NextPage),
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Previous with left, confirming with right
|
|
pub fn prev_none_confirm() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::PrevPage),
|
|
None,
|
|
Some(ButtonAction::Confirm),
|
|
)
|
|
}
|
|
|
|
/// Previous with left, confirming with middle
|
|
pub fn prev_confirm_none() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::PrevPage),
|
|
Some(ButtonAction::Confirm),
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Only confirming with middle
|
|
pub fn none_confirm_none() -> Self {
|
|
Self::new(None, Some(ButtonAction::Confirm), None)
|
|
}
|
|
|
|
/// Going to last page with left, to the next page with right
|
|
pub fn last_none_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::LastPage),
|
|
None,
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Going to last page with left, to the next page with right and confirm
|
|
/// with middle
|
|
pub fn last_confirm_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::LastPage),
|
|
Some(ButtonAction::Confirm),
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Going to previous page with left, to the next page with right and
|
|
/// confirm with middle
|
|
pub fn prev_confirm_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::PrevPage),
|
|
Some(ButtonAction::Confirm),
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Cancelling with left, going to the next page with right
|
|
pub fn cancel_none_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::Cancel),
|
|
None,
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Only going to the next page with right
|
|
pub fn none_none_next() -> Self {
|
|
Self::new(None, None, Some(ButtonAction::NextPage))
|
|
}
|
|
|
|
/// Only going to the next page with middle
|
|
pub fn none_next_none() -> Self {
|
|
Self::new(None, Some(ButtonAction::NextPage), None)
|
|
}
|
|
|
|
/// Only going to the prev page with left
|
|
pub fn prev_none_none() -> Self {
|
|
Self::new(Some(ButtonAction::PrevPage), None, None)
|
|
}
|
|
|
|
/// Cancelling with left, confirming with right
|
|
pub fn cancel_none_confirm() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::Cancel),
|
|
None,
|
|
Some(ButtonAction::Confirm),
|
|
)
|
|
}
|
|
|
|
/// Cancelling with left, confirming with middle and next with right
|
|
pub fn cancel_confirm_next() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::Cancel),
|
|
Some(ButtonAction::Confirm),
|
|
Some(ButtonAction::NextPage),
|
|
)
|
|
}
|
|
|
|
/// Cancelling with left, confirming with middle and info with right
|
|
pub fn cancel_confirm_info() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::Cancel),
|
|
Some(ButtonAction::Confirm),
|
|
Some(ButtonAction::Info),
|
|
)
|
|
}
|
|
|
|
/// Going to the beginning with left, confirming with right
|
|
pub fn beginning_none_confirm() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::FirstPage),
|
|
None,
|
|
Some(ButtonAction::Confirm),
|
|
)
|
|
}
|
|
|
|
/// Going to the beginning with left, cancelling with right
|
|
pub fn beginning_none_cancel() -> Self {
|
|
Self::new(
|
|
Some(ButtonAction::FirstPage),
|
|
None,
|
|
Some(ButtonAction::Cancel),
|
|
)
|
|
}
|
|
|
|
/// Having access to appropriate action based on the `ButtonPos`
|
|
pub fn get_action(&self, pos: ButtonPos) -> Option<ButtonAction> {
|
|
match pos {
|
|
ButtonPos::Left => self.left,
|
|
ButtonPos::Middle => self.middle,
|
|
ButtonPos::Right => self.right,
|
|
}
|
|
}
|
|
}
|
|
|
|
// DEBUG-ONLY SECTION BELOW
|
|
|
|
#[cfg(feature = "ui_debug")]
|
|
impl crate::trace::Trace for Button {
|
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
|
t.component("Button");
|
|
match self.content {
|
|
ButtonContent::Text(text) => t.string("text", text),
|
|
ButtonContent::Icon(icon) => {
|
|
t.null("text");
|
|
t.string("icon", icon.name.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ui_debug")]
|
|
impl crate::trace::Trace for ButtonDetails {
|
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
|
t.component("ButtonDetails");
|
|
match self.content {
|
|
ButtonContent::Text(text) => {
|
|
t.string("text", text);
|
|
}
|
|
ButtonContent::Icon(icon) => {
|
|
t.null("text");
|
|
t.string("icon", icon.name.into());
|
|
}
|
|
}
|
|
if let Some(duration) = &self.duration {
|
|
t.int("hold_to_confirm", duration.to_millis() as i64);
|
|
}
|
|
}
|
|
}
|