From d99e1eedd2bc156b8eba7740cb4041b542275274 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Wed, 27 Sep 2023 21:42:50 +0200 Subject: [PATCH] refactor(core/ui): clear display on rust side [no changelog] --- .../extmod/modtrezorui/modtrezorui-display.h | 12 -- core/embed/rust/build.rs | 1 + core/embed/rust/src/trezorhal/display.rs | 6 + core/embed/rust/src/ui/component/base.rs | 87 +++++++++- core/embed/rust/src/ui/component/mod.rs | 2 +- core/embed/rust/src/ui/display/mod.rs | 5 + core/embed/rust/src/ui/layout/obj.rs | 24 ++- core/mocks/generated/trezorui.pyi | 5 - core/src/trezor/ui/__init__.py | 158 +----------------- core/src/trezor/ui/layouts/tr/__init__.py | 3 - core/src/trezor/ui/layouts/tr/progress.py | 1 - core/src/trezor/ui/layouts/tt/__init__.py | 5 +- core/src/trezor/ui/layouts/tt/homescreen.py | 4 +- core/src/trezor/ui/layouts/tt/progress.py | 1 - core/tests/test_trezor.ui.display.py | 3 - 15 files changed, 128 insertions(+), 189 deletions(-) diff --git a/core/embed/extmod/modtrezorui/modtrezorui-display.h b/core/embed/extmod/modtrezorui/modtrezorui-display.h index fe504ed29..85662585a 100644 --- a/core/embed/extmod/modtrezorui/modtrezorui-display.h +++ b/core/embed/extmod/modtrezorui/modtrezorui-display.h @@ -47,17 +47,6 @@ STATIC mp_obj_t mod_trezorui_Display_make_new(const mp_obj_type_t *type, return MP_OBJ_FROM_PTR(o); } -/// def clear(self) -> None: -/// """ -/// Clear display with black color. -/// """ -STATIC mp_obj_t mod_trezorui_Display_clear(mp_obj_t self) { - display_clear(); - return mp_const_none; -} -STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui_Display_clear_obj, - mod_trezorui_Display_clear); - /// def refresh(self) -> None: /// """ /// Refresh display (update screen). @@ -161,7 +150,6 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui_Display_clear_save_obj, mod_trezorui_Display_clear_save); STATIC const mp_rom_map_elem_t mod_trezorui_Display_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&mod_trezorui_Display_clear_obj)}, {MP_ROM_QSTR(MP_QSTR_refresh), MP_ROM_PTR(&mod_trezorui_Display_refresh_obj)}, {MP_ROM_QSTR(MP_QSTR_bar), MP_ROM_PTR(&mod_trezorui_Display_bar_obj)}, diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs index ccc22b346..810f01954 100644 --- a/core/embed/rust/build.rs +++ b/core/embed/rust/build.rs @@ -290,6 +290,7 @@ fn generate_trezorhal_bindings() { .allowlist_function("storage_set_counter") .allowlist_function("storage_next_counter") // display + .allowlist_function("display_clear") .allowlist_function("display_offset") .allowlist_function("display_refresh") .allowlist_function("display_backlight") diff --git a/core/embed/rust/src/trezorhal/display.rs b/core/embed/rust/src/trezorhal/display.rs index 30481b16e..70760abf7 100644 --- a/core/embed/rust/src/trezorhal/display.rs +++ b/core/embed/rust/src/trezorhal/display.rs @@ -174,3 +174,9 @@ pub fn refresh() { ffi::display_refresh(); } } + +pub fn clear() { + unsafe { + ffi::display_clear(); + } +} diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index b65828fd7..c445ed352 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -6,7 +6,7 @@ use crate::{ time::Duration, ui::{ component::{maybe::PaintOverlapping, MsgMap}, - display::Color, + display::{self, Color}, geometry::{Offset, Rect}, }, }; @@ -191,6 +191,80 @@ where } } +/// Same as `Child` but also handles screen clearing when layout is first +/// painted. +pub struct Root { + inner: Child, + marked_for_clear: bool, +} + +impl Root { + pub fn new(component: T) -> Self { + Self { + inner: Child::new(component), + marked_for_clear: true, + } + } + + pub fn inner(&self) -> &Child { + &self.inner + } + + pub fn skip_paint(&mut self) { + self.inner.skip_paint() + } + + pub fn clear_screen(&mut self) { + self.marked_for_clear = true; + } +} + +impl Component for Root +where + T: Component, +{ + 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 { + let msg = self.inner.event(ctx, event); + if ctx.needs_repaint_root() { + self.marked_for_clear = true; + let mut dummy_ctx = EventCtx::new(); + let paint_msg = self.inner.event(&mut dummy_ctx, Event::RequestPaint); + assert!(matches!(paint_msg, None)); + assert!(dummy_ctx.timers.is_empty()); + } + msg + } + + fn paint(&mut self) { + if self.marked_for_clear && self.inner.will_paint() { + self.marked_for_clear = false; + display::clear() + } + self.inner.paint(); + } + + #[cfg(feature = "ui_bounds")] + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.inner.bounds(sink) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for Root +where + T: crate::trace::Trace, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + self.inner.trace(t) + } +} + impl Component for (T, U) where T: Component, @@ -379,6 +453,7 @@ pub struct EventCtx { paint_requested: bool, anim_frame_scheduled: bool, page_count: Option, + root_repaint_requested: bool, } impl EventCtx { @@ -404,6 +479,7 @@ impl EventCtx { * `Child::marked_for_paint` being true. */ anim_frame_scheduled: false, page_count: None, + root_repaint_requested: false, } } @@ -442,6 +518,14 @@ impl EventCtx { } } + pub fn request_repaint_root(&mut self) { + self.root_repaint_requested = true; + } + + pub fn needs_repaint_root(&self) -> bool { + self.root_repaint_requested + } + pub fn set_page_count(&mut self, count: usize) { #[cfg(feature = "ui_debug")] assert!(self.page_count.is_none()); @@ -461,6 +545,7 @@ impl EventCtx { self.paint_requested = false; self.anim_frame_scheduled = false; self.page_count = None; + self.root_repaint_requested = false; } fn register_timer(&mut self, token: TimerToken, deadline: Duration) { diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index e07909dd7..ae4c4d563 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -16,7 +16,7 @@ pub mod qr_code; pub mod text; pub mod timeout; -pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; +pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, TimerToken}; pub use border::Border; pub use empty::Empty; pub use label::Label; diff --git a/core/embed/rust/src/ui/display/mod.rs b/core/embed/rust/src/ui/display/mod.rs index 2a3b6667e..fd71860e5 100644 --- a/core/embed/rust/src/ui/display/mod.rs +++ b/core/embed/rust/src/ui/display/mod.rs @@ -341,6 +341,11 @@ pub fn rect_fill_corners(r: Rect, fg_color: Color) { } } +/// Draw black rectangle over entire screen. +pub fn clear() { + display::clear(); +} + #[derive(Copy, Clone, PartialEq, Eq)] pub struct TextOverlay { area: Rect, diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 6bec76c7b..e3d1eb734 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -17,7 +17,7 @@ use crate::{ }, time::Duration, ui::{ - component::{Child, Component, Event, EventCtx, Never, TimerToken}, + component::{Component, Event, EventCtx, Never, Root, TimerToken}, constant, display::sync, geometry::Rect, @@ -49,9 +49,10 @@ pub trait ObjComponent: MaybeTrace { fn obj_paint(&mut self) -> bool; fn obj_bounds(&self, _sink: &mut dyn FnMut(Rect)) {} fn obj_skip_paint(&mut self) {} + fn obj_request_clear(&mut self) {} } -impl ObjComponent for Child +impl ObjComponent for Root where T: ComponentMsgObj + MaybeTrace, { @@ -61,14 +62,14 @@ where fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result { if let Some(msg) = self.event(ctx, event) { - self.inner().msg_try_into_obj(msg) + self.inner().inner().msg_try_into_obj(msg) } else { Ok(Obj::const_none()) } } fn obj_paint(&mut self) -> bool { - let will_paint = self.will_paint(); + let will_paint = self.inner().will_paint(); self.paint(); will_paint } @@ -81,6 +82,10 @@ where fn obj_skip_paint(&mut self) { self.skip_paint() } + + fn obj_request_clear(&mut self) { + self.clear_screen() + } } /// `LayoutObj` is a GC-allocated object exported to MicroPython, with type @@ -102,9 +107,9 @@ struct LayoutObjInner { impl LayoutObj { /// Create a new `LayoutObj`, wrapping a root component. pub fn new(root: impl ComponentMsgObj + MaybeTrace + 'static) -> Result, Error> { - // Let's wrap the root component into a `Child` to maintain the top-level + // Let's wrap the root component into a `Root` to maintain the top-level // invalidation logic. - let wrapped_root = Child::new(root); + let wrapped_root = Root::new(root); // SAFETY: We are coercing GC-allocated sized ptr into an unsized one. let root = unsafe { Gc::from_raw(Gc::into_raw(Gc::new(wrapped_root)?) as *mut dyn ObjComponent) }; @@ -174,6 +179,12 @@ impl LayoutObj { Ok(msg) } + fn obj_request_clear(&self) { + let mut inner = self.inner.borrow_mut(); + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. + unsafe { Gc::as_mut(&mut inner.root) }.obj_request_clear(); + } + /// Run a paint pass over the component tree. Returns true if any component /// actually requested painting since last invocation of the function. fn obj_paint_if_requested(&self) -> bool { @@ -427,6 +438,7 @@ extern "C" fn ui_layout_request_complete_repaint(this: Obj) -> Obj { #[cfg(feature = "ui_debug")] panic!("cannot raise messages during RequestPaint"); }; + this.obj_request_clear(); Ok(Obj::const_none()) }; unsafe { util::try_or_raise(block) } diff --git a/core/mocks/generated/trezorui.pyi b/core/mocks/generated/trezorui.pyi index 1d96c46a6..f636a74b4 100644 --- a/core/mocks/generated/trezorui.pyi +++ b/core/mocks/generated/trezorui.pyi @@ -18,11 +18,6 @@ class Display: Initialize the display. """ - def clear(self) -> None: - """ - Clear display with black color. - """ - def refresh(self) -> None: """ Refresh display (update screen). diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index df9939463..034f20fd6 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -1,10 +1,9 @@ # pylint: disable=wrong-import-position import utime -from micropython import const from trezorui import Display from typing import TYPE_CHECKING, Any, Awaitable, Generator -from trezor import io, loop, utils, workflow +from trezor import loop, utils # all rendering is done through a singleton of `Display` display = Display() @@ -46,9 +45,6 @@ if utils.EMULATOR or utils.INTERNAL_MODEL in ("T1B1", "T2B1"): # import style later to avoid circular dep from trezor.ui import style # isort:skip -# import style definitions into namespace -from trezor.ui.style import * # isort:skip # noqa: F401,F403 - async def _alert(count: int) -> None: short_sleep = loop.sleep(20) @@ -93,84 +89,6 @@ def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None: display.backlight(val) -# Component events. Should be different from `io.TOUCH_*` events. -# Event dispatched when components should draw to the display, if they are -# marked for re-paint. -RENDER = const(-255) -# Event dispatched when components should mark themselves for re-painting. -REPAINT = const(-256) - -# How long, in milliseconds, should the layout rendering task sleep between -# the render calls. -_RENDER_DELAY_MS = const(10) - - -class Component: - """ - Abstract class. - - Components are GUI classes that inherit `Component` and form a tree, with a - `Layout` at the root, and other components underneath. Components that - have children, and therefore need to dispatch events to them, usually - override the `dispatch` method. Leaf components usually override the event - methods (`on_*`). Components signal a completion to the layout by raising - an instance of `Result`. - """ - - def __init__(self) -> None: - self.repaint = True - - if utils.INTERNAL_MODEL in ("T2T1", "D001"): - - def dispatch(self, event: int, x: int, y: int) -> None: - if event is RENDER: - self.on_render() - elif event is io.TOUCH_START: - self.on_touch_start(x, y) - elif event is io.TOUCH_MOVE: - self.on_touch_move(x, y) - elif event is io.TOUCH_END: - self.on_touch_end(x, y) - elif event is REPAINT: - self.repaint = True - - def on_touch_start(self, x: int, y: int) -> None: - pass - - def on_touch_move(self, x: int, y: int) -> None: - pass - - def on_touch_end(self, x: int, y: int) -> None: - pass - - elif utils.INTERNAL_MODEL in ("T1B1", "T2B1"): - - def dispatch(self, event: int, x: int, y: int) -> None: - if event is RENDER: - self.on_render() - elif event is io.BUTTON_PRESSED: - self.on_button_pressed(x) - elif event is io.BUTTON_RELEASED: - self.on_button_released(x) - elif event is REPAINT: - self.repaint = True - - def on_button_pressed(self, button_number: int) -> None: - pass - - def on_button_released(self, button_number: int) -> None: - pass - - def on_render(self) -> None: - pass - - if __debug__: - - def read_content_into(self, content_store: list[str]) -> None: - content_store.clear() - content_store.append(self.__class__.__name__) - - class Result(Exception): """ When components want to trigger layout completion, they do so through @@ -195,7 +113,7 @@ class Cancelled(Exception): """ -class Layout(Component): +class Layout: """ Abstract class. @@ -205,9 +123,6 @@ class Layout(Component): raised, usually from some of the child components. """ - BACKLIGHT_LEVEL = style.BACKLIGHT_NORMAL - RENDER_SLEEP: loop.Syscall = loop.sleep(_RENDER_DELAY_MS) - async def __iter__(self) -> Any: """ Run the layout and wait until it completes. Returns the result value. @@ -253,72 +168,13 @@ class Layout(Component): returns, the others are closed and `create_tasks` is called again. Usually overridden to add another tasks to the list.""" - return self.handle_input(), self.handle_rendering() - - if utils.INTERNAL_MODEL in ("T2T1", "D001"): + raise NotImplementedError - def handle_input(self) -> Generator: - """Task that is waiting for the user input.""" - touch = loop.wait(io.TOUCH) - while True: - # Using `yield` instead of `await` to avoid allocations. - event, x, y = yield touch - workflow.idle_timer.touch() - self.dispatch(event, x, y) - # We dispatch a render event right after the touch. Quick and dirty - # way to get the lowest input-to-render latency. - self.dispatch(RENDER, 0, 0) - - elif utils.INTERNAL_MODEL in ("T1B1", "T2B1"): - - def handle_input(self) -> Generator: - """Task that is waiting for the user input.""" - button = loop.wait(io.BUTTON) - while True: - event, button_num = yield button - workflow.idle_timer.touch() - self.dispatch(event, button_num, 0) - self.dispatch(RENDER, 0, 0) + if __debug__: - else: - raise ValueError("Unknown Trezor model") - - def _before_render(self) -> None: - # Before the first render, we dim the display. - backlight_fade(style.BACKLIGHT_NONE) - # Clear the screen of any leftovers, make sure everything is marked for - # repaint (we can be running the same layout instance multiple times) - # and paint it. - display.clear() - self.dispatch(REPAINT, 0, 0) - self.dispatch(RENDER, 0, 0) - - if __debug__ and self.should_notify_layout_change: - from apps.debug import notify_layout_change - - # notify about change and do not notify again until next await. - # (handle_rendering might be called multiple times in a single await, - # because of the endless loop in __iter__) - self.should_notify_layout_change = False - notify_layout_change(self) - - # Display is usually refreshed after every loop step, but here we are - # rendering everything synchronously, so refresh it manually and turn - # the brightness on again. - refresh() - backlight_fade(self.BACKLIGHT_LEVEL) - - def handle_rendering(self) -> loop.Task: # type: ignore [awaitable-is-generator] - """Task that is rendering the layout in a busy loop.""" - self._before_render() - sleep = self.RENDER_SLEEP - while True: - # Wait for a couple of ms and render the layout again. Because - # components use re-paint marking, they do not really draw on the - # display needlessly. Using `yield` instead of `await` to avoid allocations. - # TODO: remove the busy loop - yield sleep - self.dispatch(RENDER, 0, 0) + def read_content_into(self, content_store: list[str]) -> None: + content_store.clear() + content_store.append(self.__class__.__name__) def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-is-generator] diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index b4aeee1b0..e912dc642 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -194,8 +194,6 @@ class RustLayout(ui.Layout): return self.handle_timers(), self.handle_input_and_rendering() def _first_paint(self) -> None: - # Clear the screen of any leftovers. - ui.display.clear() self._paint() if __debug__ and self.should_notify_layout_change: @@ -253,7 +251,6 @@ def draw_simple(layout: Any) -> None: raise RuntimeError layout.attach_timer_fn(dummy_set_timer) - ui.display.clear() layout.paint() ui.refresh() diff --git a/core/src/trezor/ui/layouts/tr/progress.py b/core/src/trezor/ui/layouts/tr/progress.py index 20bc75447..5cc4331e8 100644 --- a/core/src/trezor/ui/layouts/tr/progress.py +++ b/core/src/trezor/ui/layouts/tr/progress.py @@ -15,7 +15,6 @@ class RustProgress: layout: Any, ): self.layout = layout - ui.display.clear() self.layout.attach_timer_fn(self.set_timer) self.layout.paint() diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py index 64a85291d..ab0cbf725 100644 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ b/core/src/trezor/ui/layouts/tt/__init__.py @@ -30,6 +30,8 @@ if __debug__: class RustLayout(ui.Layout): + BACKLIGHT_LEVEL = ui.style.BACKLIGHT_NORMAL + # pylint: disable=super-init-not-called def __init__(self, layout: Any): self.layout = layout @@ -160,9 +162,7 @@ class RustLayout(ui.Layout): return self.handle_timers(), self.handle_input_and_rendering() def _first_paint(self) -> None: - # Clear the screen of any leftovers. ui.backlight_fade(ui.style.BACKLIGHT_NONE) - ui.display.clear() self._paint() if __debug__ and self.should_notify_layout_change: @@ -223,7 +223,6 @@ def draw_simple(layout: Any) -> None: layout.attach_timer_fn(dummy_set_timer) ui.backlight_fade(ui.style.BACKLIGHT_DIM) - ui.display.clear() layout.paint() ui.refresh() ui.backlight_fade(ui.style.BACKLIGHT_NORMAL) diff --git a/core/src/trezor/ui/layouts/tt/homescreen.py b/core/src/trezor/ui/layouts/tt/homescreen.py index eda7699b0..1e4121d5f 100644 --- a/core/src/trezor/ui/layouts/tt/homescreen.py +++ b/core/src/trezor/ui/layouts/tt/homescreen.py @@ -86,7 +86,7 @@ class Homescreen(HomescreenBase): class Lockscreen(HomescreenBase): RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON - BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW + BACKLIGHT_LEVEL = ui.style.BACKLIGHT_LOW def __init__( self, @@ -95,7 +95,7 @@ class Lockscreen(HomescreenBase): ) -> None: self.bootscreen = bootscreen if bootscreen: - self.BACKLIGHT_LEVEL = ui.BACKLIGHT_NORMAL + self.BACKLIGHT_LEVEL = ui.style.BACKLIGHT_NORMAL skip = ( not bootscreen and storage_cache.homescreen_shown is self.RENDER_INDICATOR diff --git a/core/src/trezor/ui/layouts/tt/progress.py b/core/src/trezor/ui/layouts/tt/progress.py index 2164571a3..400510717 100644 --- a/core/src/trezor/ui/layouts/tt/progress.py +++ b/core/src/trezor/ui/layouts/tt/progress.py @@ -16,7 +16,6 @@ class RustProgress: ): self.layout = layout ui.backlight_fade(ui.style.BACKLIGHT_DIM) - ui.display.clear() self.layout.attach_timer_fn(self.set_timer) self.layout.paint() ui.backlight_fade(ui.style.BACKLIGHT_NORMAL) diff --git a/core/tests/test_trezor.ui.display.py b/core/tests/test_trezor.ui.display.py index 6705aec40..1f7f2a628 100644 --- a/core/tests/test_trezor.ui.display.py +++ b/core/tests/test_trezor.ui.display.py @@ -5,9 +5,6 @@ from trezor.ui import display class TestDisplay(unittest.TestCase): - def test_clear(self): - display.clear() - def test_refresh(self): display.refresh()