diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index d3de52984..d988df677 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -528,6 +528,7 @@ static void _librust_qstrs(void) { MP_QSTR_show_share_words; MP_QSTR_show_simple; MP_QSTR_show_success; + MP_QSTR_show_tx_context_menu; MP_QSTR_show_wait_text; MP_QSTR_show_warning; MP_QSTR_sign; diff --git a/core/embed/rust/src/ui/model_mercury/component/button.rs b/core/embed/rust/src/ui/model_mercury/component/button.rs index a97f74651..a3c8f6a33 100644 --- a/core/embed/rust/src/ui/model_mercury/component/button.rs +++ b/core/embed/rust/src/ui/model_mercury/component/button.rs @@ -34,9 +34,10 @@ pub struct Button { } impl Button { - /// Offsets the baseline of the button text either up (negative) or down - /// (positive). - pub const BASELINE_OFFSET: i16 = -2; + /// Offsets the baseline of the button text + /// -x/+x => left/right + /// -y/+y => up/down + pub const BASELINE_OFFSET: Offset = Offset::new(2, 6); pub const fn new(content: ButtonContent) -> Self { Self { @@ -58,8 +59,8 @@ impl Button { Self::new(ButtonContent::Icon(icon)) } - pub const fn with_icon_and_text(content: IconText) -> Self { - Self::new(ButtonContent::IconAndText(content)) + pub const fn with_icon_and_text(content: IconText) -> Self { + Self::new(ButtonContent::IconAndText::(content)) } pub const fn with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self { @@ -211,11 +212,7 @@ impl Button { 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); + let start_of_baseline = self.area.center() + Self::BASELINE_OFFSET; display::text_left( start_of_baseline, text, @@ -252,11 +249,7 @@ impl Button { 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); + let start_of_baseline = self.area.left_center() + Self::BASELINE_OFFSET; shape::Text::new(start_of_baseline, text) .with_font(style.font) .with_fg(style.text_color) @@ -269,7 +262,7 @@ impl Button { .render(target); } ButtonContent::IconAndText(child) => { - child.paint(self.area, self.style(), Self::BASELINE_OFFSET); + child.render(target, self.area, style, Self::BASELINE_OFFSET); } ButtonContent::IconBlend(bg, fg, offset) => { shape::Bar::new(self.area) @@ -399,7 +392,7 @@ where ButtonContent::Text(text) => t.string("text", text.as_ref().into()), ButtonContent::Icon(_) => t.bool("icon", true), ButtonContent::IconAndText(content) => { - t.string("text", content.text.into()); + t.string("text", content.text.as_ref().into()); t.bool("icon", true); } ButtonContent::IconBlend(_, _, _) => t.bool("icon", true), @@ -420,7 +413,7 @@ pub enum ButtonContent { Empty, Text(T), Icon(Icon), - IconAndText(IconText), + IconAndText(IconText), IconBlend(Icon, Icon, Offset), } @@ -538,43 +531,6 @@ impl Button { total_height, ) } - - pub fn select_word( - words: [T; 3], - ) -> CancelInfoConfirm< - T, - impl Fn(ButtonMsg) -> Option, - impl Fn(ButtonMsg) -> Option, - impl Fn(ButtonMsg) -> Option, - > - where - T: AsRef, - { - let btn = move |i, word| { - Button::with_text(word) - .styled(theme::button_pin()) - .map(move |msg| { - (matches!(msg, ButtonMsg::Clicked)).then(|| SelectWordMsg::Selected(i)) - }) - }; - - let [top, middle, bottom] = words; - let total_height = 3 * theme::BUTTON_HEIGHT + 2 * theme::BUTTON_SPACING; - FixedHeightBar::bottom( - Split::top( - theme::BUTTON_HEIGHT, - theme::BUTTON_SPACING, - btn(0, top), - Split::top( - theme::BUTTON_HEIGHT, - theme::BUTTON_SPACING, - btn(1, middle), - btn(2, bottom), - ), - ), - total_height, - ) - } } pub enum CancelConfirmMsg { @@ -600,23 +556,25 @@ pub enum SelectWordMsg { } #[derive(PartialEq, Eq)] -pub struct IconText { - text: &'static str, +pub struct IconText { + text: T, icon: Icon, } -impl IconText { +impl IconText +where + T: AsRef, +{ const ICON_SPACE: i16 = 46; const ICON_MARGIN: i16 = 4; const TEXT_MARGIN: i16 = 6; - pub fn new(text: &'static str, icon: Icon) -> Self { + pub fn new(text: T, icon: Icon) -> Self { Self { text, icon } } - pub fn paint(&self, area: Rect, style: &ButtonStyle, baseline_offset: i16) { - let width = style.font.text_width(self.text); - let height = style.font.text_height(); + pub fn paint(&self, area: Rect, style: &ButtonStyle, baseline_offset: Offset) { + let width = style.font.text_width(self.text.as_ref()); let mut use_icon = false; let mut use_text = false; @@ -625,8 +583,7 @@ impl IconText { area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2), area.center().y, ); - let mut text_pos = - area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset); + let mut text_pos = area.left_center() + baseline_offset; if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) { //display both icon and text @@ -644,7 +601,7 @@ impl IconText { if use_text { display::text_left( text_pos, - self.text, + self.text.as_ref(), style.font, style.text_color, style.button_color, @@ -660,4 +617,47 @@ impl IconText { ); } } + pub fn render<'s>( + & self, + target: &mut impl Renderer<'s>, + area: Rect, + style: &ButtonStyle, + baseline_offset: Offset, + ) { + let width = style.font.text_width(self.text.as_ref()); + + let mut use_icon = false; + let mut use_text = false; + + let mut icon_pos = Point::new( + area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2), + area.center().y, + ); + let mut text_pos = area.left_center() + baseline_offset; + + if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) { + //display both icon and text + text_pos = Point::new(area.top_left().x + Self::ICON_SPACE, text_pos.y); + use_text = true; + use_icon = true; + } else if area.width() > (width + Self::TEXT_MARGIN) { + use_text = true; + } else { + //if we can't fit the text, retreat to centering the icon + icon_pos = area.center(); + use_icon = true; + } + + if use_text { + shape::Text::new(text_pos, self.text.as_ref()) + .with_fg(style.text_color) + .render(target); + } + if use_icon { + shape::ToifImage::new(icon_pos, self.icon.toif) + .with_align(Alignment2D::CENTER) + .with_fg(style.text_color) + .render(target); + } + } } diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index e34c15699..fe81276ec 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -61,7 +61,7 @@ where } pub fn with_subtitle(mut self, subtitle: U) -> Self { - let style = theme::label_title_sub(); + let style = theme::TEXT_SUB; self.title = Child::new(self.title.into_inner().top_aligned()); self.subtitle = Some(Child::new(Label::new( subtitle, @@ -126,7 +126,7 @@ where fn place(&mut self, bounds: Rect) -> Rect { let (mut header_area, content_area) = bounds.split_top(TITLE_HEIGHT); - let content_area = content_area.inset(Insets::right(TITLE_SPACE)); + let content_area = content_area.inset(Insets::top(TITLE_SPACE)); if let Some(b) = &mut self.button { let (rest, button_area) = header_area.split_right(TITLE_HEIGHT); diff --git a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs index 13765bba2..1d85c744c 100644 --- a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs @@ -22,7 +22,7 @@ use crate::{ ui::{ constant::HEIGHT, display::{ - tjpgd::{jpeg_test, BufferInput}, + tjpgd::BufferInput, toif::{Toif, ToifFormat}, }, model_mercury::component::homescreen::render::{ diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index ec6d1b0b7..98e8033bf 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -6,6 +6,7 @@ mod button; mod coinjoin_progress; mod dialog; mod fido; +mod vertical_menu; #[rustfmt::skip] mod fido_icons; mod error; @@ -57,6 +58,7 @@ pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; pub use simple_page::SimplePage; pub use swipe::{Swipe, SwipeDirection}; +pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg}; pub use welcome_screen::WelcomeScreen; use super::theme; diff --git a/core/embed/rust/src/ui/model_mercury/component/number_input.rs b/core/embed/rust/src/ui/model_mercury/component/number_input.rs index 43aab67cb..891bb8ab2 100644 --- a/core/embed/rust/src/ui/model_mercury/component/number_input.rs +++ b/core/embed/rust/src/ui/model_mercury/component/number_input.rs @@ -226,7 +226,7 @@ impl Component for NumberInput { let mut buf = [0u8; 10]; if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { let digit_font = Font::DEMIBOLD; - let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET; + let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET.y; display::rect_fill(self.area, theme::BG); display::text_center( self.area.center() + Offset::y(y_offset), @@ -245,7 +245,7 @@ impl Component for NumberInput { if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { let digit_font = Font::DEMIBOLD; - let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET; + let y_offset = digit_font.text_height() / 2 + Button::<&str>::BASELINE_OFFSET.y; shape::Bar::new(self.area).with_bg(theme::BG).render(target); shape::Text::new(self.area.center() + Offset::y(y_offset), text) diff --git a/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs new file mode 100644 index 000000000..575819b70 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/vertical_menu.rs @@ -0,0 +1,156 @@ +use heapless::Vec; + +use super::theme; +use crate::ui::{ + component::{base::Component, Event, EventCtx}, + display::Icon, + geometry::Rect, + model_mercury::component::button::{Button, ButtonMsg, IconText}, + shape::{Bar, Renderer}, +}; + +pub enum VerticalMenuChoiceMsg { + Selected(usize), +} + +/// Number of buttons. +/// Presently, VerticalMenu holds only fixed number of buttons. +/// TODO: for scrollable menu, the implementation must change. +const N_ITEMS: usize = 3; + +/// Number of visual separators between buttons. +const N_SEPS: usize = N_ITEMS - 1; + +/// Fixed height of each menu button. +const MENU_BUTTON_HEIGHT: i16 = 64; + +/// Fixed height of a separator. +const MENU_SEP_HEIGHT: i16 = 2; + +type VerticalMenuButtons = Vec, N_ITEMS>; +type AreasForSeparators = Vec; + +pub struct VerticalMenu { + area: Rect, + /// buttons placed vertically from top to bottom + buttons: VerticalMenuButtons, + /// areas for visual separators between buttons + areas_sep: AreasForSeparators, +} + +impl VerticalMenu +where + T: AsRef, +{ + fn new(buttons: VerticalMenuButtons) -> Self { + Self { + area: Rect::zero(), + buttons, + areas_sep: AreasForSeparators::new(), + } + } + pub fn select_word(words: [T; 3]) -> Self { + let mut buttons_vec = VerticalMenuButtons::new(); + for word in words { + let button = Button::with_text(word).styled(theme::button_vertical_menu()); + unwrap!(buttons_vec.push(button)); + } + Self::new(buttons_vec) + } + + pub fn context_menu(options: [(T, Icon); 3]) -> Self { + // TODO: this is just POC + let mut buttons_vec = VerticalMenuButtons::new(); + for opt in options { + let button_theme; + match opt.1 { + theme::ICON_CANCEL => { + button_theme = theme::button_vertical_menu_orange(); + } + _ => { + button_theme = theme::button_vertical_menu(); + } + } + unwrap!(buttons_vec.push( + Button::with_icon_and_text(IconText::new(opt.0, opt.1)).styled(button_theme) + )); + } + Self::new(buttons_vec) + } +} + +impl Component for VerticalMenu +where + T: AsRef, +{ + type Msg = VerticalMenuChoiceMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + // VerticalMenu is supposed to be used in Frame, the remaining space is just + // enought to fit 3 buttons separated by thin bars + let height_bounds_expected = 3 * MENU_BUTTON_HEIGHT + 2 * MENU_SEP_HEIGHT; + assert!(bounds.height() == height_bounds_expected); + + self.area = bounds; + self.areas_sep.clear(); + let mut remaining = bounds; + for i in 0..N_ITEMS { + let (area_button, new_remaining) = remaining.split_top(MENU_BUTTON_HEIGHT); + self.buttons[i].place(area_button); + remaining = new_remaining; + if i < N_SEPS { + let (area_sep, new_remaining) = remaining.split_top(MENU_SEP_HEIGHT); + unwrap!(self.areas_sep.push(area_sep)); + remaining = new_remaining; + } + } + + self.area + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + for (i, button) in self.buttons.iter_mut().enumerate() { + if let Some(ButtonMsg::Clicked) = button.event(ctx, event) { + return Some(VerticalMenuChoiceMsg::Selected(i)); + } + } + None + } + + fn paint(&mut self) { + // TODO remove when ui-t3t1 done + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + // render buttons separated by thin bars + for button in &self.buttons { + button.render(target); + } + for area in self.areas_sep.iter() { + Bar::new(*area) + .with_thickness(MENU_SEP_HEIGHT) + .with_fg(theme::GREY_EXTRA_DARK) + .render(target); + } + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for VerticalMenu +where + T: AsRef, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("VerticalMenu"); + t.in_list("buttons", &|button_list| { + for button in &self.buttons { + button_list.child(button); + } + }); + } +} diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index a2733bb83..d217ca171 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -34,7 +34,7 @@ use crate::{ }, Border, Component, Empty, FormattedText, Label, Never, Qr, Timeout, }, - display::tjpgd::jpeg_info, + display::{tjpgd::jpeg_info, Icon}, geometry, layout::{ obj::{ComponentMsgObj, LayoutObj}, @@ -52,7 +52,8 @@ use super::{ FidoMsg, Frame, FrameMsg, Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress, - SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, + SelectWordCount, SelectWordCountMsg, SelectWordMsg, SimplePage, Slip39Input, VerticalMenu, + VerticalMenuChoiceMsg, }, theme, }; @@ -100,6 +101,16 @@ impl TryFrom for Obj { } } +impl TryFrom for Obj { + type Error = Error; + + fn try_from(value: VerticalMenuChoiceMsg) -> Result { + match value { + VerticalMenuChoiceMsg::Selected(i) => i.try_into(), + } + } +} + impl ComponentMsgObj for FidoConfirm where F: Fn(usize) -> T, @@ -195,6 +206,17 @@ where } } +impl ComponentMsgObj for VerticalMenu +where + T: AsRef, +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + VerticalMenuChoiceMsg::Selected(i) => i.try_into(), + } + } +} + impl ComponentMsgObj for ButtonPage where T: Component + Paginate, @@ -1299,11 +1321,30 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words: [StrBuffer; 3] = util::iter_into_array(words_iterable)?; - let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_DEMIBOLD, description)]); - let obj = LayoutObj::new(Frame::left_aligned( - title, - Dialog::new(paragraphs, Button::select_word(words)), - ))?; + let content = VerticalMenu::select_word(words); + let frame_with_menu = Frame::left_aligned(title, content).with_subtitle(description); + let obj = LayoutObj::new(frame_with_menu)?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_tx_context_menu(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], _kwargs: &Map| { + // TODO: this is just POC + let title: StrBuffer = StrBuffer::from(""); + + let options: [(StrBuffer, Icon); 3] = [ + (StrBuffer::from("Address QR code"), theme::ICON_QR_CODE), + ( + StrBuffer::from("Fee info"), + theme::ICON_CHEVRON_RIGHT, + ), + (StrBuffer::from("Cancel transaction"), theme::ICON_CANCEL), + ]; + let content = VerticalMenu::context_menu(options); + let frame_with_menu = Frame::left_aligned(title, content).with_cancel_button(); + let obj = LayoutObj::new(frame_with_menu)?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -2033,6 +2074,11 @@ pub static mp_module_trezorui2: Module = obj_module! { /// iterable must be of exact size. Returns index in range `0..3`.""" Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), + /// def show_tx_context_menu() -> LayoutObj[int]: + /// """Show transaction context menu with the options for 1) Address QR code, 2) Fee + /// information, 3) Cancel transaction""" + Qstr::MP_QSTR_show_tx_context_menu => obj_fn_kw!(0, new_show_tx_context_menu).as_obj(), + /// def show_share_words( /// *, /// title: str, diff --git a/core/embed/rust/src/ui/model_mercury/res/chevron_right24.png b/core/embed/rust/src/ui/model_mercury/res/chevron_right24.png index 5dea415ff..efe01e116 100644 Binary files a/core/embed/rust/src/ui/model_mercury/res/chevron_right24.png and b/core/embed/rust/src/ui/model_mercury/res/chevron_right24.png differ diff --git a/core/embed/rust/src/ui/model_mercury/res/chevron_right24.toif b/core/embed/rust/src/ui/model_mercury/res/chevron_right24.toif index 32666c3ec..8333079d6 100644 Binary files a/core/embed/rust/src/ui/model_mercury/res/chevron_right24.toif and b/core/embed/rust/src/ui/model_mercury/res/chevron_right24.toif differ diff --git a/core/embed/rust/src/ui/model_mercury/theme/mod.rs b/core/embed/rust/src/ui/model_mercury/theme/mod.rs index 4919d6993..f33f0b754 100644 --- a/core/embed/rust/src/ui/model_mercury/theme/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/theme/mod.rs @@ -57,7 +57,7 @@ pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E); pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41); // Commonly used corner radius (i.e. for buttons). -pub const RADIUS: u8 = 2; +pub const RADIUS: u8 = 0; // Full-size QR code. pub const QR_SIDE_MAX: u32 = 140; @@ -416,6 +416,70 @@ pub const fn button_info() -> ButtonStyleSheet { } } +pub const fn button_vertical_menu() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: Font::NORMAL, + text_color: GREY_EXTRA_LIGHT, + button_color: BG, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + // TODO: change when figma done + active: &ButtonStyle { + font: Font::NORMAL, + text_color: FG, + button_color: GREEN_LIME, + background_color: GREY_EXTRA_DARK, + border_color: GREEN_LIME, + border_radius: RADIUS, + border_width: 0, + }, + disabled: &ButtonStyle { + font: Font::NORMAL, + text_color: GREY_LIGHT, + button_color: GREEN_LIME, + background_color: GREY_EXTRA_DARK, + border_color: GREEN_LIME, + border_radius: RADIUS, + border_width: 0, + }, + } +} + +pub const fn button_vertical_menu_orange() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: Font::NORMAL, + text_color: ORANGE_LIGHT, + button_color: BG, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + active: &ButtonStyle { + font: Font::NORMAL, + text_color: FG, + button_color: GREEN_LIME, + background_color: GREY_EXTRA_DARK, + border_color: GREEN_LIME, + border_radius: RADIUS, + border_width: 0, + }, + disabled: &ButtonStyle { + font: Font::NORMAL, + text_color: GREY_LIGHT, + button_color: GREEN_LIME, + background_color: GREY_EXTRA_DARK, + border_color: GREEN_LIME, + border_radius: RADIUS, + border_width: 0, + }, + } +} pub const fn button_pin() -> ButtonStyleSheet { ButtonStyleSheet { normal: &ButtonStyle { diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 46a7e71f5..a1241a7b1 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -389,6 +389,12 @@ def select_word( iterable must be of exact size. Returns index in range `0..3`.""" +# rust/src/ui/model_mercury/layout.rs +def show_tx_context_menu() -> LayoutObj[int]: + """Show transaction context menu with the options for 1) Address QR code, 2) Fee + information, 3) Cancel transaction""" + + # rust/src/ui/model_mercury/layout.rs def show_share_words( *,