trezor-core/src/trezor/ui/passphrase.py

192 lines
6.3 KiB
Python

from micropython import const
from trezor import io, loop, ui, res
from trezor.ui import display
from trezor.ui.button import BTN_CLICKED, Button
from trezor.ui.swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe
SPACE = res.load(ui.ICON_SPACE)
KEYBOARD_KEYS = (
('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'),
(SPACE, 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz', '*#'),
(SPACE, 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '*#'),
('_', '.', '/', '!', '+', '-', '?', ',', ';', '$'))
def digit_area(i):
if i == 9: # 0-position
i = 10 # display it in the middle
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)
padding = 12
page_count = len(KEYBOARD_KEYS)
if page_count * padding > bbox:
padding = bbox // page_count
x = (bbox // 2) - (page_count // 2) * padding
y = 44
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)
class Input(Button):
def __init__(self, area: tuple, content: str=''):
super().__init__(area, content)
self.pending = False
self.disable()
def edit(self, content: str, pending: bool):
self.content = content
self.pending = pending
self.taint()
def render_content(self, s, ax, ay, aw, ah):
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
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
maxlen = const(14) # maximum text length
# input content
if len(t) > maxlen:
t = '<' + t[-maxlen:] # too long, align to the right
width = display.text_width(t, text_style)
display.text(tx, ty, t, text_style, fg_color, bg_color)
if p: # pending marker
pw = display.text_width(t[-1:], text_style)
display.bar(tx + width - pw, ty + 2, pw + 1, 3, fg_color)
else: # cursor
display.bar(tx + width + 1, ty - 18, 2, 22, fg_color)
CANCELLED = const(0)
class PassphraseKeyboard(ui.Widget):
def __init__(self, prompt, page=1):
self.prompt = prompt
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 render(self):
# passphrase or prompt
if self.input.content:
self.input.render()
else:
display.bar(0, 0, 240, 48, ui.BG)
display.text_center(ui.WIDTH // 2, 32, self.prompt, ui.BOLD, ui.GREY, ui.BG)
render_scrollbar(self.page)
# buttons
self.back.render()
self.done.render()
for btn in self.keys:
btn.render()
def touch(self, event, pos):
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
index = 0
self.pbutton = button
self.pindex = index
self.input.edit(content, button is not None)
if content:
self.back.enable()
else:
self.back.disable()
async def __iter__(self):
self.edit(self.input.content) # init button state
while True:
change = self.change_page()
enter = self.enter_text()
wait = loop.wait(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)
touch = loop.select(io.TOUCH)
wait_timeout = loop.wait(touch, timeout)
wait_touch = loop.wait(touch)
content = None
while content is None:
self.render()
if self.pbutton is not None:
wait = wait_timeout
else:
wait = wait_touch
result = await wait
if touch in wait.finished:
event, *pos = result
content = self.touch(event, pos)
else:
# disable the pending buttons
self.edit(self.input.content)
return content
async def change_page(self):
swipe = await Swipe(directions=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])