diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index f8d306fb9..aea362bac 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -93,6 +93,7 @@ static void _librust_qstrs(void) { MP_QSTR_bounds; MP_QSTR_button; MP_QSTR_button_event; + MP_QSTR_button_request; MP_QSTR_buttons__abort; MP_QSTR_buttons__access; MP_QSTR_buttons__again; diff --git a/core/embed/rust/src/ui/button_request.rs b/core/embed/rust/src/ui/button_request.rs new file mode 100644 index 000000000..9bf8065a3 --- /dev/null +++ b/core/embed/rust/src/ui/button_request.rs @@ -0,0 +1,44 @@ +// ButtonRequestType from messages-common.proto +// Eventually this should be generated +#[derive(Clone, Copy)] +#[repr(u16)] +pub enum ButtonRequestCode { + Other = 1, + FeeOverThreshold = 2, + ConfirmOutput = 3, + ResetDevice = 4, + ConfirmWord = 5, + WipeDevice = 6, + ProtectCall = 7, + SignTx = 8, + FirmwareCheck = 9, + Address = 10, + PublicKey = 11, + MnemonicWordCount = 12, + MnemonicInput = 13, + UnknownDerivationPath = 15, + RecoveryHomepage = 16, + Success = 17, + Warning = 18, + PassphraseEntry = 19, + PinEntry = 20, +} + +impl ButtonRequestCode { + pub fn num(&self) -> u16 { + *self as u16 + } + + pub fn with_type(self, br_type: &'static str) -> Option { + Some(ButtonRequest { + code: self, + br_type, + }) + } +} + +#[derive(Clone, Copy)] +pub struct ButtonRequest { + pub code: ButtonRequestCode, + pub br_type: &'static str, +} diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index 10e27fd76..0d3612ec0 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -6,6 +6,7 @@ use crate::{ strutil::TString, time::Duration, ui::{ + button_request::{ButtonRequest, ButtonRequestCode}, component::{maybe::PaintOverlapping, MsgMap}, display::{self, Color}, geometry::{Offset, Rect}, @@ -487,6 +488,7 @@ pub struct EventCtx { paint_requested: bool, anim_frame_scheduled: bool, page_count: Option, + button_request: Option, root_repaint_requested: bool, } @@ -513,6 +515,7 @@ impl EventCtx { * `Child::marked_for_paint` being true. */ anim_frame_scheduled: false, page_count: None, + button_request: None, root_repaint_requested: false, } } @@ -570,6 +573,16 @@ impl EventCtx { self.page_count } + pub fn send_button_request(&mut self, code: ButtonRequestCode, br_type: &'static str) { + #[cfg(feature = "ui_debug")] + assert!(self.button_request.is_none()); + self.button_request = Some(ButtonRequest { code, br_type }); + } + + pub fn button_request(&mut self) -> Option { + self.button_request.take() + } + pub fn pop_timer(&mut self) -> Option<(TimerToken, Duration)> { self.timers.pop() } @@ -579,6 +592,9 @@ impl EventCtx { self.paint_requested = false; self.anim_frame_scheduled = false; self.page_count = None; + #[cfg(feature = "ui_debug")] + assert!(self.button_request.is_none()); + self.button_request = None; self.root_repaint_requested = false; } diff --git a/core/embed/rust/src/ui/component/button_request.rs b/core/embed/rust/src/ui/component/button_request.rs new file mode 100644 index 000000000..b03a85941 --- /dev/null +++ b/core/embed/rust/src/ui/component/button_request.rs @@ -0,0 +1,84 @@ +use crate::ui::{ + button_request::{ButtonRequest, ButtonRequestCode}, + component::{Component, Event, EventCtx}, + geometry::Rect, + shape::Renderer, +}; + +/// Component that sends a ButtonRequest after receiving Event::Attach. The +/// request is only sent once. +#[derive(Clone, Copy)] +pub struct OneButtonRequest { + button_request: Option, + pub inner: T, +} + +impl OneButtonRequest { + pub fn new(button_request: ButtonRequest, inner: T) -> Self { + Self { + button_request: Some(button_request), + inner, + } + } +} + +impl Component for OneButtonRequest { + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.inner.place(bounds) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if matches!(event, Event::Attach) { + if let Some(button_request) = self.button_request.take() { + ctx.send_button_request(button_request.code, button_request.br_type) + } + } + self.inner.event(ctx, event) + } + + fn paint(&mut self) { + self.inner.paint() + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.inner.render(target) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for OneButtonRequest { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.inner.trace(t) + } +} + +pub trait ButtonRequestExt { + fn one_button_request( + self, + code: ButtonRequestCode, + br_type: &'static str, + ) -> OneButtonRequest + where + Self: Sized, + { + OneButtonRequest::new(ButtonRequest { code, br_type }, self) + } +} + +impl ButtonRequestExt for T {} + +#[cfg(all(feature = "micropython", feature = "touch"))] +impl crate::ui::flow::Swipable for OneButtonRequest +where + T: Component + crate::ui::flow::Swipable, +{ + fn can_swipe(&self, direction: crate::ui::component::SwipeDirection) -> bool { + self.inner.can_swipe(direction) + } + + fn swiped(&mut self, ctx: &mut EventCtx, direction: crate::ui::component::SwipeDirection) { + self.inner.swiped(ctx, direction) + } +} diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index bc1e36dbb..3d891580f 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -3,6 +3,7 @@ pub mod bar; pub mod base; pub mod border; +pub mod button_request; pub mod connect; pub mod empty; pub mod image; @@ -24,6 +25,7 @@ pub mod timeout; pub use bar::Bar; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, TimerToken}; pub use border::Border; +pub use button_request::{ButtonRequestExt, OneButtonRequest}; pub use empty::Empty; #[cfg(all(feature = "jpeg", feature = "micropython"))] pub use jpeg::Jpeg; diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 6c00316ad..cf96e885a 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -17,6 +17,7 @@ use crate::{ }, time::Duration, ui::{ + button_request::ButtonRequest, component::{Component, Event, EventCtx, Never, Root, TimerToken}, constant, display::{sync, Color}, @@ -248,6 +249,17 @@ impl LayoutObj { self.inner.borrow().page_count.into() } + fn obj_button_request(&self) -> Result { + let inner = &mut *self.inner.borrow_mut(); + + match inner.event_ctx.button_request() { + None => Ok(Obj::const_none()), + Some(ButtonRequest { code, br_type }) => { + (code.num().into(), br_type.try_into()?).try_into() + } + } + } + #[cfg(feature = "ui_debug")] fn obj_bounds(&self) { use crate::ui::display; @@ -280,6 +292,7 @@ impl LayoutObj { Qstr::MP_QSTR_trace => obj_fn_2!(ui_layout_trace).as_obj(), Qstr::MP_QSTR_bounds => obj_fn_1!(ui_layout_bounds).as_obj(), Qstr::MP_QSTR_page_count => obj_fn_1!(ui_layout_page_count).as_obj(), + Qstr::MP_QSTR_button_request => obj_fn_1!(ui_layout_button_request).as_obj(), }), }; &TYPE @@ -470,6 +483,14 @@ extern "C" fn ui_layout_page_count(this: Obj) -> Obj { unsafe { util::try_or_raise(block) } } +extern "C" fn ui_layout_button_request(this: Obj) -> Obj { + let block = || { + let this: Gc = this.try_into()?; + Ok(this.obj_button_request()?) + }; + unsafe { util::try_or_raise(block) } +} + #[cfg(feature = "ui_debug")] #[no_mangle] pub extern "C" fn ui_debug_layout_type() -> &'static Type { diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index 75c14bbf7..9e1518c8a 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -2,6 +2,7 @@ pub mod macros; pub mod animation; +pub mod button_request; pub mod component; pub mod constant; pub mod display; diff --git a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs index 4e2a65a71..0d0090554 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/get_address.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/get_address.rs @@ -1,6 +1,7 @@ use crate::{ error, ui::{ + button_request::ButtonRequestCode, component::{ image::BlendedImage, text::paragraphs::{Paragraph, Paragraphs}, @@ -116,7 +117,8 @@ impl GetAddress { ) .with_subtitle("address".into()) .with_menu_button() - .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)), + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)) + .one_button_request(ButtonRequestCode::Address, "show_address"), )? .add( Frame::left_aligned( diff --git a/core/src/apps/base.py b/core/src/apps/base.py index 4ba716ce4..65f78cc53 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -240,9 +240,10 @@ async def handle_EndSession(msg: EndSession) -> Success: async def handle_Ping(msg: Ping) -> Success: if msg.button_protection: from trezor.enums import ButtonRequestType as B - from trezor.ui.layouts import confirm_action + from trezor.ui.layouts import confirm_action, flow_demo - await confirm_action("ping", TR.words__confirm, "ping", br_code=B.ProtectCall) + await flow_demo() + # await confirm_action("ping", TR.words__confirm, "ping", br_code=B.ProtectCall) return Success(message=msg.message) diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index af012cfef..35e735caf 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -1,9 +1,10 @@ from typing import TYPE_CHECKING import trezorui2 -from trezor import TR, io, loop, ui +from trezor import TR, io, log, loop, ui from trezor.enums import ButtonRequestType -from trezor.wire import ActionCancelled +from trezor.messages import ButtonAck, ButtonRequest +from trezor.wire import ActionCancelled, context from trezor.wire.context import wait as ctx_wait from ..common import button_request, interact @@ -36,7 +37,10 @@ class RustLayout(ui.Layout): def __init__(self, layout: Any): self.layout = layout self.timer = loop.Timer() + self.br_chan = loop.chan() + self.layout.attach_timer_fn(self.set_timer) + self._send_button_request() def set_timer(self, token: int, deadline: int) -> None: self.timer.schedule(deadline, token) @@ -63,6 +67,9 @@ class RustLayout(ui.Layout): self.handle_swipe(), self.handle_click_signal(), self.handle_result_signal(), + context.with_context( + context.get_context(), self.handle_button_request() + ), ) async def handle_result_signal(self) -> None: @@ -110,6 +117,7 @@ class RustLayout(ui.Layout): (io.TOUCH_END, orig_x + 2 * off_x, orig_y + 2 * off_y), ): msg = self.layout.touch_event(event, x, y) + self._send_button_request() self._paint() if msg is not None: raise ui.Result(msg) @@ -129,10 +137,12 @@ class RustLayout(ui.Layout): from apps.debug import notify_layout_change self.layout.touch_event(io.TOUCH_START, x, y) + self._send_button_request() self._paint() if hold_ms is not None: await loop.sleep(hold_ms) msg = self.layout.touch_event(io.TOUCH_END, x, y) + self._send_button_request() if msg is not None: debug_storage.new_layout_event_id = event_id @@ -159,7 +169,13 @@ class RustLayout(ui.Layout): else: def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - return self.handle_timers(), self.handle_input_and_rendering() + return ( + self.handle_timers(), + self.handle_input_and_rendering(), + context.with_context( + context.get_context(), self.handle_button_request() + ), + ) def _first_paint(self) -> None: ui.backlight_fade(ui.style.BACKLIGHT_NONE) @@ -199,6 +215,7 @@ class RustLayout(ui.Layout): msg = None if event in (io.TOUCH_START, io.TOUCH_MOVE, io.TOUCH_END): msg = self.layout.touch_event(event, x, y) + self._send_button_request() if msg is not None: raise ui.Result(msg) self._paint() @@ -208,10 +225,25 @@ class RustLayout(ui.Layout): # Using `yield` instead of `await` to avoid allocations. token = yield self.timer msg = self.layout.timer(token) + self._send_button_request() if msg is not None: raise ui.Result(msg) self._paint() + def handle_button_request(self) -> loop.Task: + while True: + br_code, br_type, page_count = yield from self.br_chan.take() + log.debug(__name__, "ButtonRequest.type=%s", br_type) + yield from context.maybe_call( + ButtonRequest(code=br_code, pages=page_count), ButtonAck + ) + + def _send_button_request(self): + res = self.layout.button_request() + if res is not None: + br_code, br_type = res + self.br_chan.publish((br_code, br_type, self.layout.page_count())) + def page_count(self) -> int: return self.layout.page_count() @@ -279,14 +311,10 @@ async def confirm_action( async def flow_demo() -> None: - await raise_if_not_confirmed( - interact( - RustLayout(trezorui2.flow_get_address()), - "get_address", - BR_TYPE_OTHER, - ), - ActionCancelled, - ) + from trezor import workflow + + workflow.close_others() + await raise_if_not_confirmed(RustLayout(trezorui2.flow_get_address())) async def confirm_single(