from micropython import const from trezor import loop, res, ui from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault from trezor.ui.loader import Loader, LoaderDefault if __debug__: from apps.debug import swipe_signal if False: from typing import Any, Optional, Tuple from trezor.ui.button import ButtonContent, ButtonStyleType from trezor.ui.loader import LoaderStyleType CONFIRMED = object() CANCELLED = object() INFO = object() class Confirm(ui.Layout): DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) DEFAULT_CONFIRM_STYLE = ButtonConfirm DEFAULT_CANCEL = res.load(ui.ICON_CANCEL) DEFAULT_CANCEL_STYLE = ButtonCancel def __init__( self, content: ui.Component, confirm: Optional[ButtonContent] = DEFAULT_CONFIRM, confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, cancel: Optional[ButtonContent] = DEFAULT_CANCEL, cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE, major_confirm: bool = False, ) -> None: self.content = content if confirm is not None: if cancel is None: area = ui.grid(4, n_x=1) elif major_confirm: area = ui.grid(13, cells_x=2) else: area = ui.grid(9, n_x=2) self.confirm = Button( area, confirm, confirm_style ) # type: Optional[Button] self.confirm.on_click = self.on_confirm # type: ignore else: self.confirm = None if cancel is not None: if confirm is None: area = ui.grid(4, n_x=1) elif major_confirm: area = ui.grid(12, cells_x=1) else: area = ui.grid(8, n_x=2) self.cancel = Button(area, cancel, cancel_style) # type: Optional[Button] self.cancel.on_click = self.on_cancel # type: ignore else: self.cancel = None def dispatch(self, event: int, x: int, y: int) -> None: super().dispatch(event, x, y) self.content.dispatch(event, x, y) if self.confirm is not None: self.confirm.dispatch(event, x, y) if self.cancel is not None: self.cancel.dispatch(event, x, y) def on_confirm(self) -> None: raise ui.Result(CONFIRMED) def on_cancel(self) -> None: raise ui.Result(CANCELLED) class Pageable: def __init__(self) -> None: self._page = 0 def page(self) -> int: return self._page def page_count(self) -> int: raise NotImplementedError def is_first(self) -> bool: return self._page == 0 def is_last(self) -> bool: return self._page == self.page_count() - 1 def next(self) -> None: self._page = min(self._page + 1, self.page_count() - 1) def prev(self) -> None: self._page = max(self._page - 1, 0) class ConfirmPageable(Confirm): def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.pageable = pageable async def handle_paging(self) -> None: from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe if self.pageable.is_first(): directions = SWIPE_LEFT elif self.pageable.is_last(): directions = SWIPE_RIGHT else: directions = SWIPE_HORIZONTAL if __debug__: swipe = await loop.race(Swipe(directions), swipe_signal()) else: swipe = await Swipe(directions) if swipe == SWIPE_LEFT: self.pageable.next() else: self.pageable.prev() self.content.repaint = True if self.confirm is not None: self.confirm.repaint = True if self.cancel is not None: self.cancel.repaint = True def create_tasks(self) -> Tuple[loop.Task, ...]: tasks = super().create_tasks() if self.pageable.page_count() > 1: return tasks + (self.handle_paging(),) else: return tasks def on_render(self) -> None: PULSE_PERIOD = const(1200000) super().on_render() if not self.pageable.is_first(): t = ui.pulse(PULSE_PERIOD) c = ui.blend(ui.GREY, ui.DARK_GREY, t) icon = res.load(ui.ICON_SWIPE_RIGHT) ui.display.icon(18, 68, icon, c, ui.BG) if not self.pageable.is_last(): t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2) c = ui.blend(ui.GREY, ui.DARK_GREY, t) icon = res.load(ui.ICON_SWIPE_LEFT) ui.display.icon(205, 68, icon, c, ui.BG) class InfoConfirm(ui.Layout): DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) DEFAULT_CONFIRM_STYLE = ButtonConfirm DEFAULT_CANCEL = res.load(ui.ICON_CANCEL) DEFAULT_CANCEL_STYLE = ButtonCancel DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click DEFAULT_INFO_STYLE = ButtonDefault def __init__( self, content: ui.Component, confirm: ButtonContent = DEFAULT_CONFIRM, confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, cancel: ButtonContent = DEFAULT_CANCEL, cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE, info: ButtonContent = DEFAULT_INFO, info_style: ButtonStyleType = DEFAULT_INFO_STYLE, ) -> None: self.content = content self.confirm = Button(ui.grid(14), confirm, confirm_style) self.confirm.on_click = self.on_confirm # type: ignore self.info = Button(ui.grid(13), info, info_style) self.info.on_click = self.on_info self.cancel = Button(ui.grid(12), cancel, cancel_style) self.cancel.on_click = self.on_cancel # type: ignore def dispatch(self, event: int, x: int, y: int) -> None: self.content.dispatch(event, x, y) if self.confirm is not None: self.confirm.dispatch(event, x, y) if self.cancel is not None: self.cancel.dispatch(event, x, y) if self.info is not None: self.info.dispatch(event, x, y) def on_confirm(self) -> None: raise ui.Result(CONFIRMED) def on_cancel(self) -> None: raise ui.Result(CANCELLED) def on_info(self) -> None: raise ui.Result(INFO) class HoldToConfirm(ui.Layout): DEFAULT_CONFIRM = "Hold To Confirm" DEFAULT_CONFIRM_STYLE = ButtonConfirm DEFAULT_LOADER_STYLE = LoaderDefault def __init__( self, content: ui.Component, confirm: str = DEFAULT_CONFIRM, confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, loader_style: LoaderStyleType = DEFAULT_LOADER_STYLE, ): self.content = content self.loader = Loader(loader_style) self.loader.on_start = self._on_loader_start # type: ignore self.button = Button(ui.grid(4, n_x=1), confirm, confirm_style) self.button.on_press_start = self._on_press_start # type: ignore self.button.on_press_end = self._on_press_end # type: ignore self.button.on_click = self._on_click # type: ignore def _on_press_start(self) -> None: self.loader.start() def _on_press_end(self) -> None: self.loader.stop() def _on_loader_start(self) -> None: # Loader has either started growing, or returned to the 0-position. # In the first case we need to clear the content leftovers, in the latter # we need to render the content again. ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT - 60, ui.BG) self.content.dispatch(ui.REPAINT, 0, 0) def _on_click(self) -> None: if self.loader.elapsed_ms() >= self.loader.target_ms: self.on_confirm() def dispatch(self, event: int, x: int, y: int) -> None: if self.loader.start_ms is not None: self.loader.dispatch(event, x, y) else: self.content.dispatch(event, x, y) self.button.dispatch(event, x, y) def on_confirm(self) -> None: raise ui.Result(CONFIRMED)