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 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 { // 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, pub offset: Offset, } impl ButtonStyleSheet { pub fn new( normal_color: Color, active_color: Color, with_outline: bool, with_arms: bool, fixed_width: Option, 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, 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, with_outline: bool, with_arms: bool, fixed_width: Option, 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, pub btn_middle: Option, pub btn_right: Option, } impl ButtonLayout { pub fn new( btn_left: Option, btn_middle: Option, btn_right: Option, ) -> 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, pub middle: Option, pub right: Option, } impl ButtonActions { pub const fn new( left: Option, middle: Option, right: Option, ) -> 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 { 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); } } }