diff --git a/assets/5390-200.png b/assets/5390-200.png new file mode 100644 index 00000000..ad62e1cb Binary files /dev/null and b/assets/5390-200.png differ diff --git a/assets/close-button.png b/assets/click-icon.png similarity index 54% rename from assets/close-button.png rename to assets/click-icon.png index 6deb5bfb..ddf37eee 100644 Binary files a/assets/close-button.png and b/assets/click-icon.png differ diff --git a/assets/confirm.png b/assets/confirm.png index 4dadd5ff..69a3df95 100644 Binary files a/assets/confirm.png and b/assets/confirm.png differ diff --git a/assets/clear.png b/assets/cross2.png similarity index 65% rename from assets/clear.png rename to assets/cross2.png index dd5901da..e57702e5 100644 Binary files a/assets/clear.png and b/assets/cross2.png differ diff --git a/assets/left.png b/assets/left.png new file mode 100644 index 00000000..9f502018 Binary files /dev/null and b/assets/left.png differ diff --git a/assets/recovery-old.png b/assets/recovery-old.png new file mode 100644 index 00000000..dfa41cbc Binary files /dev/null and b/assets/recovery-old.png differ diff --git a/assets/recovery.png b/assets/recovery.png index dfa41cbc..40ac64a3 100644 Binary files a/assets/recovery.png and b/assets/recovery.png differ diff --git a/assets/send-old.png b/assets/send-old.png new file mode 100644 index 00000000..9dd8b49a Binary files /dev/null and b/assets/send-old.png differ diff --git a/assets/send.png b/assets/send.png index 9dd8b49a..d3aa9cfb 100644 Binary files a/assets/send.png and b/assets/send.png differ diff --git a/src/apps/common/request_words.py b/src/apps/common/request_words.py new file mode 100644 index 00000000..33888403 --- /dev/null +++ b/src/apps/common/request_words.py @@ -0,0 +1,30 @@ +from trezor import wire, ui, loop +from trezor.utils import unimport + +# used to confirm/cancel the dialogs from outside of this module (i.e. +# through debug link) +if __debug__: + signal = loop.signal() + + +@ui.layout +@unimport +async def request_words(ctx, content, code=None, *args, **kwargs): + from trezor.ui.word_select import WordSelector + from trezor.messages.ButtonRequest import ButtonRequest + from trezor.messages.ButtonRequestType import Other + from trezor.messages.wire_types import ButtonAck + + ui.display.clear() + dialog = WordSelector(content, *args, **kwargs) + dialog.render() + + if code is None: + code = Other + await ctx.call(ButtonRequest(code=code), ButtonAck) + + if __debug__: + waiter = loop.wait(signal, dialog) + else: + waiter = dialog + return await waiter diff --git a/src/apps/management/recovery_device.py b/src/apps/management/recovery_device.py index 8fab3d8d..1b2cd46e 100644 --- a/src/apps/management/recovery_device.py +++ b/src/apps/management/recovery_device.py @@ -11,15 +11,20 @@ async def layout_recovery_device(ctx, msg): from trezor.ui.text import Text from apps.common import storage from apps.common.confirm import require_confirm + from apps.common.request_words import request_words if storage.is_initialized(): raise wire.FailureError(UnexpectedMessage, 'Already initialized') words = [] + wc = await request_words(ctx, Text( + 'Device recovery', ui.ICON_RECOVERY, 'Number of words?')) + msg.word_count = int(wc) + ui.display.clear() kbd = KeyboardMultiTap() for i in range(0, msg.word_count): - kbd.prompt = '%s. ' % (i + 1) + kbd.prompt = 'Type %s. word' % (i + 1) word = await kbd words.append(word) diff --git a/src/trezor/res/click.toig b/src/trezor/res/click.toig new file mode 100644 index 00000000..8c906acf Binary files /dev/null and b/src/trezor/res/click.toig differ diff --git a/src/trezor/res/confirm2.toig b/src/trezor/res/confirm2.toig new file mode 100644 index 00000000..cae04d25 Binary files /dev/null and b/src/trezor/res/confirm2.toig differ diff --git a/src/trezor/res/cross2.toig b/src/trezor/res/cross2.toig new file mode 100644 index 00000000..8d0df4bc Binary files /dev/null and b/src/trezor/res/cross2.toig differ diff --git a/src/trezor/res/left.toig b/src/trezor/res/left.toig new file mode 100644 index 00000000..9b230ed6 Binary files /dev/null and b/src/trezor/res/left.toig differ diff --git a/src/trezor/res/recovery.toig b/src/trezor/res/recovery.toig new file mode 100644 index 00000000..1380e866 Binary files /dev/null and b/src/trezor/res/recovery.toig differ diff --git a/src/trezor/res/send2.toig b/src/trezor/res/send2.toig new file mode 100644 index 00000000..6a75b310 Binary files /dev/null and b/src/trezor/res/send2.toig differ diff --git a/src/trezor/ui/__init__.py b/src/trezor/ui/__init__.py index 9fad0f3b..f4913fde 100644 --- a/src/trezor/ui/__init__.py +++ b/src/trezor/ui/__init__.py @@ -106,11 +106,10 @@ def layout(f): return inner -def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG): - display.bar(0, 0, 240, 32, bg) +def header(title: str, icon: bytes=ICON_RESET, fg: int=BG, bg: int=BG, ifg: int=BG): if icon is not None: - display.icon(8, 4, res.load(icon), fg, bg) - display.text(8 + 24 + 2, 24, title, BOLD, fg, bg) + display.icon(14, 14, res.load(icon), ifg, bg) + display.text(44, 35, title, BOLD, fg, bg) class Widget: diff --git a/src/trezor/ui/button.py b/src/trezor/ui/button.py index 4236f99e..687374b2 100644 --- a/src/trezor/ui/button.py +++ b/src/trezor/ui/button.py @@ -32,11 +32,13 @@ class Button(Widget): self.state = BTN_DIRTY def enable(self): - self.state &= ~BTN_DISABLED - self.state |= BTN_DIRTY + if self.state & BTN_DISABLED: + self.state &= ~BTN_DISABLED + self.state |= BTN_DIRTY def disable(self): - self.state |= BTN_DISABLED | BTN_DIRTY + if not self.state & BTN_DISABLED: + self.state |= BTN_DISABLED | BTN_DIRTY def taint(self): self.state |= BTN_DIRTY @@ -54,6 +56,7 @@ class Button(Widget): ax, ay, aw, ah = self.area tx = ax + aw // 2 ty = ay + ah // 2 + 8 + display.bar_radius(ax, ay, aw, ah, s['border-color'], ui.BG, @@ -70,7 +73,7 @@ class Button(Widget): s['bg-color']) else: - display.icon(tx - 15, ty - 20, self.content, + display.icon(tx - 8, ty - 16, self.content, s['fg-color'], s['bg-color']) diff --git a/src/trezor/ui/keyboard.py b/src/trezor/ui/keyboard.py index 4a01cd95..964b26d8 100644 --- a/src/trezor/ui/keyboard.py +++ b/src/trezor/ui/keyboard.py @@ -4,7 +4,7 @@ from trezor.ui import display from trezor.ui.button import Button, BTN_CLICKED -def cell_area(i, n_x=3, n_y=3, start_x=0, start_y=40, end_x=240, end_y=240, spacing=0): +def cell_area(i, n_x=3, n_y=3, start_x=6, start_y=66, end_x=234, end_y=237, spacing=0): w = (end_x - start_x) // n_x h = (end_y - start_y) // n_y x = (i % n_x) * w @@ -17,7 +17,8 @@ def key_buttons(): # keys = [' ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'] return [Button(cell_area(i), k, normal_style=ui.BTN_KEY, - active_style=ui.BTN_KEY_ACTIVE) for i, k in enumerate(keys)] + active_style=ui.BTN_KEY_ACTIVE, + disabled_style=ui.BTN_KEY_DISABLED) for i, k in enumerate(keys)] def compute_mask(text): @@ -41,38 +42,52 @@ class KeyboardMultiTap(ui.Widget): self.pending_index = 0 self.key_buttons = key_buttons() - self.sugg_button = Button((5, 5, 240 - 35, 30), '') - self.bs_button = Button((240 - 35, 5, 30, 30), - res.load('trezor/res/pin_close.toig'), + self.sugg_button = Button((63, 0, 240 - 65, 57), '') + self.bs_button = Button((6, 5, 57, 60), + res.load('trezor/res/left.toig'), normal_style=ui.BTN_CLEAR, active_style=ui.BTN_CLEAR_ACTIVE) def render(self): - - # clear canvas under input line - display.bar(0, 0, 205, 40, ui.BG) - - # input line - content_width = display.text_width(self.prompt + self.content, ui.BOLD) - display.text(20, 30, self.prompt + self.content, ui.BOLD, ui.FG, ui.BG) - - # pending marker - if self.pending_button is not None: - pending_width = display.text_width(self.content[-1:], ui.BOLD) - pending_x = 20 + content_width - pending_width - display.bar(pending_x, 33, pending_width + 2, 3, ui.FG) - - # auto-suggest - if self.sugg_word is not None: - sugg_rest = self.sugg_word[len(self.content):] - sugg_x = 20 + content_width - display.text(sugg_x, 30, sugg_rest, ui.BOLD, ui.GREY, ui.BG) - - # render backspace button if self.content: + display.bar(62, 8, 168, 54, ui.BG) + content_width = display.text_width(self.content, ui.BOLD) + offset_x = 78 + if self.content == self.sugg_word: + # confirm button + content + display.bar_radius(67, 8, 164, 54, ui.GREEN, ui.BG, ui.RADIUS) + type_icon = res.load(ui.ICON_CONFIRM2) + display.icon(228 - 30, 28, type_icon, ui.WHITE, ui.GREEN) + display.text(offset_x, 40, self.content, ui.BOLD, ui.WHITE, ui.GREEN) + + elif self.sugg_word is not None: + # auto-suggest button + content + suggestion + display.bar_radius(67, 8, 164, 54, ui.BLACKISH, ui.BG, ui.RADIUS) + display.text(offset_x, 40, self.content, ui.BOLD, ui.FG, ui.BLACKISH) + sugg_text = self.sugg_word[len(self.content):] + sugg_x = offset_x + content_width + type_icon = res.load(ui.ICON_CLICK) + display.icon(228 - 30, 24, type_icon, ui.GREY, ui.BLACKISH) + display.text(sugg_x, 40, sugg_text, ui.BOLD, ui.GREY, ui.BLACKISH) + + else: + # content + display.bar(63, 8, 168, 54, ui.BG) + display.text(offset_x, 40, self.content, ui.BOLD, ui.FG, ui.BG) + + # backspace button self.bs_button.render() + + # pending marker + if self.pending_button is not None: + pending_width = display.text_width(self.content[-1:], ui.BOLD) + pending_x = offset_x + content_width - pending_width + display.bar(pending_x, 42, pending_width + 2, 3, ui.FG) + else: - display.bar(240 - 48, 0, 48, 42, ui.BG) + # prompt + display.bar(0, 8, 240, 60, ui.BG) + display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) # key buttons for btn in self.key_buttons: @@ -87,10 +102,12 @@ class KeyboardMultiTap(ui.Widget): self._update_buttons() return if self.sugg_button.touch(event, pos) == BTN_CLICKED: - if self.content == bip39.find_word(self.content): + if not self.content or self.sugg_word is None: + return None + if self.content == self.sugg_word: result = self.content self.content = '' - elif self.sugg_word is not None: + else: result = None self.content = self.sugg_word self.pending_button = None @@ -128,19 +145,22 @@ class KeyboardMultiTap(ui.Widget): else: btn.disable() - def __iter__(self): + async def __iter__(self): timeout = loop.sleep(1000 * 1000 * 1) touch = loop.select(io.TOUCH) wait_timeout = loop.wait(touch, timeout) wait_touch = loop.wait(touch) content = None + + self.bs_button.taint() + while content is None: self.render() if self.pending_button is not None: wait = wait_timeout else: wait = wait_touch - result = yield wait + result = await wait if touch in wait.finished: event, *pos = result content = self.touch(event, pos) @@ -154,90 +174,90 @@ class KeyboardMultiTap(ui.Widget): return content -def zoom_buttons(keys, upper=False): - n_x = len(keys) - if upper: - keys = keys + keys.upper() - n_y = 2 - else: - n_y = 1 - return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)] +# def zoom_buttons(keys, upper=False): +# n_x = len(keys) +# if upper: +# keys = keys + keys.upper() +# n_y = 2 +# else: +# n_y = 1 +# return [Button(cell_area(i, n_x, n_y), key) for i, key in enumerate(keys)] -class KeyboardZooming(ui.Widget): +# class KeyboardZooming(ui.Widget): - def __init__(self, content='', uppercase=True): - self.content = content - self.uppercase = uppercase +# def __init__(self, content='', uppercase=True): +# self.content = content +# self.uppercase = uppercase - self.zoom_buttons = None - self.key_buttons = key_buttons() - self.bs_button = Button((240 - 35, 5, 30, 30), - res.load('trezor/res/pin_close.toig'), - normal_style=ui.BTN_CLEAR, - active_style=ui.BTN_CLEAR_ACTIVE) +# self.zoom_buttons = None +# self.key_buttons = key_buttons() +# self.bs_button = Button((240 - 35, 5, 30, 30), +# res.load('trezor/res/pin_close.toig'), +# normal_style=ui.BTN_CLEAR, +# active_style=ui.BTN_CLEAR_ACTIVE) - def render(self): - self.render_input() - if self.zoom_buttons: - for btn in self.zoom_buttons: - btn.render() - else: - for btn in self.key_buttons: - btn.render() +# def render(self): +# self.render_input() +# if self.zoom_buttons: +# for btn in self.zoom_buttons: +# btn.render() +# else: +# for btn in self.key_buttons: +# btn.render() - def render_input(self): - if self.content: - display.bar(0, 0, 200, 40, ui.BG) - else: - display.bar(0, 0, 240, 40, ui.BG) - display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG) - if self.content: - self.bs_button.render() +# def render_input(self): +# if self.content: +# display.bar(0, 0, 200, 40, ui.BG) +# else: +# display.bar(0, 0, 240, 40, ui.BG) +# display.text(20, 30, self.content, ui.BOLD, ui.GREY, ui.BG) +# if self.content: +# self.bs_button.render() - def touch(self, event, pos): - if self.bs_button.touch(event, pos) == BTN_CLICKED: - self.content = self.content[:-1] - self.bs_button.taint() - return - if self.zoom_buttons: - return self.touch_zoom(event, pos) - else: - return self.touch_keyboard(event, pos) +# def touch(self, event, pos): +# if self.bs_button.touch(event, pos) == BTN_CLICKED: +# self.content = self.content[:-1] +# self.bs_button.taint() +# return +# if self.zoom_buttons: +# return self.touch_zoom(event, pos) +# else: +# return self.touch_keyboard(event, pos) - def touch_zoom(self, event, pos): - for btn in self.zoom_buttons: - if btn.touch(event, pos) == BTN_CLICKED: - self.content += btn.content - self.zoom_buttons = None - for b in self.key_buttons: - b.taint() - self.bs_button.taint() - break +# def touch_zoom(self, event, pos): +# for btn in self.zoom_buttons: +# if btn.touch(event, pos) == BTN_CLICKED: +# self.content += btn.content +# self.zoom_buttons = None +# for b in self.key_buttons: +# b.taint() +# self.bs_button.taint() +# break - def touch_keyboard(self, event, pos): - for btn in self.key_buttons: - if btn.touch(event, pos) == BTN_CLICKED: - self.zoom_buttons = zoom_buttons(btn.content, self.uppercase) - for b in self.zoom_buttons: - b.taint() - self.bs_button.taint() - break +# def touch_keyboard(self, event, pos): +# for btn in self.key_buttons: +# if btn.touch(event, pos) == BTN_CLICKED: +# self.zoom_buttons = zoom_buttons(btn.content, self.uppercase) +# for b in self.zoom_buttons: +# b.taint() +# self.bs_button.taint() +# break - def __iter__(self): - timeout = loop.sleep(1000 * 1000 * 1) - touch = loop.select(io.TOUCH) - wait = loop.wait(touch, timeout) - while True: - self.render() - result = yield wait - if touch in wait.finished: - event, *pos = result - self.touch(event, pos) - elif self.zoom_buttons: - self.zoom_buttons = None - for btn in self.key_buttons: - btn.taint() +# def __iter__(self): +# timeout = loop.sleep(1000 * 1000 * 1) +# touch = loop.select(io.TOUCH) +# wait = loop.wait(touch, timeout) +# while True: +# self.render() +# result = yield wait +# if touch in wait.finished: +# event, *pos = result +# self.touch(event, pos) +# elif self.zoom_buttons: +# self.zoom_buttons = None +# for btn in self.key_buttons: +# btn.taint() Keyboard = KeyboardMultiTap diff --git a/src/trezor/ui/style.py b/src/trezor/ui/style.py index 34da5f51..d82b8864 100644 --- a/src/trezor/ui/style.py +++ b/src/trezor/ui/style.py @@ -38,7 +38,10 @@ DARK_GREY = rgb(0x3E, 0x3E, 0x3E) BLUE_GRAY = rgb(0x60, 0x7D, 0x8B) BLACK = rgb(0x00, 0x00, 0x00) WHITE = rgb(0xFA, 0xFA, 0xFA) -BLACKISH = rgb(0x20, 0x20, 0x20) +BLACKISH = rgb(0x30, 0x30, 0x30) + +TITLE_GREY = rgb(0x9B, 0x9B, 0x9B) +ORANGE_ICON = rgb(0xF5, 0xA6, 0x23) # common color styles BG = BLACK @@ -47,11 +50,13 @@ FG = WHITE # icons ICON_RESET = 'trezor/res/header_icons/reset.toig' ICON_WIPE = 'trezor/res/header_icons/wipe.toig' -ICON_RECOVERY = 'trezor/res/header_icons/recovery.toig' +ICON_RECOVERY = 'trezor/res/header_icons/reset.toig' ICON_CLEAR = 'trezor/res/clear.toig' ICON_CONFIRM = 'trezor/res/confirm.toig' +ICON_CONFIRM2 = 'trezor/res/confirm2.toig' ICON_LOCK = 'trezor/res/lock.toig' ICON_SEND = 'trezor/res/send.toig' +ICON_CLICK = 'trezor/res/click.toig' # buttons BTN_DEFAULT = { @@ -104,7 +109,7 @@ BTN_CONFIRM_ACTIVE = { 'radius': RADIUS, } BTN_CLEAR = { - 'bg-color': BG, + 'bg-color': ORANGE, 'fg-color': FG, 'text-style': NORMAL, 'border-color': BG, @@ -131,6 +136,13 @@ BTN_KEY_ACTIVE = { 'border-color': FG, 'radius': RADIUS, } +BTN_KEY_DISABLED = { + 'bg-color': BG, + 'fg-color': GREY, + 'text-style': MONO, + 'border-color': BG, + 'radius': RADIUS, +} # loader LDR_DEFAULT = { diff --git a/src/trezor/ui/text.py b/src/trezor/ui/text.py index 7b49b659..9168a2c9 100644 --- a/src/trezor/ui/text.py +++ b/src/trezor/ui/text.py @@ -1,9 +1,9 @@ from micropython import const from trezor import ui -TEXT_HEADER_HEIGHT = const(32) +TEXT_HEADER_HEIGHT = const(51) TEXT_LINE_HEIGHT = const(23) -TEXT_MARGIN_LEFT = const(10) +TEXT_MARGIN_LEFT = const(14) class Text(ui.Widget): @@ -19,7 +19,7 @@ class Text(ui.Widget): style = ui.NORMAL fg = ui.FG bg = ui.BG - ui.header(self.header_text, self.header_icon, ui.GREEN, ui.BG) + ui.header(self.header_text, self.header_icon, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON) for item in self.content: if isinstance(item, str): diff --git a/src/trezor/ui/word_select.py b/src/trezor/ui/word_select.py new file mode 100644 index 00000000..723dbb6e --- /dev/null +++ b/src/trezor/ui/word_select.py @@ -0,0 +1,53 @@ +from micropython import const +from trezor import loop +from trezor import ui, res +from trezor.ui import Widget +from trezor.ui.button import Button, BTN_CLICKED, BTN_STARTED, BTN_ACTIVE + +W12 = '12' +W15 = '15' +W18 = '18' +W24 = '24' + +class WordSelector(Widget): + + def __init__(self, content): + self.content = content + self.w12 = Button((6, 135, 114, 51), W12, + normal_style=ui.BTN_KEY, + active_style=ui.BTN_KEY_ACTIVE) + self.w15 = Button((120, 135, 114, 51), W15, + normal_style=ui.BTN_KEY, + active_style=ui.BTN_KEY_ACTIVE) + self.w18 = Button((6, 186, 114, 51), W18, + normal_style=ui.BTN_KEY, + active_style=ui.BTN_KEY_ACTIVE) + self.w24 = Button((120, 186, 114, 51), W24, + normal_style=ui.BTN_KEY, + active_style=ui.BTN_KEY_ACTIVE) + + def render(self): + self.w12.render() + self.w15.render() + self.w18.render() + self.w24.render() + + def touch(self, event, pos): + if self.w12.touch(event, pos) == BTN_CLICKED: + return W12 + if self.w15.touch(event, pos) == BTN_CLICKED: + return W15 + if self.w18.touch(event, pos) == BTN_CLICKED: + return W18 + if self.w24.touch(event, pos) == BTN_CLICKED: + return W24 + + async def __iter__(self): + return await loop.wait(super().__iter__(), self.content) + + +_STARTED = const(-1) +_STOPPED = const(-2) + + +