diff --git a/core/src/apps/cardano/layout/__init__.py b/core/src/apps/cardano/layout/__init__.py index 4fb412014..172c5234b 100644 --- a/core/src/apps/cardano/layout/__init__.py +++ b/core/src/apps/cardano/layout/__init__.py @@ -1,13 +1,12 @@ from micropython import const from trezor import ui -from trezor.messages import ButtonRequestType, MessageType -from trezor.messages.ButtonRequest import ButtonRequest -from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog -from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.scroll import Paginated from trezor.ui.text import Text from trezor.utils import chunks, format_amount +from apps.common.confirm import confirm, hold_to_confirm + def format_coin_amount(amount): return "%s %s" % (format_amount(amount, 6), "ADA") @@ -16,107 +15,34 @@ def format_coin_amount(amount): async def confirm_sending(ctx, amount, to): to_lines = list(chunks(to, 17)) - t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) t1.normal("Confirm sending:") t1.bold(format_coin_amount(amount)) t1.normal("to:") t1.bold(to_lines[0]) - pages = [t1] - LINES_PER_PAGE = 4 + PER_PAGE = const(4) + pages = [t1] if len(to_lines) > 1: - to_pages = list(chunks(to_lines[1:], LINES_PER_PAGE)) + to_pages = list(chunks(to_lines[1:], PER_PAGE)) for page in to_pages: - t = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + t = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) for line in page: t.bold(line) pages.append(t) - await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck) - - paginator = paginate(create_renderer(ConfirmDialog), len(pages), const(0), pages) - return await ctx.wait(paginator) == CONFIRMED + return await confirm(ctx, Paginated(pages)) async def confirm_transaction(ctx, amount, fee, network_name): - t1 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + t1 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) t1.normal("Total amount:") t1.bold(format_coin_amount(amount)) t1.normal("including fee:") t1.bold(format_coin_amount(fee)) - t2 = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + t2 = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) t2.normal("Network:") t2.bold(network_name) - pages = [t1, t2] - - await ctx.call(ButtonRequest(code=ButtonRequestType.Other), MessageType.ButtonAck) - - paginator = paginate( - create_renderer(HoldToConfirmDialog), len(pages), const(0), pages - ) - return await ctx.wait(paginator) == CONFIRMED - - -def create_renderer(confirmation_wrapper): - @ui.layout - async def page_renderer(page: int, page_count: int, pages: list): - # for some reason page index can be equal to page count - if page >= page_count: - page = page_count - 1 - - pages[page].taint() - content = Scrollpage(pages[page], page, page_count) - if page + 1 >= page_count: - return await confirmation_wrapper(content) - else: - content.render() - await animate_swipe() - - return page_renderer - - -async def confirm_with_pagination( - ctx, content, title: str, icon=ui.ICON_RESET, icon_color=ui.ORANGE -): - first_page = const(0) - lines_per_page = const(4) - - if isinstance(content, (list, tuple)): - lines = content - else: - lines = list(chunks(content, 17)) - pages = list(chunks(lines, lines_per_page)) - - await ctx.call(ButtonRequest(code=ButtonRequestType.Address), MessageType.ButtonAck) - - paginator = paginate( - show_text_page, len(pages), first_page, pages, title, icon, icon_color - ) - return await ctx.wait(paginator) == CONFIRMED - - -@ui.layout -async def show_text_page( - page: int, - page_count: int, - pages: list, - title: str, - icon=ui.ICON_RESET, - icon_color=ui.ORANGE, -): - if page_count == 1: - page = 0 - - lines = pages[page] - content = Text(title, icon, icon_color=icon_color) - content.mono(*lines) - - content = Scrollpage(content, page, page_count) - - if page + 1 >= page_count: - return await ConfirmDialog(content) - else: - content.render() - await animate_swipe() + return await hold_to_confirm(ctx, Paginated([t1, t2])) diff --git a/core/src/apps/common/confirm.py b/core/src/apps/common/confirm.py index 3f057447d..9f3b5e50c 100644 --- a/core/src/apps/common/confirm.py +++ b/core/src/apps/common/confirm.py @@ -1,29 +1,59 @@ -from trezor import ui, wire +from trezor import wire from trezor.messages import ButtonRequestType, MessageType from trezor.messages.ButtonRequest import ButtonRequest -from trezor.ui.confirm import CONFIRMED, ConfirmDialog, HoldToConfirmDialog +from trezor.ui.confirm import CONFIRMED, Confirm, HoldToConfirm +if __debug__: + from apps.debug import confirm_signal -@ui.layout -async def confirm(ctx, content, code=None, *args, **kwargs): - if code is None: - code = ButtonRequestType.Other + +async def confirm( + ctx, + content, + code=ButtonRequestType.Other, + confirm=Confirm.DEFAULT_CONFIRM, + confirm_style=Confirm.DEFAULT_CONFIRM_STYLE, + cancel=Confirm.DEFAULT_CANCEL, + cancel_style=Confirm.DEFAULT_CANCEL_STYLE, +): await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck) - dialog = ConfirmDialog(content, *args, **kwargs) + if content.__class__.__name__ == "Paginated": + content.pages[-1] = Confirm( + content.pages[-1], confirm, confirm_style, cancel, cancel_style + ) + dialog = content + else: + dialog = Confirm(content, confirm, confirm_style, cancel, cancel_style) - return await ctx.wait(dialog) == CONFIRMED + if __debug__: + return await ctx.wait(dialog, confirm_signal) is CONFIRMED + else: + return await ctx.wait(dialog) is CONFIRMED -@ui.layout -async def hold_to_confirm(ctx, content, code=None, *args, **kwargs): - if code is None: - code = ButtonRequestType.Other +async def hold_to_confirm( + ctx, + content, + code=ButtonRequestType.Other, + confirm=HoldToConfirm.DEFAULT_CONFIRM, + confirm_style=HoldToConfirm.DEFAULT_CONFIRM_STYLE, + loader_style=HoldToConfirm.DEFAULT_LOADER_STYLE, +): await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck) - dialog = HoldToConfirmDialog(content, "Hold to confirm", *args, **kwargs) + if content.__class__.__name__ == "Paginated": + content.pages[-1] = HoldToConfirm( + content.pages[-1], confirm, confirm_style, loader_style + ) + dialog = content + else: + dialog = HoldToConfirm(content, confirm, confirm_style, loader_style) - return await ctx.wait(dialog) == CONFIRMED + if __debug__: + return await ctx.wait(dialog, confirm_signal) is CONFIRMED + else: + return await ctx.wait(dialog) is CONFIRMED async def require_confirm(*args, **kwargs): diff --git a/core/src/apps/common/layout.py b/core/src/apps/common/layout.py index 45610ea68..6b4571492 100644 --- a/core/src/apps/common/layout.py +++ b/core/src/apps/common/layout.py @@ -3,6 +3,7 @@ from ubinascii import hexlify from trezor import ui from trezor.messages import ButtonRequestType +from trezor.ui.button import ButtonDefault from trezor.ui.container import Container from trezor.ui.qr import Qr from trezor.ui.text import Text @@ -12,42 +13,45 @@ from apps.common import HARDENED from apps.common.confirm import confirm, require_confirm -async def show_address(ctx, address: str, desc: str = None, network: str = None): - text = Text( - desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN - ) - if network: +async def show_address( + ctx, address: str, desc: str = "Confirm address", network: str = None +): + text = Text(desc, ui.ICON_RECEIVE, ui.GREEN) + if network is not None: text.normal("%s network" % network) text.mono(*split_address(address)) + return await confirm( - ctx, text, code=ButtonRequestType.Address, cancel="QR", cancel_style=ui.BTN_KEY + ctx, + text, + code=ButtonRequestType.Address, + cancel="QR", + cancel_style=ButtonDefault, ) -async def show_qr(ctx, address: str, desc: str = None): - qr_x = const(120) - qr_y = const(115) - qr_coef = const(4) - - qr = Qr(address, (qr_x, qr_y), qr_coef) - text = Text( - desc if desc else "Confirm address", ui.ICON_RECEIVE, icon_color=ui.GREEN - ) +async def show_qr(ctx, address: str, desc: str = "Confirm address"): + QR_X = const(120) + QR_Y = const(115) + QR_COEF = const(4) + qr = Qr(address, QR_X, QR_Y, QR_COEF) + text = Text(desc, ui.ICON_RECEIVE, ui.GREEN) content = Container(qr, text) + return await confirm( ctx, content, code=ButtonRequestType.Address, cancel="Address", - cancel_style=ui.BTN_KEY, + cancel_style=ButtonDefault, ) async def show_pubkey(ctx, pubkey: bytes): lines = chunks(hexlify(pubkey).decode(), 18) - text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN) + text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN) text.mono(*lines) - return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey) + return await require_confirm(ctx, text, ButtonRequestType.PublicKey) def split_address(address: str): diff --git a/core/src/apps/common/mnemonic.py b/core/src/apps/common/mnemonic.py index 87782196a..22f60f62b 100644 --- a/core/src/apps/common/mnemonic.py +++ b/core/src/apps/common/mnemonic.py @@ -1,4 +1,4 @@ -from trezor import ui +from trezor import ui, workflow from trezor.crypto import bip39 from apps.common import storage @@ -18,8 +18,11 @@ def get_seed(passphrase: str = "", progress_bar=True): module = bip39 if progress_bar: _start_progress() - return module.seed(secret.decode(), passphrase, _render_progress) - return module.seed(secret.decode(), passphrase) + result = module.seed(secret.decode(), passphrase, _render_progress) + _stop_progress() + else: + result = module.seed(secret.decode(), passphrase) + return result def process(mnemonics: list, mnemonic_type: int): @@ -36,14 +39,18 @@ def restore() -> str: def _start_progress(): - ui.backlight_slide_sync(ui.BACKLIGHT_DIM) + ui.backlight_fade(ui.BACKLIGHT_DIM) ui.display.clear() ui.header("Please wait") ui.display.refresh() - ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL) + ui.backlight_fade(ui.BACKLIGHT_NORMAL) def _render_progress(progress: int, total: int): p = 1000 * progress // total ui.display.loader(p, False, 18, ui.WHITE, ui.BG) ui.display.refresh() + + +def _stop_progress(): + workflow.restartdefault() diff --git a/core/src/apps/common/paths.py b/core/src/apps/common/paths.py index 2494bf7f3..aebfc900e 100644 --- a/core/src/apps/common/paths.py +++ b/core/src/apps/common/paths.py @@ -15,14 +15,12 @@ async def validate_path(ctx, validate_func, keychain, path, curve, **kwargs): async def show_path_warning(ctx, path: list): - text = Text("Confirm path", ui.ICON_WRONG, icon_color=ui.RED) + text = Text("Confirm path", ui.ICON_WRONG, ui.RED) text.normal("Path") text.mono(*break_address_n_to_lines(path)) text.normal("is unknown.") text.normal("Are you sure?") - return await require_confirm( - ctx, text, code=ButtonRequestType.UnknownDerivationPath - ) + return await require_confirm(ctx, text, ButtonRequestType.UnknownDerivationPath) def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool: @@ -50,7 +48,8 @@ def validate_path_for_get_public_key(path: list, slip44_id: int) -> bool: def is_hardened(i: int) -> bool: if i & HARDENED: return True - return False + else: + return False def break_address_n_to_lines(address_n: list) -> list: diff --git a/core/src/apps/common/request_passphrase.py b/core/src/apps/common/request_passphrase.py index 096f74d8b..347429d0b 100644 --- a/core/src/apps/common/request_passphrase.py +++ b/core/src/apps/common/request_passphrase.py @@ -5,79 +5,74 @@ from trezor.messages import ButtonRequestType, MessageType, PassphraseSourceType from trezor.messages.ButtonRequest import ButtonRequest from trezor.messages.PassphraseRequest import PassphraseRequest from trezor.messages.PassphraseStateRequest import PassphraseStateRequest -from trezor.ui.entry_select import DEVICE, EntrySelector -from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard +from trezor.ui.passphrase import CANCELLED, PassphraseKeyboard, PassphraseSource from trezor.ui.text import Text -from apps.common import storage -from apps.common.cache import get_state +from apps.common import cache, storage + +if __debug__: + from apps.debug import input_signal _MAX_PASSPHRASE_LEN = const(50) -@ui.layout -async def request_passphrase_entry(ctx): - text = Text("Enter passphrase", ui.ICON_CONFIG) - text.normal("Where to enter your", "passphrase?") - text.render() +async def protect_by_passphrase(ctx) -> str: + if storage.has_passphrase(): + return await request_passphrase(ctx) + else: + return "" + - ack = await ctx.call( - ButtonRequest(code=ButtonRequestType.PassphraseType), - MessageType.ButtonAck, - MessageType.Cancel, +async def request_passphrase(ctx) -> str: + source = storage.get_passphrase_source() + if source == PassphraseSourceType.ASK: + source = await request_passphrase_source(ctx) + passphrase = await request_passphrase_ack( + ctx, source == PassphraseSourceType.DEVICE ) - if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel: - raise wire.ActionCancelled("Passphrase cancelled") + if len(passphrase) > _MAX_PASSPHRASE_LEN: + raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN) + return passphrase + + +async def request_passphrase_source(ctx) -> int: + req = ButtonRequest(code=ButtonRequestType.PassphraseType) + await ctx.call(req, MessageType.ButtonAck) + + text = Text("Enter passphrase", ui.ICON_CONFIG) + text.normal("Where to enter your", "passphrase?") + source = PassphraseSource(text) - selector = EntrySelector(text) - return await ctx.wait(selector) + return await ctx.wait(source) -@ui.layout -async def request_passphrase_ack(ctx, on_device): +async def request_passphrase_ack(ctx, on_device: bool) -> str: if not on_device: text = Text("Passphrase entry", ui.ICON_CONFIG) text.normal("Please, type passphrase", "on connected host.") text.render() req = PassphraseRequest(on_device=on_device) - ack = await ctx.call(req, MessageType.PassphraseAck, MessageType.Cancel) - if ack.MESSAGE_WIRE_TYPE == MessageType.Cancel: - raise wire.ActionCancelled("Passphrase cancelled") + ack = await ctx.call(req, MessageType.PassphraseAck) if on_device: if ack.passphrase is not None: raise wire.ProcessError("Passphrase provided when it should not be") - keyboard = PassphraseKeyboard("Enter passphrase") - passphrase = await ctx.wait(keyboard) - if passphrase == CANCELLED: + + keyboard = PassphraseKeyboard("Enter passphrase", _MAX_PASSPHRASE_LEN) + if __debug__: + passphrase = await ctx.wait(keyboard, input_signal) + else: + passphrase = await ctx.wait(keyboard) + if passphrase is CANCELLED: raise wire.ActionCancelled("Passphrase cancelled") else: if ack.passphrase is None: raise wire.ProcessError("Passphrase not provided") passphrase = ack.passphrase - req = PassphraseStateRequest( - state=get_state(prev_state=ack.state, passphrase=passphrase) - ) + state = cache.get_state(prev_state=ack.state, passphrase=passphrase) + req = PassphraseStateRequest(state=state) ack = await ctx.call(req, MessageType.PassphraseStateAck, MessageType.Cancel) return passphrase - - -async def request_passphrase(ctx): - if storage.get_passphrase_source() == PassphraseSourceType.ASK: - on_device = await request_passphrase_entry(ctx) == DEVICE - else: - on_device = storage.get_passphrase_source() == PassphraseSourceType.DEVICE - passphrase = await request_passphrase_ack(ctx, on_device) - if len(passphrase) > _MAX_PASSPHRASE_LEN: - raise wire.DataError("Maximum passphrase length is %d" % _MAX_PASSPHRASE_LEN) - return passphrase - - -async def protect_by_passphrase(ctx): - if storage.has_passphrase(): - return await request_passphrase(ctx) - else: - return "" diff --git a/core/src/apps/common/request_pin.py b/core/src/apps/common/request_pin.py index c0d9f091c..ee992e235 100644 --- a/core/src/apps/common/request_pin.py +++ b/core/src/apps/common/request_pin.py @@ -1,6 +1,5 @@ -from trezor import loop, res, ui -from trezor.ui.confirm import CONFIRMED, ConfirmDialog -from trezor.ui.pin import PinMatrix +from trezor import loop +from trezor.ui.pin import CANCELLED, PinDialog if __debug__: from apps.debug import input_signal @@ -10,71 +9,25 @@ class PinCancelled(Exception): pass -@ui.layout async def request_pin( - label=None, attempts_remaining=None, cancellable: bool = True + prompt: str = "Enter your PIN", + attempts_remaining: int = None, + allow_cancel: bool = True, ) -> str: - def onchange(): - c = dialog.cancel - if matrix.pin: - back = res.load(ui.ICON_BACK) - if c.content is not back: - c.normal_style = ui.BTN_CLEAR["normal"] - c.content = back - c.enable() - c.taint() - else: - lock = res.load(ui.ICON_LOCK) - if not cancellable and c.content: - c.content = "" - c.disable() - c.taint() - elif c.content is not lock: - c.normal_style = ui.BTN_CANCEL["normal"] - c.content = lock - c.enable() - c.taint() - c.render() - - c = dialog.confirm - if matrix.pin: - if not c.is_enabled(): - c.enable() - c.taint() - else: - if c.is_enabled(): - c.disable() - c.taint() - c.render() + if attempts_remaining is None: + subprompt = None + elif attempts_remaining == 1: + subprompt = "This is your last attempt" + else: + subprompt = "%s attempts remaining" % attempts_remaining - if label is None: - label = "Enter your PIN" - sublabel = None - if attempts_remaining: - if attempts_remaining == 1: - sublabel = "This is your last attempt" - else: - sublabel = "{} attempts remaining".format(attempts_remaining) - matrix = PinMatrix(label, sublabel) - matrix.onchange = onchange - dialog = ConfirmDialog(matrix) - dialog.cancel.area = ui.grid(12) - dialog.confirm.area = ui.grid(14) - matrix.onchange() + dialog = PinDialog(prompt, subprompt, allow_cancel) while True: if __debug__: result = await loop.spawn(dialog, input_signal) - if isinstance(result, str): - return result else: result = await dialog - if result == CONFIRMED: - if not matrix.pin: - continue - return matrix.pin - elif matrix.pin: # reset - matrix.change("") - continue - else: # cancel + if result is CANCELLED: raise PinCancelled() + return result diff --git a/core/src/apps/common/seed.py b/core/src/apps/common/seed.py index 57c5cbfce..15ec2c592 100644 --- a/core/src/apps/common/seed.py +++ b/core/src/apps/common/seed.py @@ -1,4 +1,4 @@ -from trezor import ui, wire +from trezor import wire from trezor.crypto import bip32 from apps.common import HARDENED, cache, mnemonic, storage @@ -64,30 +64,16 @@ async def get_keychain(ctx: wire.Context, namespaces: list) -> Keychain: raise wire.ProcessError("Device is not initialized") seed = cache.get_seed() if seed is None: - seed = await _compute_seed(ctx) + passphrase = cache.get_passphrase() + if passphrase is None: + passphrase = await protect_by_passphrase(ctx) + cache.set_passphrase(passphrase) + seed = mnemonic.get_seed(passphrase) + cache.set_seed(seed) keychain = Keychain(seed, namespaces) return keychain -def _path_hardened(path: list) -> bool: - # TODO: move to paths.py after #538 is fixed - for i in path: - if not (i & HARDENED): - return False - return True - - -@ui.layout_no_slide -async def _compute_seed(ctx: wire.Context) -> bytes: - passphrase = cache.get_passphrase() - if passphrase is None: - passphrase = await protect_by_passphrase(ctx) - cache.set_passphrase(passphrase) - seed = mnemonic.get_seed(passphrase) - cache.set_seed(seed) - return seed - - def derive_node_without_passphrase( path: list, curve_name: str = "secp256k1" ) -> bip32.HDNode: @@ -102,3 +88,7 @@ def derive_node_without_passphrase( def remove_ed25519_prefix(pubkey: bytes) -> bytes: # 0x01 prefix is not part of the actual public key, hence removed return pubkey[1:] + + +def _path_hardened(path: list) -> bool: + return all(i & HARDENED for i in path) diff --git a/core/src/apps/eos/actions/layout.py b/core/src/apps/eos/actions/layout.py index f84c48694..a888af0b3 100644 --- a/core/src/apps/eos/actions/layout.py +++ b/core/src/apps/eos/actions/layout.py @@ -1,7 +1,7 @@ from micropython import const from ubinascii import hexlify -from trezor import ui, wire +from trezor import ui from trezor.messages import ( ButtonRequestType, EosActionBuyRam, @@ -17,11 +17,8 @@ from trezor.messages import ( EosActionUnlinkAuth, EosActionUpdateAuth, EosActionVoteProducer, - MessageType, ) -from trezor.messages.ButtonRequest import ButtonRequest -from trezor.ui.confirm import CONFIRMED, ConfirmDialog -from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.scroll import Paginated from trezor.ui.text import Text from trezor.utils import chunks @@ -38,11 +35,19 @@ _FOUR_FIELDS_PER_PAGE = const(4) _FIVE_FIELDS_PER_PAGE = const(5) -async def confirm_action_buyram(ctx, msg: EosActionBuyRam): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) +async def _require_confirm_paginated(ctx, header, fields, per_page): + pages = [] + for page in chunks(fields, per_page): + if header == "Arbitrary data": + text = Text(header, ui.ICON_WIPE, ui.RED) + else: + text = Text(header, ui.ICON_CONFIRM, ui.GREEN) + text.mono(*page) + pages.append(text) + await require_confirm(ctx, Paginated(pages), ButtonRequestType.ConfirmOutput) + +async def confirm_action_buyram(ctx, msg: EosActionBuyRam): text = "Buy RAM" fields = [] fields.append("Payer:") @@ -51,18 +56,10 @@ async def confirm_action_buyram(ctx, msg: EosActionBuyRam): fields.append(helpers.eos_name_to_string(msg.receiver)) fields.append("Amount:") fields.append(helpers.eos_asset_to_string(msg.quantity)) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Buy RAM" fields = [] fields.append("Payer:") @@ -71,18 +68,10 @@ async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes): fields.append(helpers.eos_name_to_string(msg.receiver)) fields.append("Bytes:") fields.append(str(msg.bytes)) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_delegate(ctx, msg: EosActionDelegate): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Delegate" fields = [] fields.append("Sender:") @@ -101,35 +90,20 @@ async def confirm_action_delegate(ctx, msg: EosActionDelegate): else: fields.append("Transfer: No") - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_sellram(ctx, msg: EosActionSellRam): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Sell RAM" fields = [] fields.append("Receiver:") fields.append(helpers.eos_name_to_string(msg.account)) fields.append("Bytes:") fields.append(str(msg.bytes)) - - pages = list(chunks(fields, _TWO_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _TWO_FIELDS_PER_PAGE) async def confirm_action_undelegate(ctx, msg: EosActionUndelegate): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Undelegate" fields = [] fields.append("Sender:") @@ -140,11 +114,7 @@ async def confirm_action_undelegate(ctx, msg: EosActionUndelegate): fields.append(helpers.eos_asset_to_string(msg.cpu_quantity)) fields.append("NET:") fields.append(helpers.eos_asset_to_string(msg.net_quantity)) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_refund(ctx, msg: EosActionRefund): @@ -166,13 +136,12 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer): elif msg.producers: # PRODUCERS - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - producers = list(enumerate(msg.producers)) - pages = list(chunks(producers, _FIVE_FIELDS_PER_PAGE)) - paginator = paginate(show_voter_page, len(pages), _FIRST_PAGE, pages) - await ctx.wait(paginator) + text = "Vote for producers" + fields = [ + "{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer)) + for wi, producer in enumerate(msg.producers) + ] + await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE) else: # Cancel vote @@ -183,10 +152,6 @@ async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer): async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Transfer" fields = [] fields.append("From:") @@ -200,19 +165,12 @@ async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str): if msg.memo is not None: fields.append("Memo:") - fields += split_data(msg.memo[:512]) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + fields.extend(split_data(msg.memo[:512])) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Update Auth" fields = [] fields.append("Account:") @@ -221,12 +179,8 @@ async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth): fields.append(helpers.eos_name_to_string(msg.permission)) fields.append("Parent:") fields.append(helpers.eos_name_to_string(msg.parent)) - fields += authorization_fields(msg.auth) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - await ctx.wait(paginator) + fields.extend(authorization_fields(msg.auth)) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth): @@ -239,10 +193,6 @@ async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth): async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Link Auth" fields = [] fields.append("Account:") @@ -253,18 +203,10 @@ async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth): fields.append(helpers.eos_name_to_string(msg.type)) fields.append("Requirement:") fields.append(helpers.eos_name_to_string(msg.requirement)) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "Unlink Auth" fields = [] fields.append("Account:") @@ -273,86 +215,31 @@ async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth): fields.append(helpers.eos_name_to_string(msg.code)) fields.append("Type:") fields.append(helpers.eos_name_to_string(msg.type)) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_newaccount(ctx, msg: EosActionNewAccount): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) - text = "New Account" fields = [] fields.append("Creator:") fields.append(helpers.eos_name_to_string(msg.creator)) fields.append("Name:") fields.append(helpers.eos_name_to_string(msg.name)) - fields += authorization_fields(msg.owner) - fields += authorization_fields(msg.active) - - pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) + fields.extend(authorization_fields(msg.owner)) + fields.extend(authorization_fields(msg.active)) + await _require_confirm_paginated(ctx, text, fields, _FOUR_FIELDS_PER_PAGE) async def confirm_action_unknown(ctx, action, checksum): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck - ) text = "Arbitrary data" fields = [] fields.append("Contract:") fields.append(helpers.eos_name_to_string(action.account)) fields.append("Action Name:") fields.append(helpers.eos_name_to_string(action.name)) - fields.append("Checksum: ") - fields += split_data(hexlify(checksum).decode("ascii")) - - pages = list(chunks(fields, _FIVE_FIELDS_PER_PAGE)) - paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) - - await ctx.wait(paginator) - - -@ui.layout -async def show_lines_page(page: int, page_count: int, pages: list, header: str): - if header == "Arbitrary data": - text = Text(header, ui.ICON_WIPE, icon_color=ui.RED) - else: - text = Text(header, ui.ICON_CONFIRM, icon_color=ui.GREEN) - text.mono(*pages[page]) - - content = Scrollpage(text, page, page_count) - if page + 1 == page_count: - if await ConfirmDialog(content) != CONFIRMED: - raise wire.ActionCancelled("Action cancelled") - else: - content.render() - await animate_swipe() - - -@ui.layout -async def show_voter_page(page: int, page_count: int, pages: list): - lines = [ - "{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer)) - for wi, producer in pages[page] - ] - text = Text("Vote for producers", ui.ICON_CONFIRM, icon_color=ui.GREEN) - text.mono(*lines) - content = Scrollpage(text, page, page_count) - - if page + 1 == page_count: - if await ConfirmDialog(content) != CONFIRMED: - raise wire.ActionCancelled("Action cancelled") - else: - content.render() - await animate_swipe() + fields.extend(split_data(hexlify(checksum).decode("ascii"))) + await _require_confirm_paginated(ctx, text, fields, _FIVE_FIELDS_PER_PAGE) def authorization_fields(auth): diff --git a/core/src/apps/eos/layout.py b/core/src/apps/eos/layout.py index 278335902..70677b849 100644 --- a/core/src/apps/eos/layout.py +++ b/core/src/apps/eos/layout.py @@ -6,14 +6,14 @@ from apps.common.confirm import require_confirm async def require_get_public_key(ctx, public_key): - text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN) + text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN) text.normal(public_key) - return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey) + return await require_confirm(ctx, text, ButtonRequestType.PublicKey) async def require_sign_tx(ctx, num_actions): - text = Text("Sign transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Sign transaction", ui.ICON_SEND, ui.GREEN) text.normal("You are about") text.normal("to sign {}".format(num_actions)) text.normal("action(s).") - return await require_confirm(ctx, text, code=ButtonRequestType.SignTx) + return await require_confirm(ctx, text, ButtonRequestType.SignTx) diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 3f977e153..51015ac9d 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -16,7 +16,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type to_str = address_from_bytes(to_bytes, networks.by_chain_id(chain_id)) else: to_str = "new contract?" - text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False) + text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN, new_lines=False) text.bold(format_ethereum_amount(value, token, chain_id, tx_type)) text.normal(ui.GREY, "to", ui.FG) for to_line in split_address(to_str): @@ -29,9 +29,7 @@ async def require_confirm_tx(ctx, to_bytes, value, chain_id, token=None, tx_type async def require_confirm_fee( ctx, spending, gas_price, gas_limit, chain_id, token=None, tx_type=None ): - text = Text( - "Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False - ) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN, new_lines=False) text.bold(format_ethereum_amount(spending, token, chain_id, tx_type)) text.normal(ui.GREY, "Gas price:", ui.FG) text.bold(format_ethereum_amount(gas_price, None, chain_id, tx_type)) @@ -48,7 +46,7 @@ async def require_confirm_data(ctx, data, data_total): data_str = hexlify(data[:36]).decode() if data_total > 36: data_str = data_str[:-2] + ".." - text = Text("Confirm data", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm data", ui.ICON_SEND, ui.GREEN) text.bold("Size: %d bytes" % data_total) text.mono(*split_data(data_str)) # we use SignTx, not ConfirmOutput, for compatibility with T1 diff --git a/core/src/apps/homescreen/homescreen.py b/core/src/apps/homescreen/homescreen.py index 904a976af..d082e4e8b 100644 --- a/core/src/apps/homescreen/homescreen.py +++ b/core/src/apps/homescreen/homescreen.py @@ -5,9 +5,9 @@ from apps.common import storage async def homescreen(): # render homescreen in dimmed mode and fade back in - await ui.backlight_slide(ui.BACKLIGHT_DIM) + ui.backlight_fade(ui.BACKLIGHT_DIM) display_homescreen() - await ui.backlight_slide(ui.BACKLIGHT_NORMAL) + ui.backlight_fade(ui.BACKLIGHT_NORMAL) # loop forever, never return touch = loop.wait(io.TOUCH) diff --git a/core/src/apps/lisk/layout.py b/core/src/apps/lisk/layout.py index 10388a561..411c9163f 100644 --- a/core/src/apps/lisk/layout.py +++ b/core/src/apps/lisk/layout.py @@ -10,7 +10,7 @@ from apps.common.layout import show_pubkey, split_address async def require_confirm_tx(ctx, to, value): - text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN) text.bold(format_coin_amount(value)) text.normal("to") text.mono(*split_address(to)) @@ -18,7 +18,7 @@ async def require_confirm_tx(ctx, to, value): async def require_confirm_delegate_registration(ctx, delegate_name): - text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) text.normal("Do you really want to") text.normal("register a delegate?") text.bold(*chunks(delegate_name, 20)) @@ -26,7 +26,7 @@ async def require_confirm_delegate_registration(ctx, delegate_name): async def require_confirm_vote_tx(ctx, votes): - text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) text.normal(*get_vote_tx_text(votes)) return await require_confirm(ctx, text, ButtonRequestType.SignTx) @@ -36,7 +36,7 @@ async def require_confirm_public_key(ctx, public_key): async def require_confirm_multisig(ctx, multisignature): - text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) text.normal("Keys group length: %s" % len(multisignature.keys_group)) text.normal("Life time: %s" % multisignature.life_time) text.normal("Min: %s" % multisignature.min) @@ -44,7 +44,7 @@ async def require_confirm_multisig(ctx, multisignature): async def require_confirm_fee(ctx, value, fee): - text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) text.bold(format_coin_amount(value)) text.normal("fee:") text.bold(format_coin_amount(fee)) diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index ea86aaefb..3ef58d137 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -51,14 +51,14 @@ async def apply_settings(ctx, msg): async def require_confirm_change_homescreen(ctx): text = Text("Change homescreen", ui.ICON_CONFIG) text.normal("Do you really want to", "change homescreen?") - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) async def require_confirm_change_label(ctx, label): text = Text("Change label", ui.ICON_CONFIG) text.normal("Do you really want to", "change label to") text.bold("%s?" % label) - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) async def require_confirm_change_passphrase(ctx, use): @@ -66,7 +66,7 @@ async def require_confirm_change_passphrase(ctx, use): text.normal("Do you really want to") text.normal("enable passphrase" if use else "disable passphrase") text.normal("encryption?") - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) async def require_confirm_change_passphrase_source(ctx, source): @@ -79,7 +79,7 @@ async def require_confirm_change_passphrase_source(ctx, source): text = Text("Passphrase source", ui.ICON_CONFIG) text.normal("Do you really want to", "change the passphrase", "source to") text.bold("ALWAYS %s?" % desc) - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) async def require_confirm_change_display_rotation(ctx, rotation): @@ -95,4 +95,4 @@ async def require_confirm_change_display_rotation(ctx, rotation): text.normal("Do you really want to", "change display rotation") text.normal("to") text.bold("%s?" % label) - await require_confirm(ctx, text, code=ButtonRequestType.ProtectCall) + await require_confirm(ctx, text, ButtonRequestType.ProtectCall) diff --git a/core/src/apps/management/backup_device.py b/core/src/apps/management/backup_device.py index d8c3b7344..15fec3942 100644 --- a/core/src/apps/management/backup_device.py +++ b/core/src/apps/management/backup_device.py @@ -4,8 +4,8 @@ from trezor.messages.Success import Success from apps.common import mnemonic, storage from apps.management.reset_device import ( check_mnemonic, + show_backup_warning, show_mnemonic, - show_warning, show_wrong_entry, ) @@ -19,7 +19,7 @@ async def backup_device(ctx, msg): words = mnemonic.restore() # warn user about mnemonic safety - await show_warning(ctx) + await show_backup_warning(ctx) storage.set_unfinished_backup(True) storage.set_backed_up() diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py index bff28395a..b0c48c30c 100644 --- a/core/src/apps/management/change_pin.py +++ b/core/src/apps/management/change_pin.py @@ -1,8 +1,9 @@ -from trezor import config, loop, ui, wire +from trezor import config, ui, wire from trezor.messages import ButtonRequestType, MessageType from trezor.messages.ButtonRequest import ButtonRequest from trezor.messages.Success import Success from trezor.pin import pin_to_int +from trezor.ui.popup import Popup from trezor.ui.text import Text from apps.common.confirm import require_confirm @@ -81,11 +82,10 @@ async def request_pin_ack(ctx, *args, **kwargs): raise wire.ActionCancelled("Cancelled") -@ui.layout async def pin_mismatch(): - text = Text("PIN mismatch", ui.ICON_WRONG, icon_color=ui.RED) + text = Text("PIN mismatch", ui.ICON_WRONG, ui.RED) text.normal("Entered PINs do not", "match each other.") text.normal("") text.normal("Please, try again...") - text.render() - await loop.sleep(3 * 1000 * 1000) + popup = Popup(text, 3000) # show for 3 seconds + await popup diff --git a/core/src/apps/management/recovery_device.py b/core/src/apps/management/recovery_device.py index 0d050a24b..6b9038d45 100644 --- a/core/src/apps/management/recovery_device.py +++ b/core/src/apps/management/recovery_device.py @@ -19,6 +19,9 @@ from apps.common import mnemonic, storage from apps.common.confirm import require_confirm from apps.management.change_pin import request_pin_ack, request_pin_confirm +if __debug__: + from apps.debug import input_signal + async def recovery_device(ctx, msg): """ @@ -65,7 +68,7 @@ async def recovery_device(ctx, msg): # ask for pin repeatedly if msg.pin_protection: - newpin = await request_pin_confirm(ctx, cancellable=False) + newpin = await request_pin_confirm(ctx, allow_cancel=False) else: newpin = "" @@ -100,26 +103,31 @@ async def recovery_device(ctx, msg): return Success(message="Device recovered") -@ui.layout async def request_wordcount(ctx, title: str) -> int: await ctx.call(ButtonRequest(code=MnemonicWordCount), ButtonAck) text = Text(title, ui.ICON_RECOVERY) text.normal("Number of words?") - count = await ctx.wait(WordSelector(text)) + + if __debug__: + count = await ctx.wait(WordSelector(text), input_signal) + count = int(count) # if input_signal was triggered, count is a string + else: + count = await ctx.wait(WordSelector(text)) return count -@ui.layout async def request_mnemonic(ctx, count: int) -> str: await ctx.call(ButtonRequest(code=MnemonicInput), ButtonAck) words = [] - board = MnemonicKeyboard() for i in range(count): - board.prompt = "Type the %s word:" % format_ordinal(i + 1) - word = await ctx.wait(board) + keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(i + 1)) + if __debug__: + word = await ctx.wait(keyboard, input_signal) + else: + word = await ctx.wait(keyboard) words.append(word) return " ".join(words) diff --git a/core/src/apps/management/reset_device.py b/core/src/apps/management/reset_device.py index 6d4862f3a..25fab76b3 100644 --- a/core/src/apps/management/reset_device.py +++ b/core/src/apps/management/reset_device.py @@ -1,21 +1,19 @@ from micropython import const from ubinascii import hexlify -from trezor import config, ui, wire, workflow +from trezor import config, ui, wire from trezor.crypto import bip39, hashlib, random from trezor.messages import ButtonRequestType, MessageType -from trezor.messages.ButtonRequest import ButtonRequest from trezor.messages.EntropyRequest import EntropyRequest from trezor.messages.Success import Success from trezor.pin import pin_to_int -from trezor.ui.confirm import HoldToConfirmDialog from trezor.ui.mnemonic import MnemonicKeyboard -from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.scroll import Paginated from trezor.ui.text import Text from trezor.utils import chunks, format_ordinal from apps.common import mnemonic, storage -from apps.common.confirm import require_confirm +from apps.common.confirm import hold_to_confirm, require_confirm from apps.management.change_pin import request_pin_confirm if __debug__: @@ -31,18 +29,8 @@ async def reset_device(ctx, msg): if storage.is_initialized(): raise wire.UnexpectedMessage("Already initialized") - text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False) - text.normal("Do you really want to") - text.br() - text.normal("create a new wallet?") - text.br() - text.br_half() - text.normal("By continuing you agree") - text.br() - text.normal("to") - text.bold("https://trezor.io/tos") - - await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice) + # make sure use knows he's setting up a new wallet + await show_reset_warning(ctx) # request new PIN if msg.pin_protection: @@ -63,7 +51,7 @@ async def reset_device(ctx, msg): if not msg.skip_backup and not msg.no_backup: # require confirmation of the mnemonic safety - await show_warning(ctx) + await show_backup_warning(ctx) # show mnemonic and require confirmation of a random word while True: @@ -87,12 +75,9 @@ async def reset_device(ctx, msg): no_backup=msg.no_backup, ) - # show success message. if we skipped backup, it's possible that homescreen - # is still running, uninterrupted. restart it to pick up new label. + # show success message if not msg.skip_backup and not msg.no_backup: await show_success(ctx) - else: - workflow.restartdefault() return Success(message="Initialized") @@ -105,7 +90,21 @@ def generate_mnemonic(strength: int, int_entropy: bytes, ext_entropy: bytes) -> return bip39.from_data(entropy[: strength // 8]) -async def show_warning(ctx): +async def show_reset_warning(ctx): + text = Text("Create a new wallet", ui.ICON_RESET, new_lines=False) + text.normal("Do you really want to") + text.br() + text.normal("create a new wallet?") + text.br() + text.br_half() + text.normal("By continuing you agree") + text.br() + text.normal("to") + text.bold("https://trezor.io/tos") + await require_confirm(ctx, text, code=ButtonRequestType.ResetDevice) + + +async def show_backup_warning(ctx): text = Text("Backup your seed", ui.ICON_NOCOPY) text.normal( "Never make a digital", @@ -119,7 +118,7 @@ async def show_warning(ctx): async def show_wrong_entry(ctx): - text = Text("Wrong entry!", ui.ICON_WRONG, icon_color=ui.RED) + text = Text("Wrong entry!", ui.ICON_WRONG, ui.RED) text.normal("You have entered", "wrong seed word.", "Please check again.") await require_confirm( ctx, text, ButtonRequestType.ResetDevice, confirm="Check again", cancel=None @@ -127,7 +126,7 @@ async def show_wrong_entry(ctx): async def show_success(ctx): - text = Text("Backup is done!", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Backup is done!", ui.ICON_CONFIRM, ui.GREEN) text.normal( "Never make a digital", "copy of your recovery", @@ -148,32 +147,33 @@ async def show_entropy(ctx, entropy: bytes): async def show_mnemonic(ctx, mnemonic: str): - await ctx.call( - ButtonRequest(code=ButtonRequestType.ResetDevice), MessageType.ButtonAck - ) - first_page = const(0) - words_per_page = const(4) - words = list(enumerate(mnemonic.split())) - pages = list(chunks(words, words_per_page)) - paginator = paginate(show_mnemonic_page, len(pages), first_page, pages) - await ctx.wait(paginator) + # split mnemonic words into pages + PER_PAGE = const(4) + words = mnemonic.split() + words = list(enumerate(words)) + words = list(chunks(words, PER_PAGE)) + # display the pages, with a confirmation dialog on the last one + pages = [get_mnemonic_page(page) for page in words] + paginated = Paginated(pages) -@ui.layout -async def show_mnemonic_page(page: int, page_count: int, pages: list): if __debug__: - debug.reset_current_words = [word for _, word in pages[page]] - lines = ["%2d. %s" % (wi + 1, word) for wi, word in pages[page]] - text = Text("Recovery seed", ui.ICON_RESET) - text.mono(*lines) - content = Scrollpage(text, page, page_count) + def export_displayed_words(): + # export currently displayed mnemonic words into debuglink + debug.reset_current_words = [w for _, w in words[paginated.page]] - if page + 1 == page_count: - await HoldToConfirmDialog(content) - else: - content.render() - await animate_swipe() + paginated.on_change = export_displayed_words + export_displayed_words() + + await hold_to_confirm(ctx, paginated, ButtonRequestType.ResetDevice) + + +def get_mnemonic_page(words: list): + text = Text("Recovery seed", ui.ICON_RESET) + for index, word in words: + text.mono("%2d. %s" % (index + 1, word)) + return text async def check_mnemonic(ctx, mnemonic: str) -> bool: @@ -192,11 +192,12 @@ async def check_mnemonic(ctx, mnemonic: str) -> bool: return True -@ui.layout async def check_word(ctx, words: list, index: int): if __debug__: debug.reset_word_index = index - keyboard = MnemonicKeyboard("Type the %s word:" % format_ordinal(index + 1)) - result = await ctx.wait(keyboard) + if __debug__: + result = await ctx.wait(keyboard, debug.input_signal) + else: + result = await ctx.wait(keyboard) return result == words[index] diff --git a/core/src/apps/management/wipe_device.py b/core/src/apps/management/wipe_device.py index e8e00a032..268506cd1 100644 --- a/core/src/apps/management/wipe_device.py +++ b/core/src/apps/management/wipe_device.py @@ -1,6 +1,8 @@ from trezor import ui from trezor.messages import ButtonRequestType from trezor.messages.Success import Success +from trezor.ui.button import ButtonCancel +from trezor.ui.loader import LoaderDanger from trezor.ui.text import Text from apps.common import storage @@ -8,17 +10,15 @@ from apps.common.confirm import require_hold_to_confirm async def wipe_device(ctx, msg): - - text = Text("Wipe device", ui.ICON_WIPE, icon_color=ui.RED) + text = Text("Wipe device", ui.ICON_WIPE, ui.RED) text.normal("Do you really want to", "wipe the device?", "") text.bold("All data will be lost.") - await require_hold_to_confirm( ctx, text, - code=ButtonRequestType.WipeDevice, - button_style=ui.BTN_CANCEL, - loader_style=ui.LDR_DANGER, + ButtonRequestType.WipeDevice, + confirm_style=ButtonCancel, + loader_style=LoaderDanger, ) storage.wipe() diff --git a/core/src/apps/monero/layout/common.py b/core/src/apps/monero/layout/common.py index a7b2c74c1..1820f002f 100644 --- a/core/src/apps/monero/layout/common.py +++ b/core/src/apps/monero/layout/common.py @@ -1,7 +1,43 @@ -from trezor import res, ui, utils -from trezor.messages import ButtonRequestType +from trezor import loop, ui, utils +from trezor.messages import ButtonRequestType, MessageType +from trezor.messages.ButtonRequest import ButtonRequest from trezor.ui.text import Text -from trezor.utils import chunks + +if __debug__: + from apps.debug import confirm_signal + + +async def naive_pagination( + ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5 +): + from trezor.ui.scroll import CANCELLED, CONFIRMED, PaginatedWithButtons + + pages = [] + page_lines = paginate_lines(lines, per_page) + + for i, lines in enumerate(page_lines): + if len(page_lines) > 1: + paging = "%s/%s" % (i + 1, len(page_lines)) + else: + paging = "" + text = Text("%s %s" % (title, paging), icon, icon_color) + text.normal(*lines) + pages.append(text) + + paginated = PaginatedWithButtons(pages, one_by_one=True) + + while True: + await ctx.call( + ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck + ) + if __debug__: + result = await loop.spawn(paginated, confirm_signal) + else: + result = await paginated + if result is CONFIRMED: + return True + if result is CANCELLED: + return False def paginate_lines(lines, lines_per_page=5): @@ -28,94 +64,9 @@ def paginate_lines(lines, lines_per_page=5): return pages -@ui.layout -async def tx_dialog( - ctx, - code, - content, - cancel_btn, - confirm_btn, - cancel_style, - confirm_style, - scroll_tuple=None, -): - from trezor.messages import MessageType - from trezor.messages.ButtonRequest import ButtonRequest - from trezor.ui.confirm import ConfirmDialog - from trezor.ui.scroll import Scrollpage - - await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck) - - if scroll_tuple and scroll_tuple[1] > 1: - content = Scrollpage(content, scroll_tuple[0], scroll_tuple[1]) - - dialog = ConfirmDialog( - content, - cancel=cancel_btn, - confirm=confirm_btn, - cancel_style=cancel_style, - confirm_style=confirm_style, - ) - return await ctx.wait(dialog) - - -async def naive_pagination( - ctx, lines, title, icon=ui.ICON_RESET, icon_color=ui.ORANGE, per_page=5 -): - from trezor.ui.confirm import CANCELLED, CONFIRMED, DEFAULT_CANCEL, DEFAULT_CONFIRM - - if isinstance(lines, (list, tuple)): - lines = lines - else: - lines = list(chunks(lines, 16)) - - pages = paginate_lines(lines, per_page) - npages = len(pages) - cur_step = 0 - code = ButtonRequestType.SignTx - iback = res.load(ui.ICON_BACK) - inext = res.load(ui.ICON_CLICK) - - while cur_step <= npages: - text = pages[cur_step] - fst_page = cur_step == 0 - lst_page = cur_step + 1 >= npages - - cancel_btn = DEFAULT_CANCEL if fst_page else iback - cancel_style = ui.BTN_CANCEL if fst_page else ui.BTN_DEFAULT - confirm_btn = DEFAULT_CONFIRM if lst_page else inext - confirm_style = ui.BTN_CONFIRM if lst_page else ui.BTN_DEFAULT - - paging = ("%d/%d" % (cur_step + 1, npages)) if npages > 1 else "" - content = Text("%s %s" % (title, paging), icon, icon_color=icon_color) - content.normal(*text) - - reaction = await tx_dialog( - ctx, - code, - content, - cancel_btn, - confirm_btn, - cancel_style, - confirm_style, - (cur_step, npages), - ) - - if fst_page and reaction == CANCELLED: - return False - elif not lst_page and reaction == CONFIRMED: - cur_step += 1 - elif lst_page and reaction == CONFIRMED: - return True - elif reaction == CANCELLED: - cur_step -= 1 - elif reaction == CONFIRMED: - cur_step += 1 - - def format_amount(value): return "%s XMR" % utils.format_amount(value, 12) def split_address(address): - return chunks(address, 16) + return utils.chunks(address, 16) diff --git a/core/src/apps/monero/layout/confirms.py b/core/src/apps/monero/layout/confirms.py index 64bd7ccf4..c79a715e9 100644 --- a/core/src/apps/monero/layout/confirms.py +++ b/core/src/apps/monero/layout/confirms.py @@ -2,6 +2,7 @@ from ubinascii import hexlify from trezor import ui, wire from trezor.messages import ButtonRequestType +from trezor.ui.popup import Popup from trezor.ui.text import Text from trezor.utils import chunks @@ -12,25 +13,25 @@ DUMMY_PAYMENT_ID = b"\x00" * 8 async def require_confirm_watchkey(ctx): - content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN) + content = Text("Confirm export", ui.ICON_SEND, ui.GREEN) content.normal("Do you really want to", "export watch-only", "credentials?") return await require_confirm(ctx, content, ButtonRequestType.SignTx) async def require_confirm_keyimage_sync(ctx): - content = Text("Confirm ki sync", ui.ICON_SEND, icon_color=ui.GREEN) + content = Text("Confirm ki sync", ui.ICON_SEND, ui.GREEN) content.normal("Do you really want to", "sync key images?") return await require_confirm(ctx, content, ButtonRequestType.SignTx) async def require_confirm_live_refresh(ctx): - content = Text("Confirm refresh", ui.ICON_SEND, icon_color=ui.GREEN) + content = Text("Confirm refresh", ui.ICON_SEND, ui.GREEN) content.normal("Do you really want to", "start refresh?") return await require_confirm(ctx, content, ButtonRequestType.SignTx) async def require_confirm_tx_key(ctx, export_key=False): - content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN) + content = Text("Confirm export", ui.ICON_SEND, ui.GREEN) txt = ["Do you really want to"] if export_key: txt.append("export tx_key?") @@ -111,14 +112,55 @@ async def _require_confirm_payment_id(ctx, payment_id): async def _require_confirm_fee(ctx, fee): - content = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN) + content = Text("Confirm fee", ui.ICON_SEND, ui.GREEN) content.bold(common.format_amount(fee)) await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput) -@ui.layout_no_slide +class TransactionStep(ui.Control): + def __init__(self, state, info): + self.state = state + self.info = info + + def on_render(self): + state = self.state + info = self.info + ui.header("Signing transaction", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE) + p = 1000 * state.progress_cur // state.progress_total + ui.display.loader(p, False, -4, ui.WHITE, ui.BG) + ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG) + if len(info) > 1: + ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG) + + +class KeyImageSyncStep(ui.Control): + def __init__(self, current, total_num): + self.current = current + self.total_num = total_num + + def on_render(self): + current = self.current + total_num = self.total_num + ui.header("Syncing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE) + p = (1000 * (current + 1) // total_num) if total_num > 0 else 0 + ui.display.loader(p, False, 18, ui.WHITE, ui.BG) + + +class LiveRefreshStep(ui.Control): + def __init__(self, current): + self.current = current + + def on_render(self): + current = self.current + ui.header("Refreshing", ui.ICON_SEND, ui.TITLE_GREY, ui.BG, ui.BLUE) + p = (1000 * current // 8) % 1000 + ui.display.loader(p, True, 18, ui.WHITE, ui.BG) + ui.display.text_center( + ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG + ) + + async def transaction_step(state, step, sub_step=None): - info = [] if step == 0: info = ["Signing..."] elif step == 100: @@ -139,43 +181,16 @@ async def transaction_step(state, step, sub_step=None): info = ["Processing..."] state.progress_cur += 1 - - ui.display.clear() - text = Text("Signing transaction", ui.ICON_SEND, icon_color=ui.BLUE) - text.render() - - p = 1000 * state.progress_cur // state.progress_total - ui.display.loader(p, False, -4, ui.WHITE, ui.BG) - ui.display.text_center(ui.WIDTH // 2, 210, info[0], ui.NORMAL, ui.FG, ui.BG) - if len(info) > 1: - ui.display.text_center(ui.WIDTH // 2, 235, info[1], ui.NORMAL, ui.FG, ui.BG) - ui.display.refresh() + await Popup(TransactionStep(state, info)) -@ui.layout_no_slide async def keyimage_sync_step(ctx, current, total_num): if current is None: return - ui.display.clear() - text = Text("Syncing", ui.ICON_SEND, icon_color=ui.BLUE) - text.render() + await Popup(KeyImageSyncStep(current, total_num)) - p = (1000 * (current + 1) // total_num) if total_num > 0 else 0 - ui.display.loader(p, False, 18, ui.WHITE, ui.BG) - ui.display.refresh() - -@ui.layout_no_slide async def live_refresh_step(ctx, current): if current is None: return - ui.display.clear() - text = Text("Refreshing", ui.ICON_SEND, icon_color=ui.BLUE) - text.render() - - step = 8 - p = (1000 * current // step) % 1000 - - ui.display.loader(p, True, 18, ui.WHITE, ui.BG) - ui.display.text_center(ui.WIDTH // 2, 145, "%d" % current, ui.NORMAL, ui.FG, ui.BG) - ui.display.refresh() + await Popup(LiveRefreshStep(current)) diff --git a/core/src/apps/nem/layout.py b/core/src/apps/nem/layout.py index 6c116218b..acba46d99 100644 --- a/core/src/apps/nem/layout.py +++ b/core/src/apps/nem/layout.py @@ -10,7 +10,7 @@ from apps.common.confirm import require_confirm, require_hold_to_confirm async def require_confirm_text(ctx, action: str): content = action.split(" ") - text = Text("Confirm action", ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False) + text = Text("Confirm action", ui.ICON_SEND, ui.GREEN, new_lines=False) text.normal(*content) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -26,13 +26,13 @@ async def require_confirm_fee(ctx, action: str, fee: int): async def require_confirm_content(ctx, headline: str, content: list): - text = Text(headline, ui.ICON_SEND, icon_color=ui.GREEN) + text = Text(headline, ui.ICON_SEND, ui.GREEN) text.normal(*content) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) async def require_confirm_final(ctx, fee: int): - text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Final confirm", ui.ICON_SEND, ui.GREEN) text.normal("Sign this transaction") text.bold("and pay %s XEM" % format_amount(fee, NEM_MAX_DIVISIBILITY)) text.normal("for network fee?") diff --git a/core/src/apps/nem/mosaic/layout.py b/core/src/apps/nem/mosaic/layout.py index 0c19736b2..c270de4c5 100644 --- a/core/src/apps/nem/mosaic/layout.py +++ b/core/src/apps/nem/mosaic/layout.py @@ -1,5 +1,3 @@ -from micropython import const - from trezor import ui, wire from trezor.messages import ( NEMMosaicCreation, @@ -9,8 +7,8 @@ from trezor.messages import ( NEMSupplyChangeType, NEMTransactionCommon, ) -from trezor.ui.confirm import CONFIRMED, ConfirmDialog -from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.confirm import CONFIRMED, Confirm +from trezor.ui.scroll import Paginated from trezor.ui.text import Text from ..layout import ( @@ -22,6 +20,9 @@ from ..layout import ( from apps.common.layout import split_address +if __debug__: + from apps.debug import confirm_signal + async def ask_mosaic_creation( ctx, common: NEMTransactionCommon, creation: NEMMosaicCreation @@ -76,21 +77,16 @@ def _supply_message(supply_change): async def _require_confirm_properties(ctx, definition: NEMMosaicDefinition): # TODO: we should send a button request here - properties = _get_mosaic_properties(definition) - first_page = const(0) - paginator = paginate(_show_page, len(properties), first_page, properties) - await ctx.wait(paginator) - - -@ui.layout -async def _show_page(page: int, page_count: int, content): - content = Scrollpage(content[page], page, page_count) - if page + 1 == page_count: - if await ConfirmDialog(content) != CONFIRMED: - raise wire.ActionCancelled("Action cancelled") + pages = _get_mosaic_properties(definition) + pages[-1] = Confirm(pages[-1]) + paginated = Paginated(pages) + + if __debug__: + result = await ctx.wait(paginated, confirm_signal) else: - content.render() - await animate_swipe() + result = await ctx.wait(paginated) + if result is not CONFIRMED: + raise wire.ActionCancelled("Action cancelled") def _get_mosaic_properties(definition: NEMMosaicDefinition): diff --git a/core/src/apps/nem/multisig/layout.py b/core/src/apps/nem/multisig/layout.py index 5c91fddb6..3857af9e7 100644 --- a/core/src/apps/nem/multisig/layout.py +++ b/core/src/apps/nem/multisig/layout.py @@ -53,7 +53,7 @@ async def ask_aggregate_modification( async def _require_confirm_address(ctx, action: str, address: str): - text = Text("Confirm address", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm address", ui.ICON_SEND, ui.GREEN) text.normal(action) text.mono(*split_address(address)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) diff --git a/core/src/apps/nem/transfer/layout.py b/core/src/apps/nem/transfer/layout.py index a0c5a78ba..b9942b28e 100644 --- a/core/src/apps/nem/transfer/layout.py +++ b/core/src/apps/nem/transfer/layout.py @@ -48,7 +48,7 @@ async def ask_transfer_mosaic( mosaic_quantity = mosaic.quantity * transfer.amount / NEM_MOSAIC_AMOUNT_DIVISOR if definition: - msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN) + msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN) msg.normal("Confirm transfer of") msg.bold( format_amount(mosaic_quantity, definition["divisibility"]) @@ -60,20 +60,20 @@ async def ask_transfer_mosaic( if "levy" in definition and "fee" in definition: levy_msg = _get_levy_msg(definition, mosaic_quantity, common.network) - msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN) + msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN) msg.normal("Confirm mosaic", "levy fee of") msg.bold(levy_msg) await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) else: - msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.RED) + msg = Text("Confirm mosaic", ui.ICON_SEND, ui.RED) msg.bold("Unknown mosaic!") msg.normal("Divisibility and levy") msg.normal("cannot be shown for") msg.normal("unknown mosaics") await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) - msg = Text("Confirm mosaic", ui.ICON_SEND, icon_color=ui.GREEN) + msg = Text("Confirm mosaic", ui.ICON_SEND, ui.GREEN) msg.normal("Confirm transfer of") msg.bold("%s raw units" % mosaic_quantity) msg.normal("of") @@ -121,7 +121,7 @@ async def ask_importance_transfer( async def _require_confirm_transfer(ctx, recipient, value): - text = Text("Confirm transfer", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transfer", ui.ICON_SEND, ui.GREEN) text.bold("Send %s XEM" % format_amount(value, NEM_MAX_DIVISIBILITY)) text.normal("to") text.mono(*split_address(recipient)) @@ -132,11 +132,11 @@ async def _require_confirm_payload(ctx, payload: bytearray, encrypt=False): payload = bytes(payload).decode() if encrypt: - text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm payload", ui.ICON_SEND, ui.GREEN) text.bold("Encrypted:") text.normal(*payload.split(" ")) else: - text = Text("Confirm payload", ui.ICON_SEND, icon_color=ui.RED) + text = Text("Confirm payload", ui.ICON_SEND, ui.RED) text.bold("Unencrypted:") text.normal(*payload.split(" ")) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) diff --git a/core/src/apps/ripple/layout.py b/core/src/apps/ripple/layout.py index 35aae084c..425d8644c 100644 --- a/core/src/apps/ripple/layout.py +++ b/core/src/apps/ripple/layout.py @@ -10,14 +10,14 @@ from apps.common.layout import split_address async def require_confirm_fee(ctx, fee): - text = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm fee", ui.ICON_SEND, ui.GREEN) text.normal("Transaction fee:") text.bold(format_amount(fee, helpers.DIVISIBILITY) + " XRP") await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) async def require_confirm_destination_tag(ctx, tag): - text = Text("Confirm tag", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm tag", ui.ICON_SEND, ui.GREEN) text.normal("Destination tag:") text.bold(str(tag)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -25,7 +25,7 @@ async def require_confirm_destination_tag(ctx, tag): async def require_confirm_tx(ctx, to, value): - text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN) text.bold(format_amount(value, helpers.DIVISIBILITY) + " XRP") text.normal("to") text.mono(*split_address(to)) diff --git a/core/src/apps/stellar/layout.py b/core/src/apps/stellar/layout.py index 92cce082c..f4d132170 100644 --- a/core/src/apps/stellar/layout.py +++ b/core/src/apps/stellar/layout.py @@ -9,7 +9,7 @@ from apps.stellar import consts async def require_confirm_init( ctx, address: str, network_passphrase: str, accounts_match: bool ): - text = Text("Confirm Stellar", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm Stellar", ui.ICON_SEND, ui.GREEN) text.normal("Initialize signing with") if accounts_match: text.normal("your account") @@ -19,14 +19,14 @@ async def require_confirm_init( await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) network = get_network_warning(network_passphrase) if network: - text = Text("Confirm network", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm network", ui.ICON_CONFIRM, ui.GREEN) text.normal("Transaction is on") text.bold(network) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) async def require_confirm_memo(ctx, memo_type: int, memo_text: str): - text = Text("Confirm memo", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm memo", ui.ICON_CONFIRM, ui.GREEN) if memo_type == consts.MEMO_TYPE_TEXT: text.bold("Memo (TEXT)") elif memo_type == consts.MEMO_TYPE_ID: @@ -47,7 +47,7 @@ async def require_confirm_final(ctx, fee: int, num_operations: int): op_str = str(num_operations) + " operation" if num_operations > 1: op_str += "s" - text = Text("Final confirm", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Final confirm", ui.ICON_SEND, ui.GREEN) text.normal("Sign this transaction") text.normal("made up of " + op_str) text.bold("and pay " + format_amount(fee)) diff --git a/core/src/apps/stellar/operations/layout.py b/core/src/apps/stellar/operations/layout.py index ec86bb36b..fae0828d6 100644 --- a/core/src/apps/stellar/operations/layout.py +++ b/core/src/apps/stellar/operations/layout.py @@ -21,7 +21,7 @@ from apps.stellar.layout import format_amount, require_confirm, split, trim_to_r async def confirm_source_account(ctx, source_account: bytes): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Source account:") text.mono(*split(source_account)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -32,7 +32,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp): t = "Allow Trust" else: t = "Revoke Trust" - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold(t) text.normal("of %s by:" % op.asset_code) text.mono(*split(trim_to_rows(op.trusted_account, 3))) @@ -41,7 +41,7 @@ async def confirm_allow_trust_op(ctx, op: StellarAllowTrustOp): async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Account Merge") text.normal("All XLM will be sent to:") text.mono(*split(trim_to_rows(op.destination_account, 3))) @@ -49,7 +49,7 @@ async def confirm_account_merge_op(ctx, op: StellarAccountMergeOp): async def confirm_bump_sequence_op(ctx, op: StellarBumpSequenceOp): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Bump Sequence") text.normal("Set sequence to") text.mono(str(op.bump_to)) @@ -61,7 +61,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp): t = "Delete Trust" else: t = "Add Trust" - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold(t) text.normal("Asset: %s" % op.asset.code) text.normal("Amount: %s" % format_amount(op.limit, ticker=False)) @@ -70,7 +70,7 @@ async def confirm_change_trust_op(ctx, op: StellarChangeTrustOp): async def confirm_create_account_op(ctx, op: StellarCreateAccountOp): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Create Account") text.normal("with %s" % format_amount(op.starting_balance)) text.mono(*split(trim_to_rows(op.new_account, 3))) @@ -98,7 +98,7 @@ async def confirm_manage_offer_op(ctx, op: StellarManageOfferOp): async def _confirm_offer(ctx, title, op): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold(title) text.normal( "Sell %s %s" % (format_amount(op.amount, ticker=False), op.selling_asset.code) @@ -117,28 +117,28 @@ async def confirm_manage_data_op(ctx, op: StellarManageDataOp): title = "Set" else: title = "Clear" - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("%s data value key" % title) text.mono(*split(op.key)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) if op.value: digest = sha256(op.value).digest() digest_str = hexlify(digest).decode() - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Value (SHA-256):") text.mono(*split(digest_str)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Path Pay %s" % format_amount(op.destination_amount, ticker=False)) text.bold("%s to:" % format_asset_code(op.destination_asset)) text.mono(*split(trim_to_rows(op.destination_account, 3))) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) await confirm_asset_issuer(ctx, op.destination_asset) # confirm what the sender is using to pay - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.normal("Pay using") text.bold(format_amount(op.send_max, ticker=False)) text.bold(format_asset_code(op.send_asset)) @@ -149,7 +149,7 @@ async def confirm_path_payment_op(ctx, op: StellarPathPaymentOp): async def confirm_payment_op(ctx, op: StellarPaymentOp): - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Pay %s" % format_amount(op.amount, ticker=False)) text.bold("%s to:" % format_asset_code(op.asset)) text.mono(*split(trim_to_rows(op.destination_account, 3))) @@ -159,30 +159,30 @@ async def confirm_payment_op(ctx, op: StellarPaymentOp): async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): if op.inflation_destination_account: - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Set Inflation Destination") text.mono(*split(op.inflation_destination_account)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) if op.clear_flags: t = _format_flags(op.clear_flags) - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Clear Flags") text.mono(*t) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) if op.set_flags: t = _format_flags(op.set_flags) - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Set Flags") text.mono(*t) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) thresholds = _format_thresholds(op) if thresholds: - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Account Thresholds") text.mono(*thresholds) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) if op.home_domain: - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold("Home Domain") text.mono(*split(op.home_domain)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -192,7 +192,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): else: t = "Remove Signer (%s)" if op.signer_type == consts.SIGN_TYPE_ACCOUNT: - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold(t % "acc") text.mono(*split(helpers.address_from_public_key(op.signer_key))) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -201,7 +201,7 @@ async def confirm_set_options_op(ctx, op: StellarSetOptionsOp): signer_type = "auth" else: signer_type = "hash" - text = Text("Confirm operation", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm operation", ui.ICON_CONFIRM, ui.GREEN) text.bold(t % signer_type) text.mono(*split(hexlify(op.signer_key).decode())) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) @@ -244,7 +244,7 @@ def format_asset_code(asset: StellarAssetType) -> str: async def confirm_asset_issuer(ctx, asset: StellarAssetType): if asset is None or asset.type == consts.ASSET_TYPE_NATIVE: return - text = Text("Confirm issuer", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text = Text("Confirm issuer", ui.ICON_CONFIRM, ui.GREEN) text.bold("%s issuer:" % asset.code) text.mono(*split(asset.issuer)) await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) diff --git a/core/src/apps/tezos/get_public_key.py b/core/src/apps/tezos/get_public_key.py index ecb06bc0e..6a6fd9ec9 100644 --- a/core/src/apps/tezos/get_public_key.py +++ b/core/src/apps/tezos/get_public_key.py @@ -26,6 +26,6 @@ async def get_public_key(ctx, msg, keychain): async def _show_tezos_pubkey(ctx, pubkey): lines = chunks(pubkey, 18) - text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN) + text = Text("Confirm public key", ui.ICON_RECEIVE, ui.GREEN) text.mono(*lines) return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey) diff --git a/core/src/apps/tezos/layout.py b/core/src/apps/tezos/layout.py index 325d73b26..672de5385 100644 --- a/core/src/apps/tezos/layout.py +++ b/core/src/apps/tezos/layout.py @@ -1,10 +1,6 @@ -from micropython import const - -from trezor import ui, wire -from trezor.messages import ButtonRequestType, MessageType -from trezor.messages.ButtonRequest import ButtonRequest -from trezor.ui.confirm import CANCELLED, ConfirmDialog -from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.ui.scroll import Paginated from trezor.ui.text import Text from trezor.utils import chunks, format_amount @@ -13,7 +9,7 @@ from apps.tezos.helpers import TEZOS_AMOUNT_DIVISIBILITY async def require_confirm_tx(ctx, to, value): - text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN) text.bold(format_tezos_amount(value)) text.normal("to") text.mono(*split_address(to)) @@ -21,7 +17,7 @@ async def require_confirm_tx(ctx, to, value): async def require_confirm_fee(ctx, value, fee): - text = Text("Confirm transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN) text.normal("Amount:") text.bold(format_tezos_amount(value)) text.normal("Fee:") @@ -30,14 +26,14 @@ async def require_confirm_fee(ctx, value, fee): async def require_confirm_origination(ctx, address): - text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE) + text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE) text.normal("Address:") text.mono(*split_address(address)) return await require_confirm(ctx, text, ButtonRequestType.SignTx) async def require_confirm_origination_fee(ctx, balance, fee): - text = Text("Confirm origination", ui.ICON_SEND, icon_color=ui.ORANGE) + text = Text("Confirm origination", ui.ICON_SEND, ui.ORANGE) text.normal("Balance:") text.bold(format_tezos_amount(balance)) text.normal("Fee:") @@ -46,21 +42,21 @@ async def require_confirm_origination_fee(ctx, balance, fee): async def require_confirm_delegation_baker(ctx, baker): - text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE) + text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE) text.normal("Baker address:") text.mono(*split_address(baker)) return await require_confirm(ctx, text, ButtonRequestType.SignTx) async def require_confirm_set_delegate(ctx, fee): - text = Text("Confirm delegation", ui.ICON_SEND, icon_color=ui.BLUE) + text = Text("Confirm delegation", ui.ICON_SEND, ui.BLUE) text.normal("Fee:") text.bold(format_tezos_amount(fee)) await require_hold_to_confirm(ctx, text, ButtonRequestType.SignTx) async def require_confirm_register_delegate(ctx, address, fee): - text = Text("Register delegate", ui.ICON_SEND, icon_color=ui.BLUE) + text = Text("Register delegate", ui.ICON_SEND, ui.BLUE) text.bold("Fee: " + format_tezos_amount(fee)) text.normal("Address:") text.mono(*split_address(address)) @@ -88,28 +84,18 @@ async def require_confirm_ballot(ctx, proposal, ballot): await require_confirm(ctx, text, ButtonRequestType.SignTx) -# use, when there are more then one proposals in one operation async def require_confirm_proposals(ctx, proposals): - await ctx.call(ButtonRequest(code=ButtonRequestType.SignTx), MessageType.ButtonAck) - first_page = const(0) - pages = proposals - title = "Submit proposals" if len(proposals) > 1 else "Submit proposal" - - paginator = paginate(show_proposal_page, len(pages), first_page, pages, title) - return await ctx.wait(paginator) - - -@ui.layout -async def show_proposal_page(page: int, page_count: int, pages: list, title: str): - text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE) - text.bold("Proposal {}: ".format(page + 1)) - text.mono(*split_proposal(pages[page])) - content = Scrollpage(text, page, page_count) - - if page + 1 >= page_count: - confirm = await ConfirmDialog(content) - if confirm == CANCELLED: - raise wire.ActionCancelled("Cancelled") + if len(proposals) > 1: + title = "Submit proposals" else: - content.render() - await animate_swipe() + title = "Submit proposal" + + pages = [] + for page, proposal in enumerate(proposals): + text = Text(title, ui.ICON_SEND, icon_color=ui.PURPLE) + text.bold("Proposal {}: ".format(page + 1)) + text.mono(*split_proposal(proposal)) + pages.append(text) + paginated = Paginated(pages) + + await require_confirm(ctx, paginated, ButtonRequestType.SignTx) diff --git a/core/src/apps/wallet/sign_tx/layout.py b/core/src/apps/wallet/sign_tx/layout.py index 7744383db..229fa9ce2 100644 --- a/core/src/apps/wallet/sign_tx/layout.py +++ b/core/src/apps/wallet/sign_tx/layout.py @@ -29,19 +29,19 @@ async def confirm_output(ctx, output, coin): data = output.op_return_data if omni.is_valid(data): # OMNI transaction - text = Text("OMNI transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("OMNI transaction", ui.ICON_SEND, ui.GREEN) text.normal(omni.parse(data)) else: # generic OP_RETURN data = hexlify(data).decode() if len(data) >= 18 * 5: data = data[: (18 * 5 - 3)] + "..." - text = Text("OP_RETURN", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("OP_RETURN", ui.ICON_SEND, ui.GREEN) text.mono(*split_op_return(data)) else: address = output.address address_short = addresses.address_short(coin, address) - text = Text("Confirm sending", ui.ICON_SEND, icon_color=ui.GREEN) + text = Text("Confirm sending", ui.ICON_SEND, ui.GREEN) text.normal(format_coin_amount(output.amount, coin) + " to") text.mono(*split_address(address_short)) return await confirm(ctx, text, ButtonRequestType.ConfirmOutput) diff --git a/core/src/apps/webauthn/__init__.py b/core/src/apps/webauthn/__init__.py index 9c2e95696..66f3ad824 100644 --- a/core/src/apps/webauthn/__init__.py +++ b/core/src/apps/webauthn/__init__.py @@ -383,37 +383,32 @@ class ConfirmState: workflow.onclose(self.workflow) self.workflow = None - @ui.layout async def confirm_layout(self) -> None: - workflow.webauthn_stop_signal.reset() - await loop.spawn(self.confirm_layout_inner(), workflow.webauthn_stop_signal) - - async def confirm_layout_inner(self) -> None: - from trezor.ui.confirm import ConfirmDialog, CONFIRMED + from trezor.ui.confirm import Confirm, CONFIRMED from trezor.ui.text import Text app_id = bytes(self.app_id) # could be bytearray, which doesn't have __hash__ if app_id == _BOGUS_APPID and self.action == _CONFIRM_REGISTER: - text = Text("U2F", ui.ICON_WRONG, icon_color=ui.RED) + text = Text("U2F", ui.ICON_WRONG, ui.RED) text.normal( "Another U2F device", "was used to register", "in this application." ) - text.render() - dialog = ConfirmDialog(text) + dialog = Confirm(text) else: content = ConfirmContent(self.action, app_id) - dialog = ConfirmDialog(content) + dialog = Confirm(content) - self.confirmed = await dialog == CONFIRMED + self.confirmed = await dialog is CONFIRMED -class ConfirmContent(ui.Widget): +class ConfirmContent(ui.Control): def __init__(self, action: int, app_id: bytes) -> None: self.action = action self.app_id = app_id self.app_name = None self.app_icon = None + self.repaint = True self.boot() def boot(self) -> None: @@ -439,14 +434,18 @@ class ConfirmContent(ui.Widget): self.app_name = name self.app_icon = icon - def render(self) -> None: - if self.action == _CONFIRM_REGISTER: - header = "U2F Register" - else: - header = "U2F Authenticate" - ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN) - ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon) - ui.display.text_center(ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG) + def on_render(self) -> None: + if self.repaint: + if self.action == _CONFIRM_REGISTER: + header = "U2F Register" + else: + header = "U2F Authenticate" + ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN) + ui.display.image((ui.WIDTH - 64) // 2, 64, self.app_icon) + ui.display.text_center( + ui.WIDTH // 2, 168, self.app_name, ui.MONO, ui.FG, ui.BG + ) + self.repaint = False def dispatch_cmd(req: Cmd, state: ConfirmState) -> Cmd: diff --git a/core/src/boot.py b/core/src/boot.py index 1368da229..dd058436d 100644 --- a/core/src/boot.py +++ b/core/src/boot.py @@ -14,7 +14,7 @@ async def bootscreen(): storage.init_unlocked() return await lockscreen() - label = None + label = "Enter your PIN" while True: pin = await request_pin(label, config.get_pin_rem()) if config.unlock(pin_to_int(pin)): @@ -35,7 +35,7 @@ async def lockscreen(): if not image: image = res.load("apps/homescreen/res/bg.toif") - await ui.backlight_slide(ui.BACKLIGHT_DIM) + ui.backlight_fade(ui.BACKLIGHT_DIM) ui.display.clear() ui.display.avatar(48, 48, image, ui.TITLE_GREY, ui.BG) @@ -50,12 +50,13 @@ async def lockscreen(): ) ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG) - await ui.backlight_slide(ui.BACKLIGHT_NORMAL) + ui.backlight_fade(ui.BACKLIGHT_NORMAL) + await ui.click() ui.display.backlight(ui.BACKLIGHT_NONE) -ui.backlight_slide_sync(ui.BACKLIGHT_NORMAL) +ui.backlight_fade(ui.BACKLIGHT_NORMAL) config.init(show_pin_timeout) loop.schedule(bootscreen()) loop.run() diff --git a/core/src/trezor/loop.py b/core/src/trezor/loop.py index 7d25e5583..5a4424640 100644 --- a/core/src/trezor/loop.py +++ b/core/src/trezor/loop.py @@ -18,6 +18,7 @@ after_step_hook = None # function, called after each task step _QUEUE_SIZE = const(64) # maximum number of scheduled tasks _queue = utimeq.utimeq(_QUEUE_SIZE) _paused = {} +_finalizers = {} if __debug__: # for performance stats @@ -28,13 +29,15 @@ if __debug__: log_delay_rb = array.array("i", [0] * log_delay_rb_len) -def schedule(task, value=None, deadline=None): +def schedule(task, value=None, deadline=None, finalizer=None): """ Schedule task to be executed with `value` on given `deadline` (in microseconds). Does not start the event loop itself, see `run`. """ if deadline is None: deadline = utime.ticks_us() + if finalizer is not None: + _finalizers[id(task)] = finalizer _queue.push(deadline, task, value) @@ -45,11 +48,18 @@ def pause(task, iface): tasks.add(task) +def finalize(task, value): + fn = _finalizers.pop(id(task), None) + if fn is not None: + fn(task, value) + + def close(task): for iface in _paused: _paused[iface].discard(task) _queue.discard(task) task.close() + finalize(task, GeneratorExit()) def run(): @@ -93,16 +103,18 @@ def run(): def _step(task, value): try: - if isinstance(value, Exception): + if isinstance(value, BaseException): result = task.throw(value) else: result = task.send(value) - except StopIteration: # as e: + except StopIteration as e: # as e: if __debug__: log.debug(__name__, "finish: %s", task) + finalize(task, e.value) except Exception as e: if __debug__: log.exception(__name__, e) + finalize(task, e) else: if isinstance(result, Syscall): result.handle(task) @@ -213,6 +225,9 @@ class signal(Syscall): raise +_type_gen = type((lambda: (yield))()) + + class spawn(Syscall): """ Execute one or more children tasks and wait until one of them exits. @@ -241,39 +256,41 @@ class spawn(Syscall): def __init__(self, *children, exit_others=True): self.children = children self.exit_others = exit_others - self.scheduled = None # list of scheduled wrapper tasks - self.finished = None # list of children that finished + self.scheduled = [] # list of scheduled tasks + self.finished = [] # list of children that finished self.callback = None def handle(self, task): - self.callback = task - self.finished = [] - self.scheduled = [] - for index, child in enumerate(self.children): - parent = self._wait(child, index) - schedule(parent) - self.scheduled.append(parent) - - def exit(self, skip_index=-1): - for index, parent in enumerate(self.scheduled): - if index != skip_index: - close(parent) - - async def _wait(self, child, index): - try: - result = await child - except Exception as e: - self._finish(child, index, e) - if __debug__: - log.exception(__name__, e) - else: - self._finish(child, index, result) + finalizer = self._finish + scheduled = self.scheduled + finished = self.finished - def _finish(self, child, index, result): + self.callback = task + scheduled.clear() + finished.clear() + + for child in self.children: + if isinstance(child, _type_gen): + child_task = child + else: + child_task = iter(child) + schedule(child_task, None, None, finalizer) + scheduled.append(child_task) + + def exit(self, except_for=None): + for task in self.scheduled: + if task != except_for: + close(task) + + def _finish(self, task, result): if not self.finished: + for index, child_task in enumerate(self.scheduled): + if child_task is task: + child = self.children[index] + break self.finished.append(child) if self.exit_others: - self.exit(index) + self.exit(task) schedule(self.callback, result) def __iter__(self): @@ -284,66 +301,3 @@ class spawn(Syscall): # close() or throw(), kill the children tasks and re-raise self.exit() raise - - -class put(Syscall): - def __init__(self, ch, value=None): - self.ch = ch - self.value = value - - def __call__(self, value): - self.value = value - return self - - def handle(self, task): - self.ch.schedule_put(schedule, task, self.value) - - -class take(Syscall): - def __init__(self, ch): - self.ch = ch - - def __call__(self): - return self - - def handle(self, task): - if self.ch.schedule_take(schedule, task) and self.ch.id is not None: - pause(self.ch, self.ch.id) - - -class chan: - def __init__(self, id=None): - self.id = id - self.putters = [] - self.takers = [] - self.put = put(self) - self.take = take(self) - - def schedule_publish(self, schedule, value): - if self.takers: - for taker in self.takers: - schedule(taker, value) - self.takers.clear() - return True - else: - return False - - def schedule_put(self, schedule, putter, value): - if self.takers: - taker = self.takers.pop(0) - schedule(taker, value) - schedule(putter, value) - return True - else: - self.putters.append((putter, value)) - return False - - def schedule_take(self, schedule, taker): - if self.putters: - putter, value = self.putters.pop(0) - schedule(taker, value) - schedule(putter, value) - return True - else: - self.takers.append(taker) - return False diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index 579e20490..2260e4861 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -31,6 +31,10 @@ SIZE = Display.FONT_SIZE WIDTH = Display.WIDTH HEIGHT = Display.HEIGHT +# viewport margins +VIEWX = const(6) +VIEWY = const(9) + def lerpi(a: int, b: int, t: float) -> int: return int(a + t * (b - a)) @@ -55,43 +59,9 @@ from trezor.ui import style # isort:skip from trezor.ui.style import * # isort:skip # noqa: F401,F403 -def contains(area: tuple, pos: tuple) -> bool: - x, y = pos - ax, ay, aw, ah = area - return ax <= x <= ax + aw and ay <= y <= ay + ah - - -def rotate(pos: tuple) -> tuple: - r = display.orientation() - if r == 0: - return pos - x, y = pos - if r == 90: - return (y, WIDTH - x) - if r == 180: - return (WIDTH - x, HEIGHT - y) - if r == 270: - return (HEIGHT - y, x) - - def pulse(delay: int): - while True: - # normalize sin from interval -1:1 to 0:1 - yield 0.5 + 0.5 * math.sin(utime.ticks_us() / delay) - - -async def alert(count: int = 3): - short_sleep = loop.sleep(20000) - long_sleep = loop.sleep(80000) - current = display.backlight() - for i in range(count * 2): - if i % 2 == 0: - display.backlight(style.BACKLIGHT_MAX) - yield short_sleep - else: - display.backlight(style.BACKLIGHT_NORMAL) - yield long_sleep - display.backlight(current) + # normalize sin from interval -1:1 to 0:1 + return 0.5 + 0.5 * math.sin(utime.ticks_us() / delay) async def click() -> tuple: @@ -107,53 +77,22 @@ async def click() -> tuple: return pos -async def backlight_slide(val: int, delay: int = 35000, step: int = 20): - sleep = loop.sleep(delay) +def backlight_fade(val: int, delay: int = 14000, step: int = 15): + if __debug__: + if utils.DISABLE_FADE: + display.backlight(val) + return current = display.backlight() - for i in range(current, val, -step if current > val else step): - display.backlight(i) - yield sleep - - -def backlight_slide_sync(val: int, delay: int = 35000, step: int = 20): - current = display.backlight() - for i in range(current, val, -step if current > val else step): + if current > val: + step = -step + for i in range(current, val, step): display.backlight(i) utime.sleep_us(delay) -def layout(f): - async def inner(*args, **kwargs): - await backlight_slide(style.BACKLIGHT_DIM) - slide = backlight_slide(style.BACKLIGHT_NORMAL) - try: - layout = f(*args, **kwargs) - workflow.onlayoutstart(layout) - loop.schedule(slide) - display.clear() - return await layout - finally: - loop.close(slide) - workflow.onlayoutclose(layout) - - return inner - - -def layout_no_slide(f): - async def inner(*args, **kwargs): - try: - layout = f(*args, **kwargs) - workflow.onlayoutstart(layout) - return await layout - finally: - workflow.onlayoutclose(layout) - - return inner - - def header( title: str, - icon: bytes = style.ICON_DEFAULT, + icon: str = style.ICON_DEFAULT, fg: int = style.FG, bg: int = style.BG, ifg: int = style.GREEN, @@ -163,10 +102,6 @@ def header( display.text(44, 35, title, BOLD, fg, bg) -VIEWX = const(6) -VIEWY = const(9) - - def grid( i: int, n_x: int = 3, @@ -186,23 +121,90 @@ def grid( return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y) -class Widget: - tainted = True +def in_area(area: tuple, x: int, y: int) -> bool: + ax, ay, aw, ah = area + return ax <= x <= ax + aw and ay <= y <= ay + ah + - def taint(self): - self.tainted = True +# render events +RENDER = const(-255) +REPAINT = const(-256) + + +class Control: + def dispatch(self, event, x, y): + 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_render(self): + pass + + def on_touch_start(self, x, y): + pass - def render(self): + def on_touch_move(self, x, y): pass - def touch(self, event, pos): + def on_touch_end(self, x, y): pass - def __iter__(self): + +_RENDER_DELAY_US = const(10000) # 10 msec + + +class LayoutCancelled(Exception): + pass + + +class Result(Exception): + def __init__(self, value): + self.value = value + + +class Layout(Control): + """ + """ + + async def __iter__(self): + value = None + try: + if workflow.layout_signal.task is not None: + workflow.layout_signal.send(LayoutCancelled()) + workflow.onlayoutstart(self) + while True: + layout_tasks = self.create_tasks() + await loop.spawn(workflow.layout_signal, *layout_tasks) + except Result as result: + value = result.value + finally: + workflow.onlayoutclose(self) + return value + + def create_tasks(self): + return self.handle_input(), self.handle_rendering() + + def handle_input(self): touch = loop.wait(io.TOUCH) - result = None - while result is None: - self.render() - event, *pos = yield touch - result = self.touch(event, pos) - return result + while True: + event, x, y = yield touch + self.dispatch(event, x, y) + self.dispatch(RENDER, 0, 0) + + def handle_rendering(self): + backlight_fade(style.BACKLIGHT_DIM) + display.clear() + self.dispatch(RENDER, 0, 0) + display.refresh() + backlight_fade(style.BACKLIGHT_NORMAL) + sleep = loop.sleep(_RENDER_DELAY_US) + while True: + self.dispatch(RENDER, 0, 0) + yield sleep diff --git a/core/src/trezor/ui/button.py b/core/src/trezor/ui/button.py index 75f5e427d..cf82889b8 100644 --- a/core/src/trezor/ui/button.py +++ b/core/src/trezor/ui/button.py @@ -1,115 +1,213 @@ from micropython import const -from trezor import io, ui -from trezor.ui import Widget, contains, display, rotate +from trezor import ui +from trezor.ui import display, in_area + + +class ButtonDefault: + class normal: + bg_color = ui.BLACKISH + fg_color = ui.FG + text_style = ui.BOLD + border_color = ui.BG + radius = ui.RADIUS + + class active: + bg_color = ui.FG + fg_color = ui.BLACKISH + text_style = ui.BOLD + border_color = ui.FG + radius = ui.RADIUS + + class disabled: + bg_color = ui.BG + fg_color = ui.GREY + text_style = ui.NORMAL + border_color = ui.BG + radius = ui.RADIUS + + +class ButtonMono(ButtonDefault): + class normal(ButtonDefault.normal): + text_style = ui.MONO + + class active(ButtonDefault.active): + text_style = ui.MONO + + class disabled(ButtonDefault.disabled): + text_style = ui.MONO + + +class ButtonMonoDark: + class normal: + bg_color = ui.DARK_BLACK + fg_color = ui.DARK_WHITE + text_style = ui.MONO + border_color = ui.BG + radius = ui.RADIUS + + class active: + bg_color = ui.FG + fg_color = ui.DARK_BLACK + text_style = ui.MONO + border_color = ui.FG + radius = ui.RADIUS + + class disabled: + bg_color = ui.DARK_BLACK + fg_color = ui.GREY + text_style = ui.MONO + border_color = ui.BG + radius = ui.RADIUS + + +class ButtonConfirm(ButtonDefault): + class normal(ButtonDefault.normal): + bg_color = ui.GREEN + + class active(ButtonDefault.active): + fg_color = ui.GREEN + + +class ButtonCancel(ButtonDefault): + class normal(ButtonDefault.normal): + bg_color = ui.RED + + class active(ButtonDefault.active): + fg_color = ui.RED + + +class ButtonClear(ButtonDefault): + class normal(ButtonDefault.normal): + bg_color = ui.ORANGE + + class active(ButtonDefault.active): + fg_color = ui.ORANGE + + +class ButtonMonoConfirm(ButtonDefault): + class normal(ButtonDefault.normal): + text_style = ui.MONO + bg_color = ui.GREEN + + class active(ButtonDefault.active): + text_style = ui.MONO + fg_color = ui.GREEN + + class disabled(ButtonDefault.disabled): + text_style = ui.MONO -# button events -BTN_CLICKED = const(1) # button states -BTN_INITIAL = const(0) -BTN_DISABLED = const(1) -BTN_FOCUSED = const(2) -BTN_ACTIVE = const(3) +_INITIAL = const(0) +_PRESSED = const(1) +_RELEASED = const(2) +_DISABLED = const(3) -# constants -ICON = const(16) # icon size in pixels -BORDER = const(4) # border size in pixels +# button constants +_ICON = const(16) # icon size in pixels +_BORDER = const(4) # border size in pixels -class Button(Widget): - def __init__(self, area: tuple, content: str, style: dict = ui.BTN_KEY): +class Button(ui.Control): + def __init__(self, area, content, style=ButtonDefault): self.area = area self.content = content - self.normal_style = style["normal"] or ui.BTN_KEY["normal"] - self.active_style = style["active"] or ui.BTN_KEY["active"] - self.disabled_style = style["disabled"] or ui.BTN_KEY["disabled"] - self.state = BTN_INITIAL + self.normal_style = style.normal + self.active_style = style.active + self.disabled_style = style.disabled + self.state = _INITIAL + self.repaint = True def enable(self): - if self.state == BTN_DISABLED: - self.state = BTN_INITIAL - self.tainted = True + if self.state is not _INITIAL: + self.state = _INITIAL + self.repaint = True def disable(self): - if self.state != BTN_DISABLED: - self.state = BTN_DISABLED - self.tainted = True - - def is_enabled(self): - return self.state != BTN_DISABLED - - def render(self): - if not self.tainted: - return - state = self.state - if state == BTN_DISABLED: - s = self.disabled_style - elif state == BTN_ACTIVE: - s = self.active_style - else: - s = self.normal_style - ax, ay, aw, ah = self.area - self.render_background(s, ax, ay, aw, ah) - self.render_content(s, ax, ay, aw, ah) - self.tainted = False + if self.state is not _DISABLED: + self.state = _DISABLED + self.repaint = True + + def on_render(self): + if self.repaint: + if self.state is _DISABLED: + s = self.disabled_style + elif self.state is _PRESSED: + s = self.active_style + else: + s = self.normal_style + ax, ay, aw, ah = self.area + self.render_background(s, ax, ay, aw, ah) + self.render_content(s, ax, ay, aw, ah) + self.repaint = False def render_background(self, s, ax, ay, aw, ah): - radius = s["radius"] - bg_color = s["bg-color"] - border_color = s["border-color"] - if border_color != bg_color: + radius = s.radius + bg_color = s.bg_color + border_color = s.border_color + if border_color == bg_color: + # we don't need to render the border + display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius) + else: # render border and background on top of it display.bar_radius(ax, ay, aw, ah, border_color, ui.BG, radius) display.bar_radius( - ax + BORDER, - ay + BORDER, - aw - BORDER * 2, - ah - BORDER * 2, + ax + _BORDER, + ay + _BORDER, + aw - _BORDER * 2, + ah - _BORDER * 2, bg_color, border_color, radius, ) - else: - # render only the background - display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius) def render_content(self, s, ax, ay, aw, ah): - c = self.content tx = ax + aw // 2 ty = ay + ah // 2 + 8 - if isinstance(c, str): - display.text_center( - tx, ty, c, s["text-style"], s["fg-color"], s["bg-color"] - ) - else: - display.icon(tx - ICON // 2, ty - ICON, c, s["fg-color"], s["bg-color"]) - - def touch(self, event, pos): - pos = rotate(pos) - - state = self.state - if state == BTN_DISABLED: + t = self.content + if isinstance(t, str): + display.text_center(tx, ty, t, s.text_style, s.fg_color, s.bg_color) + elif isinstance(t, bytes): + display.icon(tx - _ICON // 2, ty - _ICON, t, s.fg_color, s.bg_color) + + def on_touch_start(self, x, y): + if self.state is _DISABLED: return + if in_area(self.area, x, y): + self.state = _PRESSED + self.repaint = True + self.on_press_start() - if event == io.TOUCH_START: - if contains(self.area, pos): - self.state = BTN_ACTIVE - self.tainted = True + def on_touch_move(self, x, y): + if self.state is _DISABLED: + return + if in_area(self.area, x, y): + if self.state is _RELEASED: + self.state = _PRESSED + self.repaint = True + self.on_press_start() + else: + if self.state is _PRESSED: + self.state = _RELEASED + self.repaint = True + self.on_press_end() - elif event == io.TOUCH_MOVE: - if contains(self.area, pos): - if state == BTN_FOCUSED: - self.state = BTN_ACTIVE - self.tainted = True - else: - if state == BTN_ACTIVE: - self.state = BTN_FOCUSED - self.tainted = True - - elif event == io.TOUCH_END: - if state != BTN_INITIAL: - self.state = BTN_INITIAL - self.tainted = True - if state == BTN_ACTIVE and contains(self.area, pos): - return BTN_CLICKED + def on_touch_end(self, x, y): + state = self.state + if state is not _INITIAL and state is not _DISABLED: + self.state = _INITIAL + self.repaint = True + if in_area(self.area, x, y): + if state is _PRESSED: + self.on_press_end() + self.on_click() + + def on_press_start(self): + pass + + def on_press_end(self): + pass + + def on_click(self): + pass diff --git a/core/src/trezor/ui/confirm.py b/core/src/trezor/ui/confirm.py index 32289df7d..1be20caa8 100644 --- a/core/src/trezor/ui/confirm.py +++ b/core/src/trezor/ui/confirm.py @@ -1,115 +1,106 @@ -from micropython import const +from trezor import res, ui +from trezor.ui.button import Button, ButtonCancel, ButtonConfirm +from trezor.ui.loader import Loader, LoaderDefault -from trezor import loop, res, ui -from trezor.ui import Widget -from trezor.ui.button import BTN_ACTIVE, BTN_CLICKED, Button -from trezor.ui.loader import Loader +CONFIRMED = object() +CANCELLED = object() -if __debug__: - from apps.debug import confirm_signal -CONFIRMED = const(1) -CANCELLED = const(2) -DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) -DEFAULT_CANCEL = res.load(ui.ICON_CANCEL) +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 - -class ConfirmDialog(Widget): def __init__( self, content, confirm=DEFAULT_CONFIRM, + confirm_style=DEFAULT_CONFIRM_STYLE, cancel=DEFAULT_CANCEL, - confirm_style=ui.BTN_CONFIRM, - cancel_style=ui.BTN_CANCEL, + cancel_style=DEFAULT_CANCEL_STYLE, ): self.content = content + + if confirm is not None: + if cancel is None: + area = ui.grid(4, n_x=1) + else: + area = ui.grid(9, n_x=2) + self.confirm = Button(area, confirm, confirm_style) + self.confirm.on_click = self.on_confirm + else: + self.confirm = None + if cancel is not None: - self.confirm = Button(ui.grid(9, n_x=2), confirm, style=confirm_style) - self.cancel = Button(ui.grid(8, n_x=2), cancel, style=cancel_style) + if confirm is None: + area = ui.grid(4, n_x=1) + else: + area = ui.grid(8, n_x=2) + self.cancel = Button(area, cancel, cancel_style) + self.cancel.on_click = self.on_cancel else: - self.confirm = Button(ui.grid(4, n_x=1), confirm, style=confirm_style) self.cancel = None - def render(self): - self.confirm.render() + def dispatch(self, 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.render() + self.cancel.dispatch(event, x, y) - def touch(self, event, pos): - if self.confirm.touch(event, pos) == BTN_CLICKED: - return CONFIRMED - if self.cancel is not None: - if self.cancel.touch(event, pos) == BTN_CLICKED: - return CANCELLED - - async def __iter__(self): - if __debug__: - return await loop.spawn(super().__iter__(), self.content, confirm_signal) - else: - return await loop.spawn(super().__iter__(), self.content) + def on_confirm(self): + raise ui.Result(CONFIRMED) + def on_cancel(self): + raise ui.Result(CANCELLED) -_STARTED = const(-1) -_STOPPED = const(-2) +class HoldToConfirm(ui.Layout): + DEFAULT_CONFIRM = "Hold To Confirm" + DEFAULT_CONFIRM_STYLE = ButtonConfirm + DEFAULT_LOADER_STYLE = LoaderDefault -class HoldToConfirmDialog(Widget): def __init__( self, content, - hold="Hold to confirm", - button_style=ui.BTN_CONFIRM, - loader_style=ui.LDR_DEFAULT, + confirm=DEFAULT_CONFIRM, + confirm_style=DEFAULT_CONFIRM_STYLE, + loader_style=DEFAULT_LOADER_STYLE, ): self.content = content - self.button = Button(ui.grid(4, n_x=1), hold, style=button_style) - self.loader = Loader(style=loader_style) - - if content.__class__.__iter__ is not Widget.__iter__: - raise TypeError( - "HoldToConfirmDialog does not support widgets with custom event loop" - ) - - def taint(self): - super().taint() - self.button.taint() - self.content.taint() - - def render(self): - self.button.render() - if not self.loader.is_active(): - self.content.render() - - def touch(self, event, pos): - button = self.button - was_active = button.state == BTN_ACTIVE - button.touch(event, pos) - is_active = button.state == BTN_ACTIVE - if is_active and not was_active: - ui.display.clear() - self.loader.start() - return _STARTED - if was_active and not is_active: - if self.loader.stop(): - return CONFIRMED - else: - return _STOPPED - - async def __iter__(self): - result = None - while result is None or result < 0: # _STARTED or _STOPPED - if self.loader.is_active(): - if __debug__: - result = await loop.spawn( - self.loader, super().__iter__(), confirm_signal - ) - else: - result = await loop.spawn(self.loader, super().__iter__()) - else: - self.content.taint() - if __debug__: - result = await loop.spawn(super().__iter__(), confirm_signal) - else: - result = await super().__iter__() - return result + + self.loader = Loader(loader_style) + self.loader.on_start = self._on_loader_start + + self.button = Button(ui.grid(4, n_x=1), confirm, confirm_style) + self.button.on_press_start = self._on_press_start + self.button.on_press_end = self._on_press_end + self.button.on_click = self._on_click + + def _on_press_start(self): + self.loader.start() + + def _on_press_end(self): + self.loader.stop() + + def _on_loader_start(self): + # 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): + if self.loader.elapsed_ms() >= self.loader.target_ms: + self.on_confirm() + + def dispatch(self, event, x, y): + 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): + raise ui.Result(CONFIRMED) diff --git a/core/src/trezor/ui/container.py b/core/src/trezor/ui/container.py index 64c0ea860..98da6bdfd 100644 --- a/core/src/trezor/ui/container.py +++ b/core/src/trezor/ui/container.py @@ -1,21 +1,10 @@ -from trezor.ui import Widget +from trezor import ui -class Container(Widget): +class Container(ui.Control): def __init__(self, *children): self.children = children - def taint(self): - super().taint() + def dispatch(self, event, x, y): for child in self.children: - child.taint() - - def render(self): - for child in self.children: - child.render() - - def touch(self, event, pos): - for child in self.children: - result = child.touch(event, pos) - if result is not None: - return result + child.dispatch(event, x, y) diff --git a/core/src/trezor/ui/entry_select.py b/core/src/trezor/ui/entry_select.py deleted file mode 100644 index 7d1e4a5b6..000000000 --- a/core/src/trezor/ui/entry_select.py +++ /dev/null @@ -1,33 +0,0 @@ -from micropython import const - -from trezor import loop, ui -from trezor.ui import Widget -from trezor.ui.button import BTN_CLICKED, Button - -DEVICE = const(0) -HOST = const(1) - - -class EntrySelector(Widget): - def __init__(self, content): - self.content = content - self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device") - self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host") - - def taint(self): - super().taint() - self.device.taint() - self.host.taint() - - def render(self): - self.device.render() - self.host.render() - - def touch(self, event, pos): - if self.device.touch(event, pos) == BTN_CLICKED: - return DEVICE - if self.host.touch(event, pos) == BTN_CLICKED: - return HOST - - async def __iter__(self): - return await loop.spawn(super().__iter__(), self.content) diff --git a/core/src/trezor/ui/loader.py b/core/src/trezor/ui/loader.py index 9e81749fd..1e0fd7036 100644 --- a/core/src/trezor/ui/loader.py +++ b/core/src/trezor/ui/loader.py @@ -1,36 +1,57 @@ import utime from micropython import const -from trezor import loop, res, ui +from trezor import res, ui +from trezor.ui import display + + +class LoaderDefault: + class normal: + bg_color = ui.BG + fg_color = ui.GREEN + icon = None + icon_fg_color = None + + class active: + bg_color = ui.BG + fg_color = ui.GREEN + icon = ui.ICON_CHECK + icon_fg_color = ui.WHITE + + +class LoaderDanger: + class normal(LoaderDefault.normal): + fg_color = ui.RED + + class active(LoaderDefault.active): + fg_color = ui.RED + _TARGET_MS = const(1000) -_SHRINK_BY = const(2) -class Loader(ui.Widget): - def __init__(self, style=ui.LDR_DEFAULT): +class Loader(ui.Control): + def __init__(self, style=LoaderDefault): + self.normal_style = style.normal + self.active_style = style.active self.target_ms = _TARGET_MS - self.normal_style = style["normal"] or ui.LDR_DEFAULT["normal"] - self.active_style = style["active"] or ui.LDR_DEFAULT["active"] self.start_ms = None self.stop_ms = None def start(self): self.start_ms = utime.ticks_ms() self.stop_ms = None + self.on_start() def stop(self): - if self.start_ms is not None and self.stop_ms is None: - diff_ms = utime.ticks_ms() - self.start_ms - else: - diff_ms = 0 self.stop_ms = utime.ticks_ms() - return diff_ms >= self.target_ms - def is_active(self): - return self.start_ms is not None + def elapsed_ms(self): + if self.start_ms is None: + return 0 + return utime.ticks_ms() - self.start_ms - def render(self): + def on_render(self): target = self.target_ms start = self.start_ms stop = self.stop_ms @@ -38,35 +59,24 @@ class Loader(ui.Widget): if stop is None: r = min(now - start, target) else: - r = max(stop - start + (stop - now) * _SHRINK_BY, 0) - if r == 0: - self.start_ms = None - self.stop_ms = None + r = max(stop - start + (stop - now) * 2, 0) if r == target: s = self.active_style else: s = self.normal_style - if s["icon"] is None: - ui.display.loader(r, False, -24, s["fg-color"], s["bg-color"]) - elif s["icon-fg-color"] is None: - ui.display.loader( - r, False, -24, s["fg-color"], s["bg-color"], res.load(s["icon"]) - ) + + Y = const(-24) + + if s.icon is None: + display.loader(r, False, Y, s.fg_color, s.bg_color) else: - ui.display.loader( - r, - False, - -24, - s["fg-color"], - s["bg-color"], - res.load(s["icon"]), - s["icon-fg-color"], + display.loader( + r, False, Y, s.fg_color, s.bg_color, res.load(s.icon), s.icon_fg_color ) + if r == 0: + self.start_ms = None + self.stop_ms = None + self.on_start() - def __iter__(self): - sleep = loop.sleep(1000000 // 30) # 30 fps - ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear - while self.is_active(): - self.render() - yield sleep - ui.display.bar(0, 32, ui.WIDTH, ui.HEIGHT - 83, ui.BG) # clear + def on_start(self): + pass diff --git a/core/src/trezor/ui/mnemonic.py b/core/src/trezor/ui/mnemonic.py index 22cfe2d69..94c51f44d 100644 --- a/core/src/trezor/ui/mnemonic.py +++ b/core/src/trezor/ui/mnemonic.py @@ -1,16 +1,7 @@ from trezor import io, loop, res, ui from trezor.crypto import bip39 from trezor.ui import display -from trezor.ui.button import BTN_CLICKED, ICON, Button - -if __debug__: - from apps.debug import input_signal - -MNEMONIC_KEYS = ("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz") - - -def key_buttons(keys): - return [Button(ui.grid(i + 3, n_y=4), k) for i, k in enumerate(keys)] +from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm def compute_mask(text: str) -> int: @@ -23,42 +14,59 @@ def compute_mask(text: str) -> int: return mask -class Input(Button): - def __init__(self, area: tuple, content: str = "", word: str = ""): +class KeyButton(Button): + def __init__(self, area, content, keyboard): + self.keyboard = keyboard + super().__init__(area, content) + + def on_click(self): + self.keyboard.on_key_click(self) + + +class InputButton(Button): + def __init__(self, area, content, word): super().__init__(area, content) self.word = word - self.icon = None self.pending = False + self.icon = None + self.disable() - def edit(self, content: str, word: str, pending: bool): + def edit(self, content, word, pending): self.word = word self.content = content self.pending = pending - self.taint() - if content == word: # confirm button - self.enable() - self.normal_style = ui.BTN_KEY_CONFIRM["normal"] - self.active_style = ui.BTN_KEY_CONFIRM["active"] - self.icon = ui.ICON_CONFIRM - elif word: # auto-complete button - self.enable() - self.normal_style = ui.BTN_KEY["normal"] - self.active_style = ui.BTN_KEY["active"] - self.icon = ui.ICON_CLICK + self.repaint = True + if word: + if content == word: # confirm button + self.enable() + self.normal_style = ButtonMonoConfirm.normal + self.active_style = ButtonMonoConfirm.active + self.icon = ui.ICON_CONFIRM + else: # auto-complete button + self.enable() + self.normal_style = ButtonMono.normal + self.active_style = ButtonMono.active + self.icon = ui.ICON_CLICK else: # disabled button + self.disabled_style = ButtonMono.disabled self.disable() self.icon = None def render_content(self, s, ax, ay, aw, ah): - text_style = s["text-style"] - fg_color = s["fg-color"] - bg_color = s["bg-color"] + text_style = s.text_style + fg_color = s.fg_color + bg_color = s.bg_color p = self.pending # should we draw the pending marker? t = self.content # input content w = self.word[len(t) :] # suggested word i = self.icon # rendered icon + if not t: + # render prompt + display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) + return + tx = ax + 24 # x-offset of the content ty = ay + ah // 2 + 8 # y-offset of the content @@ -73,79 +81,103 @@ class Input(Button): display.bar(px, ty + 2, pw + 1, 3, fg_color) if i: # icon - ix = ax + aw - ICON * 2 - iy = ty - ICON + ix = ax + aw - 16 * 2 + iy = ty - 16 display.icon(ix, iy, res.load(i), fg_color, bg_color) -class MnemonicKeyboard(ui.Widget): - def __init__(self, prompt: str = ""): +class Prompt(ui.Control): + def __init__(self, prompt): self.prompt = prompt - self.input = Input(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "") - self.back = Button( - ui.grid(0, n_x=4, n_y=4), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR - ) - self.keys = key_buttons(MNEMONIC_KEYS) - self.pbutton = None # pending key button - self.pindex = 0 # index of current pending char in pbutton - - def taint(self): - super().taint() - self.input.taint() - self.back.taint() - for btn in self.keys: - btn.taint() + self.repaint = True - def render(self): - if self.input.content: - # content button and backspace - self.input.render() - self.back.render() - else: - # prompt + def on_render(self): + if self.repaint: display.bar(0, 8, ui.WIDTH, 60, ui.BG) display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) - # key buttons + self.repaint = False + + +class MnemonicKeyboard(ui.Layout): + def __init__(self, prompt): + self.prompt = Prompt(prompt) + + icon_back = res.load(ui.ICON_BACK) + self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear) + self.back.on_click = self.on_back_click + + self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "") + self.input.on_click = self.on_input_click + + self.keys = [ + KeyButton(ui.grid(i + 3, n_y=4), k, self) + for i, k in enumerate( + ("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz") + ) + ] + self.pending_button = None + self.pending_index = 0 + + def dispatch(self, event: int, x: int, y: int): for btn in self.keys: - btn.render() + btn.dispatch(event, x, y) + if self.input.content: + self.input.dispatch(event, x, y) + self.back.dispatch(event, x, y) + else: + self.prompt.dispatch(event, x, y) + + def on_back_click(self): + # Backspace was clicked, let's delete the last character of input. + self.edit(self.input.content[:-1]) - def touch(self, event, pos): + def on_input_click(self): + # Input button was clicked. If the content matches the suggested word, + # let's confirm it, otherwise just auto-complete. content = self.input.content word = self.input.word + if word and word == content: + self.edit("") + self.on_confirm(word) + else: + self.edit(word) + + def on_key_click(self, btn: Button): + # Key button was clicked. If this button is pending, let's cycle the + # pending character in input. If not, let's just append the first + # character. + if self.pending_button is btn: + index = (self.pending_index + 1) % len(btn.content) + content = self.input.content[:-1] + btn.content[index] + else: + index = 0 + content = self.input.content + btn.content[0] + self.edit(content, btn, index) + + def on_timeout(self): + # Timeout occurred. If we can auto-complete current input, let's just + # reset the pending marker. If not, input is invalid, let's backspace + # the last character. + if self.input.word: + self.edit(self.input.content) + else: + self.edit(self.input.content[:-1]) - if self.back.touch(event, pos) == BTN_CLICKED: - # backspace, delete the last character of input - self.edit(content[:-1]) - return + def on_confirm(self, word): + # Word was confirmed by the user. + raise ui.Result(word) - if self.input.touch(event, pos) == BTN_CLICKED: - # input press, either auto-complete or confirm - if word and content == word: - self.edit("") - return content - else: - self.edit(word) - return + def edit(self, content: str, button: KeyButton = None, index: int = 0): + self.pending_button = button + self.pending_index = index - for btn in self.keys: - if btn.touch(event, pos) == BTN_CLICKED: - # key press, add new char to input or cycle the pending button - if self.pbutton is btn: - index = (self.pindex + 1) % len(btn.content) - content = content[:-1] + btn.content[index] - else: - index = 0 - content += btn.content[0] - self.edit(content, btn, index) - return - - def edit(self, content, button=None, index=0): + # find the completions + pending = button is not None word = bip39.find_word(content) or "" mask = bip39.complete_word(content) - self.pbutton = button - self.pindex = index - self.input.edit(content, word, button is not None) + # modify the input state + self.input.edit(content, word, pending) # enable or disable key buttons for btn in self.keys: @@ -154,37 +186,25 @@ class MnemonicKeyboard(ui.Widget): else: btn.disable() - async def __iter__(self): - if __debug__: - return await loop.spawn(self.edit_loop(), input_signal) - else: - return await self.edit_loop() + # invalidate the prompt if we display it next frame + if not self.input.content: + self.prompt.repaint = True - async def edit_loop(self): - timeout = loop.sleep(1000 * 1000 * 1) + async def handle_input(self): touch = loop.wait(io.TOUCH) - wait_timeout = loop.spawn(touch, timeout) - wait_touch = loop.spawn(touch) - content = None - - self.back.taint() - self.input.taint() + timeout = loop.sleep(1000 * 1000 * 1) + spawn_touch = loop.spawn(touch) + spawn_timeout = loop.spawn(touch, timeout) - while content is None: - self.render() - if self.pbutton is not None: - wait = wait_timeout + while True: + if self.pending_button is not None: + spawn = spawn_timeout else: - wait = wait_touch - result = await wait - if touch in wait.finished: - event, *pos = result - content = self.touch(event, pos) + spawn = spawn_touch + result = await spawn + + if touch in spawn.finished: + event, x, y = result + self.dispatch(event, x, y) else: - if self.input.word: - # just reset the pending state - self.edit(self.input.content) - else: - # invalid character, backspace it - self.edit(self.input.content[:-1]) - return content + self.on_timeout() diff --git a/core/src/trezor/ui/num_pad.py b/core/src/trezor/ui/num_pad.py index 795a5f5f7..7c6bcbb81 100644 --- a/core/src/trezor/ui/num_pad.py +++ b/core/src/trezor/ui/num_pad.py @@ -1,17 +1,14 @@ -from trezor import res, ui -from trezor.ui import display -from trezor.ui.button import BTN_CLICKED, Button - -ITEMS_PER_PAGE = 10 -PLUS_BUTTON_POSITION = 11 -BACK_BUTTON_POSITION = 9 +from micropython import const +from trezor import res, ui +from trezor.ui.button import Button, ButtonMono, ButtonMonoDark -def digit_area(i): - return ui.grid(i + 3) # skip the first line +_ITEMS_PER_PAGE = const(10) +_BACK_BUTTON_POSITION = const(9) +_PLUS_BUTTON_POSITION = const(11) -class NumPad(ui.Widget): +class NumPad(ui.Layout): def __init__(self, label: str, start: int, end: int): """ Generates a numpad with numbers from `start` to `end` excluding. @@ -20,50 +17,63 @@ class NumPad(ui.Widget): self.start = start self.end = end self.page = 0 + self.buttons = generate_buttons(self.start, self.end, self.page, self) - self._generate_buttons() - - def render(self): - for btn in self.buttons: - btn.render() - - # header label - display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG) - - def touch(self, event, pos): - for btn in self.buttons: - if btn.touch(event, pos) == BTN_CLICKED: - if "+" in btn.content: - self.page += 1 - self._generate_buttons() - elif isinstance(btn.content, bytes): - self.page -= 1 - self._generate_buttons() - else: - return btn.content - - def _generate_buttons(self): - display.clear() # we need to clear old buttons - start = self.start + (ITEMS_PER_PAGE + 1) * self.page - self.page - end = min(self.end, (ITEMS_PER_PAGE + 1) * (self.page + 1) - self.page) - - digits = list(range(start, end)) - self.buttons = [Button(digit_area(i), str(d)) for i, d in enumerate(digits)] - if len(digits) == ITEMS_PER_PAGE: - more = Button( - digit_area(PLUS_BUTTON_POSITION), str(end) + "+", style=ui.BTN_KEY_DARK - ) - self.buttons.append(more) - # move the tenth button to its proper place and make place for the back button - self.buttons[BACK_BUTTON_POSITION].area = digit_area( - BACK_BUTTON_POSITION + 1 + def dispatch(self, event, x, y): + for button in self.buttons: + button.dispatch(event, x, y) + if event is ui.RENDER: + # render header label + ui.display.text_center( + ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG ) - back = Button( - digit_area(BACK_BUTTON_POSITION), - res.load(ui.ICON_BACK), - style=ui.BTN_KEY_DARK, - ) - if self.page == 0: - back.disable() - self.buttons.append(back) + def on_back(self): + self.page -= 1 + self.buttons = generate_buttons(self.start, self.end, self.page, self) + ui.display.clear() # we need to clear old buttons + + def on_plus(self): + self.page += 1 + self.buttons = generate_buttons(self.start, self.end, self.page, self) + ui.display.clear() # we need to clear old buttons + + def on_select(self, number): + raise ui.Result(number) + + +class NumButton(Button): + def __init__(self, index, digit, pad): + self.pad = pad + area = ui.grid(index + 3) # skip the first line + super().__init__(area, str(digit), ButtonMono) + + def on_click(self): + self.pad.on_select(int(self.content)) + + +def generate_buttons(start, end, page, pad): + start = start + (_ITEMS_PER_PAGE + 1) * page - page + end = min(end, (_ITEMS_PER_PAGE + 1) * (page + 1) - page) + digits = list(range(start, end)) + + buttons = [NumButton(i, d, pad) for i, d in enumerate(digits)] + + area = ui.grid(_PLUS_BUTTON_POSITION + 3) + plus = Button(area, str(end) + "+", ButtonMonoDark) + plus.on_click = pad.on_plus + + area = ui.grid(_BACK_BUTTON_POSITION + 3) + back = Button(area, res.load(ui.ICON_BACK), ButtonMonoDark) + back.on_click = pad.on_back + + if len(digits) == _ITEMS_PER_PAGE: + # move the tenth button to its proper place and make place for the back button + buttons[-1].area = ui.grid(_PLUS_BUTTON_POSITION - 1 + 3) + buttons.append(plus) + + if page == 0: + back.disable() + buttons.append(back) + + return buttons diff --git a/core/src/trezor/ui/passphrase.py b/core/src/trezor/ui/passphrase.py index 4d38f6137..b4b308763 100644 --- a/core/src/trezor/ui/passphrase.py +++ b/core/src/trezor/ui/passphrase.py @@ -1,8 +1,9 @@ from micropython import const from trezor import io, loop, res, ui +from trezor.messages import PassphraseSourceType from trezor.ui import display -from trezor.ui.button import BTN_CLICKED, Button +from trezor.ui.button import Button, ButtonClear, ButtonConfirm from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe SPACE = res.load(ui.ICON_SPACE) @@ -21,45 +22,60 @@ def digit_area(i): return ui.grid(i + 3) # skip the first line -def key_buttons(keys): - return [Button(digit_area(i), k) for i, k in enumerate(keys)] - - def render_scrollbar(page): - bbox = const(240) - size = const(8) + BBOX = const(240) + SIZE = const(8) + pages = len(KEYBOARD_KEYS) + padding = 12 - page_count = len(KEYBOARD_KEYS) + if pages * padding > BBOX: + padding = BBOX // pages + + x = (BBOX // 2) - (pages // 2) * padding + Y = const(44) + + for i in range(0, pages): + if i == page: + fg = ui.FG + else: + fg = ui.DARK_GREY + ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2) - if page_count * padding > bbox: - padding = bbox // page_count - x = (bbox // 2) - (page_count // 2) * padding - y = 44 +class KeyButton(Button): + def __init__(self, area, content, keyboard): + self.keyboard = keyboard + super().__init__(area, content) + + def on_click(self): + self.keyboard.on_key_click(self) + + def get_text_content(self): + if self.content is SPACE: + return " " + else: + return self.content + - for i in range(0, page_count): - if i != page: - ui.display.bar_radius( - x + i * padding, y, size, size, ui.DARK_GREY, ui.BG, size // 2 - ) - ui.display.bar_radius(x + page * padding, y, size, size, ui.FG, ui.BG, size // 2) +def key_buttons(keys, keyboard): + return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)] class Input(Button): - def __init__(self, area: tuple, content: str = ""): + def __init__(self, area, content): super().__init__(area, content) self.pending = False self.disable() - def edit(self, content: str, pending: bool): + def edit(self, content, pending): self.content = content self.pending = pending - self.taint() + self.repaint = True def render_content(self, s, ax, ay, aw, ah): - text_style = s["text-style"] - fg_color = s["fg-color"] - bg_color = s["bg-color"] + text_style = s.text_style + fg_color = s.fg_color + bg_color = s.bg_color p = self.pending # should we draw the pending marker? t = self.content # input content @@ -76,142 +92,167 @@ class Input(Button): if p: # pending marker pw = display.text_width(t[-1:], text_style) - display.bar(tx + width - pw, ty + 2, pw + 1, 3, fg_color) + px = tx + width - pw + display.bar(px, ty + 2, pw + 1, 3, fg_color) else: # cursor - display.bar(tx + width + 1, ty - 18, 2, 22, fg_color) + cx = tx + width + 1 + display.bar(cx, ty - 18, 2, 22, fg_color) + + def on_click(self): + pass -class Prompt(ui.Widget): +class Prompt(ui.Control): def __init__(self, text): self.text = text + self.repaint = True - def render(self): - if self.tainted: + def on_render(self): + if self.repaint: display.bar(0, 0, ui.WIDTH, 48, ui.BG) display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG) - self.tainted = False + self.repaint = False -CANCELLED = const(0) +CANCELLED = object() -class PassphraseKeyboard(ui.Widget): - def __init__(self, prompt, page=1): +class PassphraseKeyboard(ui.Layout): + def __init__(self, prompt, max_length, page=1): self.prompt = Prompt(prompt) + self.max_length = max_length self.page = page + self.input = Input(ui.grid(0, n_x=1, n_y=6), "") - self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), style=ui.BTN_CLEAR) - self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), style=ui.BTN_CONFIRM) - self.keys = key_buttons(KEYBOARD_KEYS[self.page]) - self.pbutton = None # pending key button - self.pindex = 0 # index of current pending char in pbutton - - def taint(self): - super().taint() - self.prompt.taint() - self.input.taint() - self.back.taint() - self.done.taint() - for btn in self.keys: - btn.taint() - def render(self): - # passphrase or prompt + self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear) + self.back.on_click = self.on_back_click + self.back.disable() + + self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm) + self.done.on_click = self.on_confirm + + self.keys = key_buttons(KEYBOARD_KEYS[self.page], self) + self.pending_button = None + self.pending_index = 0 + + def dispatch(self, event, x, y): if self.input.content: - self.input.render() + self.input.dispatch(event, x, y) else: - self.prompt.render() - render_scrollbar(self.page) - # buttons - self.back.render() - self.done.render() + self.prompt.dispatch(event, x, y) + self.back.dispatch(event, x, y) + self.done.dispatch(event, x, y) for btn in self.keys: - btn.render() + btn.dispatch(event, x, y) + + if event == ui.RENDER: + render_scrollbar(self.page) - def touch(self, event, pos): + def on_back_click(self): + # Backspace was clicked. If we have any content in the input, let's delete + # the last character. Otherwise cancel. content = self.input.content - if self.back.touch(event, pos) == BTN_CLICKED: - if content: - # backspace, delete the last character of input - self.edit(content[:-1]) - return - else: - # cancel - return CANCELLED - if self.done.touch(event, pos) == BTN_CLICKED: - # confirm button, return the content - return content - for btn in self.keys: - if btn.touch(event, pos) == BTN_CLICKED: - if isinstance(btn.content[0], str): - # key press, add new char to input or cycle the pending button - if self.pbutton is btn: - index = (self.pindex + 1) % len(btn.content) - content = content[:-1] + btn.content[index] - else: - index = 0 - content += btn.content[0] - else: - index = 0 - content += " " - - self.edit(content, btn, index) - return - - def edit(self, content, button=None, index=0): - if button and len(button.content) == 1: - # one-letter buttons are never pending - button = None + if content: + self.edit(content[:-1]) + else: + self.on_cancel() + + def on_key_click(self, button: KeyButton): + # Key button was clicked. If this button is pending, let's cycle the + # pending character in input. If not, let's just append the first + # character. + button_text = button.get_text_content() + if self.pending_button is button: + index = (self.pending_index + 1) % len(button_text) + prefix = self.input.content[:-1] + else: index = 0 - self.pbutton = button - self.pindex = index - self.input.edit(content, button is not None) + prefix = self.input.content + if len(button_text) > 1: + self.edit(prefix + button_text[index], button, index) + else: + self.edit(prefix + button_text[index]) + + def on_timeout(self): + # Timeout occurred, let's just reset the pending marker. + self.edit(self.input.content) + + def edit(self, content: str, button: Button = None, index: int = 0): + if len(content) > self.max_length: + return + + self.pending_button = button + self.pending_index = index + + # modify the input state + pending = button is not None + self.input.edit(content, pending) + if content: self.back.enable() else: self.back.disable() - self.prompt.taint() + self.prompt.repaint = True - async def __iter__(self): - self.edit(self.input.content) # init button state - while True: - change = self.change_page() - enter = self.enter_text() - wait = loop.spawn(change, enter) - result = await wait - if enter in wait.finished: - return result - - @ui.layout - async def enter_text(self): - timeout = loop.sleep(1000 * 1000 * 1) + async def handle_input(self): touch = loop.wait(io.TOUCH) - wait_timeout = loop.spawn(touch, timeout) - wait_touch = loop.spawn(touch) - content = None - while content is None: - self.render() - if self.pbutton is not None: - wait = wait_timeout + timeout = loop.sleep(1000 * 1000 * 1) + spawn_touch = loop.spawn(touch) + spawn_timeout = loop.spawn(touch, timeout) + + while True: + if self.pending_button is not None: + spawn = spawn_timeout else: - wait = wait_touch - result = await wait - if touch in wait.finished: - event, *pos = result - content = self.touch(event, pos) + spawn = spawn_touch + result = await spawn + + if touch in spawn.finished: + event, x, y = result + self.dispatch(event, x, y) else: - # disable the pending buttons - self.edit(self.input.content) - return content + self.on_timeout() - async def change_page(self): - swipe = await Swipe(directions=SWIPE_HORIZONTAL) + async def handle_paging(self): + swipe = await Swipe(SWIPE_HORIZONTAL) if swipe == SWIPE_LEFT: self.page = (self.page + 1) % len(KEYBOARD_KEYS) else: self.page = (self.page - 1) % len(KEYBOARD_KEYS) - self.keys = key_buttons(KEYBOARD_KEYS[self.page]) - self.back.taint() - self.done.taint() - self.input.taint() - self.prompt.taint() + self.keys = key_buttons(KEYBOARD_KEYS[self.page], self) + self.back.repaint = True + self.done.repaint = True + self.input.repaint = True + self.prompt.repaint = True + + def on_cancel(self): + raise ui.Result(CANCELLED) + + def on_confirm(self): + raise ui.Result(self.input.content) + + def create_tasks(self): + return self.handle_input(), self.handle_rendering(), self.handle_paging() + + +class PassphraseSource(ui.Layout): + def __init__(self, content): + self.content = content + + self.device = Button(ui.grid(8, n_y=4, n_x=4, cells_x=4), "Device") + self.device.on_click = self.on_device + + self.host = Button(ui.grid(12, n_y=4, n_x=4, cells_x=4), "Host") + self.host.on_click = self.on_host + + def dispatch(self, event, x, y): + self.content.dispatch(event, x, y) + self.device.dispatch(event, x, y) + self.host.dispatch(event, x, y) + + def on_device(self): + raise ui.Result(PassphraseSourceType.DEVICE) + + def on_host(self): + raise ui.Result(PassphraseSourceType.HOST) diff --git a/core/src/trezor/ui/pin.py b/core/src/trezor/ui/pin.py index be4c739b1..06284d28f 100644 --- a/core/src/trezor/ui/pin.py +++ b/core/src/trezor/ui/pin.py @@ -1,9 +1,15 @@ from micropython import const -from trezor import ui +from trezor import res, ui from trezor.crypto import random from trezor.ui import display -from trezor.ui.button import BTN_CLICKED, Button +from trezor.ui.button import ( + Button, + ButtonCancel, + ButtonClear, + ButtonConfirm, + ButtonMono, +) def digit_area(i): @@ -15,78 +21,119 @@ def digit_area(i): def generate_digits(): digits = list(range(0, 10)) # 0-9 random.shuffle(digits) - return digits + # We lay out the buttons top-left to bottom-right, but the order + # of the digits is defined as bottom-left to top-right (on numpad). + return digits[6:] + digits[3:6] + digits[:3] -class PinMatrix(ui.Widget): - def __init__(self, label, sublabel, pin="", maxlength=9): - self.label = label - self.sublabel = sublabel +class PinInput(ui.Control): + def __init__(self, prompt, subprompt, pin): + self.prompt = prompt + self.subprompt = subprompt self.pin = pin - self.maxlength = maxlength - self.digits = generate_digits() + self.repaint = True - # we lay out the buttons top-left to bottom-right, but the order of the - # digits is defined as bottom-left to top-right (on numpad) - reordered_digits = self.digits[6:] + self.digits[3:6] + self.digits[:3] + def on_render(self): + if self.repaint: + if self.pin: + self.render_pin() + else: + self.render_prompt() + self.repaint = False + + def render_pin(self): + display.bar(0, 0, ui.WIDTH, 50, ui.BG) + count = len(self.pin) + BOX_WIDTH = const(240) + DOT_SIZE = const(10) + PADDING = const(14) + RENDER_Y = const(20) + render_x = (BOX_WIDTH - count * PADDING) // 2 + for i in range(0, count): + display.bar_radius( + render_x + i * PADDING, RENDER_Y, DOT_SIZE, DOT_SIZE, ui.GREY, ui.BG, 4 + ) - self.pin_buttons = [ - Button(digit_area(i), str(d)) for i, d in enumerate(reordered_digits) - ] - self.onchange = None + def render_prompt(self): + display.bar(0, 0, ui.WIDTH, 50, ui.BG) + if self.subprompt: + display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG) + display.text_center( + ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG + ) + else: + display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG) - def taint(self): - super().taint() - for btn in self.pin_buttons: - btn.taint() - def render(self): - # pin matrix buttons - for btn in self.pin_buttons: - btn.render() +class PinButton(Button): + def __init__(self, index, digit, matrix): + self.matrix = matrix + super().__init__(digit_area(index), str(digit), ButtonMono) - if not self.tainted: - return + def on_click(self): + self.matrix.assign(self.matrix.input.pin + self.content) - # clear canvas under input line - display.bar(0, 0, ui.WIDTH, 52, ui.BG) - - if self.pin: - # input line with pin - l = len(self.pin) - y = const(20) - size = const(10) - padding = const(14) - box_w = const(240) - x = (box_w - l * padding) // 2 - for i in range(0, l): - ui.display.bar_radius(x + i * padding, y, size, size, ui.GREY, ui.BG, 4) - elif self.sublabel: - # input line with header label and sublabel - display.text_center(ui.WIDTH // 2, 20, self.label, ui.BOLD, ui.GREY, ui.BG) - display.text_center( - ui.WIDTH // 2, 46, self.sublabel, ui.NORMAL, ui.GREY, ui.BG - ) + +CANCELLED = object() + + +class PinDialog(ui.Layout): + def __init__(self, prompt, subprompt, allow_cancel=True, maxlength=9): + self.maxlength = maxlength + self.input = PinInput(prompt, subprompt, "") + + icon_confirm = res.load(ui.ICON_CONFIRM) + self.confirm_button = Button(ui.grid(14), icon_confirm, ButtonConfirm) + self.confirm_button.on_click = self.on_confirm + + icon_back = res.load(ui.ICON_BACK) + self.reset_button = Button(ui.grid(12), icon_back, ButtonClear) + self.reset_button.on_click = self.on_reset + + if allow_cancel: + icon_lock = res.load(ui.ICON_LOCK) + self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel) + self.cancel_button.on_click = self.on_cancel else: - # input line with header label - display.text_center(ui.WIDTH // 2, 36, self.label, ui.BOLD, ui.GREY, ui.BG) + self.cancel_button = Button(ui.grid(12), "") + self.cancel_button.disable() - self.tainted = False + self.pin_buttons = [ + PinButton(i, d, self) for i, d in enumerate(generate_digits()) + ] - def touch(self, event, pos): + def dispatch(self, event, x, y): for btn in self.pin_buttons: - if btn.touch(event, pos) == BTN_CLICKED: - if len(self.pin) < self.maxlength: - self.change(self.pin + btn.content) - break + btn.dispatch(event, x, y) + self.input.dispatch(event, x, y) + self.confirm_button.dispatch(event, x, y) + if self.input.pin: + self.reset_button.dispatch(event, x, y) + else: + self.cancel_button.dispatch(event, x, y) - def change(self, pin): - self.tainted = True - self.pin = pin + def assign(self, pin): + if len(pin) > self.maxlength: + return for btn in self.pin_buttons: - if len(self.pin) == self.maxlength: - btn.disable() - else: + if len(pin) < self.maxlength: btn.enable() - if self.onchange: - self.onchange() + else: + btn.disable() + if pin: + self.reset_button.enable() + self.cancel_button.disable() + else: + self.reset_button.disable() + self.cancel_button.enable() + self.input.pin = pin + self.input.repaint = True + + def on_reset(self): + self.assign("") + + def on_cancel(self): + raise ui.Result(CANCELLED) + + def on_confirm(self): + raise ui.Result(self.input.pin) diff --git a/core/src/trezor/ui/popup.py b/core/src/trezor/ui/popup.py new file mode 100644 index 000000000..fb6671784 --- /dev/null +++ b/core/src/trezor/ui/popup.py @@ -0,0 +1,17 @@ +from trezor import loop, ui + + +class Popup(ui.Layout): + def __init__(self, content, time_ms=0): + self.content = content + self.time_ms = time_ms + + def dispatch(self, event, x, y): + self.content.dispatch(event, x, y) + + def create_tasks(self): + return self.handle_input(), self.handle_rendering(), self.handle_timeout() + + def handle_timeout(self): + yield loop.sleep(self.time_ms * 1000) + raise ui.Result(None) diff --git a/core/src/trezor/ui/qr.py b/core/src/trezor/ui/qr.py index d573dbdd1..62527915a 100644 --- a/core/src/trezor/ui/qr.py +++ b/core/src/trezor/ui/qr.py @@ -1,11 +1,12 @@ from trezor import ui -class Qr(ui.Widget): - def __init__(self, data, pos, scale): +class Qr(ui.Control): + def __init__(self, data, x, y, scale): self.data = data - self.pos = pos + self.x = x + self.y = y self.scale = scale - def render(self): - ui.display.qrcode(self.pos[0], self.pos[1], self.data, self.scale) + def on_render(self): + ui.display.qrcode(self.x, self.y, self.data, self.scale) diff --git a/core/src/trezor/ui/scroll.py b/core/src/trezor/ui/scroll.py index d144f76d7..b5c9866e6 100644 --- a/core/src/trezor/ui/scroll.py +++ b/core/src/trezor/ui/scroll.py @@ -1,92 +1,180 @@ from micropython import const from trezor import loop, res, ui +from trezor.ui.button import Button, ButtonCancel, ButtonConfirm, ButtonDefault +from trezor.ui.confirm import CANCELLED, CONFIRMED from trezor.ui.swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe if __debug__: from apps.debug import swipe_signal -async def change_page(page, page_count): - while True: - if page == 0: - d = SWIPE_UP - elif page == page_count - 1: - d = SWIPE_DOWN - else: - d = SWIPE_VERTICAL - swipe = Swipe(directions=d) - if __debug__: - s = await loop.spawn(swipe, swipe_signal) - else: - s = await swipe - if s == SWIPE_UP: - return min(page + 1, page_count - 1) # scroll down - elif s == SWIPE_DOWN: - return max(page - 1, 0) # scroll up - - -async def paginate(render_page, page_count, page=0, *args): - while True: - changer = change_page(page, page_count) - renderer = render_page(page, page_count, *args) - waiter = loop.spawn(changer, renderer) - result = await waiter - if changer in waiter.finished: - page = result +def render_scrollbar(pages: int, page: int): + BBOX = const(220) + SIZE = const(8) + + padding = 14 + if pages * padding > BBOX: + padding = BBOX // pages + + X = const(220) + Y = (BBOX // 2) - (pages // 2) * padding + + for i in range(0, pages): + if i == page: + fg = ui.FG else: - return result + fg = ui.GREY + ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4) + + +def render_swipe_icon(): + DRAW_DELAY = const(200000) + icon = res.load(ui.ICON_SWIPE) + t = ui.pulse(DRAW_DELAY) + c = ui.blend(ui.GREY, ui.DARK_GREY, t) + ui.display.icon(70, 205, icon, c, ui.BG) -async def animate_swipe(): - time_delay = const(40000) - draw_delay = const(200000) +def render_swipe_text(): ui.display.text_center(130, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG) - sleep = loop.sleep(time_delay) - icon = res.load(ui.ICON_SWIPE) - for t in ui.pulse(draw_delay): - fg = ui.blend(ui.GREY, ui.DARK_GREY, t) - ui.display.icon(70, 205, icon, fg, ui.BG) - yield sleep +class Paginated(ui.Layout): + def __init__(self, pages, page=0, one_by_one=False): + self.pages = pages + self.page = page + self.one_by_one = one_by_one + self.repaint = True + + def dispatch(self, event, x, y): + pages = self.pages + page = self.page + pages[page].dispatch(event, x, y) + + if event is ui.RENDER: + length = len(pages) + if page < length - 1: + render_swipe_icon() + if self.repaint: + render_swipe_text() + if self.repaint: + render_scrollbar(length, page) + self.repaint = False + + async def handle_paging(self): + if self.page == 0: + directions = SWIPE_UP + elif self.page == len(self.pages) - 1: + directions = SWIPE_DOWN + else: + directions = SWIPE_VERTICAL + + if __debug__: + swipe = await loop.spawn(Swipe(directions), swipe_signal) + else: + swipe = await Swipe(directions) -def render_scrollbar(page, page_count): - bbox = const(220) - size = const(8) + if swipe is SWIPE_UP: + self.page = min(self.page + 1, len(self.pages) - 1) + elif swipe is SWIPE_DOWN: + self.page = max(self.page - 1, 0) - padding = 14 - if page_count * padding > bbox: - padding = bbox // page_count + self.pages[self.page].dispatch(ui.REPAINT, 0, 0) + self.repaint = True - x = const(220) - y = (bbox // 2) - (page_count // 2) * padding + self.on_change() - for i in range(0, page_count): - if i != page: - ui.display.bar_radius(x, y + i * padding, size, size, ui.GREY, ui.BG, 4) - ui.display.bar_radius(x, y + page * padding, size, size, ui.FG, ui.BG, 4) + def create_tasks(self): + return self.handle_input(), self.handle_rendering(), self.handle_paging() + def on_change(self): + if self.one_by_one: + raise ui.Result(self.page) -class Scrollpage(ui.Widget): - def __init__(self, content, page, page_count): - self.content = content - self.page = page - self.page_count = page_count - if content.__class__.__iter__ is not ui.Widget.__iter__: - raise TypeError( - "Scrollpage does not support widgets with custom event loop" - ) +class PageWithButtons(ui.Control): + def __init__(self, content, paginated, index, count): + self.content = content + self.paginated = paginated + self.index = index + self.count = count + + if self.index == 0: + # first page, we can cancel or go down + left = res.load(ui.ICON_CANCEL) + left_style = ButtonCancel + right = res.load(ui.ICON_CLICK) + right_style = ButtonDefault + elif self.index == count - 1: + # last page, we can go up or confirm + left = res.load(ui.ICON_BACK) + left_style = ButtonDefault + right = res.load(ui.ICON_CONFIRM) + right_style = ButtonConfirm + else: + # somewhere in the middle, we can go up or down + left = res.load(ui.ICON_BACK) + left_style = ButtonDefault + right = res.load(ui.ICON_CLICK) + right_style = ButtonDefault + + self.left = Button(ui.grid(8, n_x=2), left, left_style) + self.left.on_click = self.on_left + + self.right = Button(ui.grid(9, n_x=2), right, right_style) + self.right.on_click = self.on_right + + def dispatch(self, event, x, y): + self.content.dispatch(event, x, y) + self.left.dispatch(event, x, y) + self.right.dispatch(event, x, y) + + def on_left(self): + if self.index == 0: + self.paginated.on_cancel() + else: + self.paginated.on_down() - def taint(self): - super().taint() - self.content.taint() + def on_right(self): + if self.index == self.count - 1: + self.paginated.on_confirm() + else: + self.paginated.on_up() - def render(self): - self.content.render() - render_scrollbar(self.page, self.page_count) - def touch(self, event, pos): - return self.content.touch(event, pos) +class PaginatedWithButtons(ui.Layout): + def __init__(self, pages, page=0, one_by_one=False): + self.pages = [ + PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages) + ] + self.page = page + self.one_by_one = one_by_one + + def dispatch(self, event, x, y): + pages = self.pages + page = self.page + pages[page].dispatch(event, x, y) + if event is ui.RENDER: + render_scrollbar(len(pages), page) + + def on_up(self): + self.page = max(self.page - 1, 0) + self.pages[self.page].dispatch(ui.REPAINT, 0, 0) + self.on_change() + + def on_down(self): + self.page = min(self.page + 1, len(self.pages) - 1) + self.pages[self.page].dispatch(ui.REPAINT, 0, 0) + self.on_change() + + def on_confirm(self): + raise ui.Result(CONFIRMED) + + def on_cancel(self): + raise ui.Result(CANCELLED) + + def on_change(self): + if self.one_by_one: + raise ui.Result(self.page) diff --git a/core/src/trezor/ui/style.py b/core/src/trezor/ui/style.py index c1e0864d9..38e73efac 100644 --- a/core/src/trezor/ui/style.py +++ b/core/src/trezor/ui/style.py @@ -1,6 +1,6 @@ from micropython import const -from trezor.ui import BOLD, MONO, NORMAL, rgb +from trezor.ui import rgb # radius for buttons and other elements RADIUS = const(2) @@ -66,193 +66,3 @@ ICON_BACK = "trezor/res/left.toig" ICON_SWIPE = "trezor/res/swipe.toig" ICON_CHECK = "trezor/res/check.toig" ICON_SPACE = "trezor/res/space.toig" - -# buttons -BTN_DEFAULT = { - "normal": { - "bg-color": BG, - "fg-color": FG, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": BG, - "text-style": BOLD, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_CANCEL = { - "normal": { - "bg-color": RED, - "fg-color": FG, - "text-style": BOLD, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": RED, - "text-style": BOLD, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_CONFIRM = { - "normal": { - "bg-color": GREEN, - "fg-color": FG, - "text-style": BOLD, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": GREEN, - "text-style": BOLD, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_CLEAR = { - "normal": { - "bg-color": ORANGE, - "fg-color": FG, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": BG, - "fg-color": GREY, - "text-style": NORMAL, - "border-color": BG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_KEY = { - "normal": { - "bg-color": BLACKISH, - "fg-color": FG, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": BLACKISH, - "text-style": MONO, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_KEY_DARK = { - "normal": { - "bg-color": DARK_BLACK, - "fg-color": DARK_WHITE, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": DARK_BLACK, - "text-style": MONO, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": DARK_BLACK, - "fg-color": GREY, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, -} - -BTN_KEY_CONFIRM = { - "normal": { - "bg-color": GREEN, - "fg-color": FG, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, - "active": { - "bg-color": FG, - "fg-color": GREEN, - "text-style": MONO, - "border-color": FG, - "radius": RADIUS, - }, - "disabled": { - "bg-color": BG, - "fg-color": GREY, - "text-style": MONO, - "border-color": BG, - "radius": RADIUS, - }, -} - -# loader -LDR_DEFAULT = { - "normal": {"bg-color": BG, "fg-color": GREEN, "icon": None, "icon-fg-color": None}, - "active": { - "bg-color": BG, - "fg-color": GREEN, - "icon": ICON_CHECK, - "icon-fg-color": WHITE, - }, -} - -LDR_DANGER = { - "normal": {"bg-color": BG, "fg-color": RED, "icon": None, "icon-fg-color": None}, - "active": { - "bg-color": BG, - "fg-color": RED, - "icon": ICON_CHECK, - "icon-fg-color": WHITE, - }, -} diff --git a/core/src/trezor/ui/swipe.py b/core/src/trezor/ui/swipe.py index 02dd2ccbb..34fe9b82a 100644 --- a/core/src/trezor/ui/swipe.py +++ b/core/src/trezor/ui/swipe.py @@ -1,7 +1,6 @@ from micropython import const -from trezor import io, ui -from trezor.ui import contains, rotate +from trezor import io, loop, ui SWIPE_UP = const(0x01) SWIPE_DOWN = const(0x02) @@ -12,79 +11,99 @@ SWIPE_HORIZONTAL = const(SWIPE_LEFT | SWIPE_RIGHT) SWIPE_ALL = const(SWIPE_VERTICAL | SWIPE_HORIZONTAL) _SWIPE_DISTANCE = const(120) +_SWIPE_TRESHOLD = const(30) -class Swipe(ui.Widget): - def __init__(self, area=None, absolute=False, directions=SWIPE_ALL, treshold=30): - self.area = area or (0, 0, ui.WIDTH, ui.HEIGHT) - self.absolute = absolute +class Swipe(ui.Control): + def __init__(self, directions=SWIPE_ALL, area=None): + if area is None: + area = (0, 0, ui.WIDTH, ui.HEIGHT) + self.area = area self.directions = directions - self.treshold = treshold - self.start_pos = None + self.start_x = None + self.start_y = None self.light_origin = None self.light_target = ui.BACKLIGHT_NONE - def touch(self, event, pos): - - if not self.absolute: - pos = rotate(pos) + def on_touch_start(self, x, y): + if ui.in_area(self.area, x, y): + self.start_x = x + self.start_y = y + self.light_origin = ui.BACKLIGHT_NORMAL - if event == io.TOUCH_MOVE and self.start_pos is not None: - pdx = pos[0] - self.start_pos[0] - pdy = pos[1] - self.start_pos[1] + def on_touch_move(self, x, y): + if self.start_x is None: + return # not started in our area - pdxa = abs(pdx) - pdya = abs(pdy) - if pdxa > pdya and self.directions & SWIPE_HORIZONTAL: - # Horizontal direction - if (pdx > 0 and self.directions & SWIPE_RIGHT) or ( - pdx < 0 and self.directions & SWIPE_LEFT - ): - ui.display.backlight( - ui.lerpi( - self.light_origin, - self.light_target, - pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1, - ) + dirs = self.directions + pdx = x - self.start_x + pdy = y - self.start_y + pdxa = abs(pdx) + pdya = abs(pdy) + if pdxa > pdya and dirs & SWIPE_HORIZONTAL: + # horizontal direction + if (pdx > 0 and dirs & SWIPE_RIGHT) or (pdx < 0 and dirs & SWIPE_LEFT): + ui.display.backlight( + ui.lerpi( + self.light_origin, + self.light_target, + min(pdxa / _SWIPE_DISTANCE, 1), ) - elif pdxa < pdya and self.directions & SWIPE_VERTICAL: - # Vertical direction - if (pdy > 0 and self.directions & SWIPE_DOWN) or ( - pdy < 0 and self.directions & SWIPE_UP - ): - ui.display.backlight( - ui.lerpi( - self.light_origin, - self.light_target, - pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1, - ) + ) + elif pdxa < pdya and dirs & SWIPE_VERTICAL: + # vertical direction + if (pdy > 0 and dirs & SWIPE_DOWN) or (pdy < 0 and dirs & SWIPE_UP): + ui.display.backlight( + ui.lerpi( + self.light_origin, + self.light_target, + min(pdya / _SWIPE_DISTANCE, 1), ) + ) - elif event == io.TOUCH_START and contains(self.area, pos): - self.start_pos = pos - self.light_origin = ui.BACKLIGHT_NORMAL + def on_touch_end(self, x, y): + if self.start_x is None: + return # not started in our area + + dirs = self.directions + pdx = x - self.start_x + pdy = y - self.start_y + pdxa = abs(pdx) + pdya = abs(pdy) + if pdxa > pdya and dirs & SWIPE_HORIZONTAL: + # horizontal direction + ratio = min(pdxa / _SWIPE_DISTANCE, 1) + if ratio * 100 >= _SWIPE_TRESHOLD: + if pdx > 0 and dirs & SWIPE_RIGHT: + self.on_swipe(SWIPE_RIGHT) + return + elif pdx < 0 and dirs & SWIPE_LEFT: + self.on_swipe(SWIPE_LEFT) + return + elif pdxa < pdya and dirs & SWIPE_VERTICAL: + # vertical direction + ratio = min(pdya / _SWIPE_DISTANCE, 1) + if ratio * 100 >= _SWIPE_TRESHOLD: + if pdy > 0 and dirs & SWIPE_DOWN: + self.on_swipe(SWIPE_DOWN) + return + elif pdy < 0 and dirs & SWIPE_UP: + self.on_swipe(SWIPE_UP) + return + + # no swipe detected, reset the state + ui.display.backlight(self.light_origin) + self.start_x = None + self.start_y = None + + def on_swipe(self, swipe): + raise ui.Result(swipe) - elif event == io.TOUCH_END and self.start_pos is not None: - pdx = pos[0] - self.start_pos[0] - pdy = pos[1] - self.start_pos[1] - pdxa = abs(pdx) - pdya = abs(pdy) - if pdxa > pdya and self.directions & SWIPE_HORIZONTAL: - # Horizontal direction - ratio = pdxa / _SWIPE_DISTANCE if pdxa < _SWIPE_DISTANCE else 1 - if ratio * 100 >= self.treshold: - if pdx > 0 and self.directions & SWIPE_RIGHT: - return SWIPE_RIGHT - elif pdx < 0 and self.directions & SWIPE_LEFT: - return SWIPE_LEFT - elif pdxa < pdya and self.directions & SWIPE_VERTICAL: - # Vertical direction - ratio = pdya / _SWIPE_DISTANCE if pdya < _SWIPE_DISTANCE else 1 - if ratio * 100 >= self.treshold: - if pdy > 0 and self.directions & SWIPE_DOWN: - return SWIPE_DOWN - elif pdy < 0 and self.directions & SWIPE_UP: - return SWIPE_UP - # No swipe, reset the state - self.start_pos = None - ui.display.backlight(self.light_origin) + def __iter__(self): + try: + touch = loop.wait(io.TOUCH) + while True: + event, x, y = yield touch + self.dispatch(event, x, y) + except ui.Result as result: + return result.value diff --git a/core/src/trezor/ui/text.py b/core/src/trezor/ui/text.py index e2304f151..0746001ba 100644 --- a/core/src/trezor/ui/text.py +++ b/core/src/trezor/ui/text.py @@ -33,13 +33,13 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None: has_next_word = word_index < len(words) - 1 if isinstance(word, int): - if word in [BR, BR_HALF]: + if word is BR or word is BR_HALF: # line break or half-line break if offset_y >= OFFSET_Y_MAX: ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg) return offset_x = TEXT_MARGIN_LEFT - offset_y += TEXT_LINE_HEIGHT if word == BR else TEXT_LINE_HEIGHT_HALF + offset_y += TEXT_LINE_HEIGHT if word is BR else TEXT_LINE_HEIGHT_HALF elif word in FONTS: # change of font style font = word @@ -108,7 +108,7 @@ def render_text(words: list, new_lines: bool, max_lines: int) -> None: offset_x += SPACE -class Text(ui.Widget): +class Text(ui.Control): def __init__( self, header_text: str, @@ -123,6 +123,7 @@ class Text(ui.Widget): self.max_lines = max_lines self.new_lines = new_lines self.content = [] + self.repaint = True def normal(self, *content): self.content.append(ui.NORMAL) @@ -146,8 +147,8 @@ class Text(ui.Widget): def br_half(self): self.content.append(BR_HALF) - def render(self): - if self.tainted: + def on_render(self): + if self.repaint: ui.header( self.header_text, self.header_icon, @@ -156,4 +157,4 @@ class Text(ui.Widget): self.icon_color, ) render_text(self.content, self.new_lines, self.max_lines) - self.tainted = False + self.repaint = False diff --git a/core/src/trezor/ui/word_select.py b/core/src/trezor/ui/word_select.py index 67db8d124..ee3209550 100644 --- a/core/src/trezor/ui/word_select.py +++ b/core/src/trezor/ui/word_select.py @@ -1,56 +1,28 @@ -from micropython import const +from trezor import ui +from trezor.ui.button import Button -from trezor import loop, ui -from trezor.ui import Widget -from trezor.ui.button import BTN_CLICKED, Button -if __debug__: - from apps.debug import input_signal - - -_W12 = const(12) -_W18 = const(18) -_W24 = const(24) - - -class WordSelector(Widget): +class WordSelector(ui.Layout): def __init__(self, content): self.content = content - self.w12 = Button( - ui.grid(6, n_y=4, n_x=3, cells_y=2), str(_W12), style=ui.BTN_KEY - ) - self.w18 = Button( - ui.grid(7, n_y=4, n_x=3, cells_y=2), str(_W18), style=ui.BTN_KEY - ) - self.w24 = Button( - ui.grid(8, n_y=4, n_x=3, cells_y=2), str(_W24), style=ui.BTN_KEY - ) - - def taint(self): - super().taint() - self.w12.taint() - self.w18.taint() - self.w24.taint() - - def render(self): - self.w12.render() - self.w18.render() - self.w24.render() - - def touch(self, event, pos): - if self.w12.touch(event, pos) == BTN_CLICKED: - return _W12 - if self.w18.touch(event, pos) == BTN_CLICKED: - return _W18 - if self.w24.touch(event, pos) == BTN_CLICKED: - return _W24 - - async def __iter__(self): - if __debug__: - result = await loop.spawn(super().__iter__(), self.content, input_signal) - if isinstance(result, str): - return int(result) - else: - return result - else: - return await loop.spawn(super().__iter__(), self.content) + self.w12 = Button(ui.grid(6, n_y=4, n_x=3, cells_y=2), "12") + self.w12.on_click = self.on_w12 + self.w18 = Button(ui.grid(7, n_y=4, n_x=3, cells_y=2), "18") + self.w18.on_click = self.on_w18 + self.w24 = Button(ui.grid(8, n_y=4, n_x=3, cells_y=2), "24") + self.w24.on_click = self.on_w24 + + def dispatch(self, event, x, y): + self.content.dispatch(event, x, y) + self.w12.dispatch(event, x, y) + self.w18.dispatch(event, x, y) + self.w24.dispatch(event, x, y) + + def on_w12(self): + raise ui.Result(12) + + def on_w18(self): + raise ui.Result(18) + + def on_w24(self): + raise ui.Result(24) diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index bd5191ee1..84019d187 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -18,10 +18,12 @@ if __debug__: import uos TEST = int(uos.getenv("TREZOR_TEST") or "0") + DISABLE_FADE = int(uos.getenv("TREZOR_DISABLE_FADE") or "0") SAVE_SCREEN = int(uos.getenv("TREZOR_SAVE_SCREEN") or "0") LOG_MEMORY = int(uos.getenv("TREZOR_LOG_MEMORY") or "0") else: TEST = 0 + DISABLE_FADE = 0 SAVE_SCREEN = 0 LOG_MEMORY = 0 diff --git a/core/src/trezor/workflow.py b/core/src/trezor/workflow.py index c565c9478..89f1fdab7 100644 --- a/core/src/trezor/workflow.py +++ b/core/src/trezor/workflow.py @@ -2,12 +2,10 @@ from trezor import loop workflows = [] layouts = [] +layout_signal = loop.signal() default = None default_layout = None -# HACK: workaround way to stop the WebAuthn layout from the outside -webauthn_stop_signal = loop.signal() - def onstart(w): workflows.append(w) @@ -54,7 +52,6 @@ def restartdefault(): def onlayoutstart(l): closedefault() layouts.append(l) - webauthn_stop_signal.send(None) def onlayoutclose(l):