refactor(core/ui): clear display on rust side

[no changelog]
tychovrahe/btconly/cli
Martin Milata 9 months ago
parent d8e7c00087
commit d99e1eedd2

@ -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)},

@ -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")

@ -174,3 +174,9 @@ pub fn refresh() {
ffi::display_refresh();
}
}
pub fn clear() {
unsafe {
ffi::display_clear();
}
}

@ -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<T> {
inner: Child<T>,
marked_for_clear: bool,
}
impl<T> Root<T> {
pub fn new(component: T) -> Self {
Self {
inner: Child::new(component),
marked_for_clear: true,
}
}
pub fn inner(&self) -> &Child<T> {
&self.inner
}
pub fn skip_paint(&mut self) {
self.inner.skip_paint()
}
pub fn clear_screen(&mut self) {
self.marked_for_clear = true;
}
}
impl<T> Component for Root<T>
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<Self::Msg> {
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<T> crate::trace::Trace for Root<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.inner.trace(t)
}
}
impl<M, T, U> Component for (T, U)
where
T: Component<Msg = M>,
@ -379,6 +453,7 @@ pub struct EventCtx {
paint_requested: bool,
anim_frame_scheduled: bool,
page_count: Option<usize>,
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) {

@ -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;

@ -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<T> {
area: Rect,

@ -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<T> ObjComponent for Child<T>
impl<T> ObjComponent for Root<T>
where
T: ComponentMsgObj + MaybeTrace,
{
@ -61,14 +62,14 @@ where
fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result<Obj, Error> {
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<Gc<Self>, 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) }

@ -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).

@ -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]

@ -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()

@ -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()

@ -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)

@ -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

@ -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)

@ -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()

Loading…
Cancel
Save