You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/src/trezor/ui/components/tt/pin.py

184 lines
5.5 KiB

from micropython import const
from typing import TYPE_CHECKING
from trezor import config, res, ui
from trezor.crypto import random
from trezor.ui import display
from .button import Button, ButtonCancel, ButtonClear, ButtonConfirm, ButtonMono
if TYPE_CHECKING:
from trezor import loop
from typing import Iterable
def digit_area(i: int) -> ui.Area:
if i == 9: # 0-position
i = 10 # display it in the middle
return ui.grid(i + 3) # skip the first line
def generate_digits() -> Iterable[int]:
digits = list(range(0, 10)) # 0-9
random.shuffle(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 PinInput(ui.Component):
def __init__(self, prompt: str, subprompt: str | None, pin: str) -> None:
super().__init__()
self.prompt = prompt
self.subprompt = subprompt
self.pin = pin
def on_render(self) -> None:
if self.repaint:
if self.pin:
self.render_pin()
else:
self.render_prompt()
self.repaint = False
def render_pin(self) -> None:
MAX_LENGTH = const(14) # maximum length of displayed PIN
CONTD_MARK = "<"
BOX_WIDTH = const(240)
DOT_SIZE = const(10)
PADDING = const(4)
RENDER_Y = const(20)
TWITCH = const(3)
display.bar(0, 0, ui.WIDTH, 50, ui.BG)
if len(self.pin) > MAX_LENGTH:
contd_width = display.text_width(CONTD_MARK, ui.BOLD) + PADDING
twitch = TWITCH * (len(self.pin) % 2)
else:
contd_width = 0
twitch = 0
count = min(len(self.pin), MAX_LENGTH)
render_x = (BOX_WIDTH - count * (DOT_SIZE + PADDING) - contd_width) // 2
if contd_width:
display.text(
render_x, RENDER_Y + DOT_SIZE, CONTD_MARK, ui.BOLD, ui.GREY, ui.BG
)
for i in range(0, count):
display.bar_radius(
render_x + contd_width + twitch + i * (DOT_SIZE + PADDING),
RENDER_Y,
DOT_SIZE,
DOT_SIZE,
ui.GREY,
ui.BG,
4,
)
def render_prompt(self) -> None:
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)
class PinButton(Button):
def __init__(self, index: int, digit: int, dialog: "PinDialog"):
self.dialog = dialog
super().__init__(digit_area(index), str(digit), ButtonMono)
def on_click(self) -> None:
self.dialog.assign(self.dialog.input.pin + self.text)
CANCELLED = object()
class PinDialog(ui.Layout):
def __init__(
self,
prompt: str,
subprompt: str | None,
allow_cancel: bool = True,
maxlength: int = 50,
) -> None:
super().__init__()
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
self.confirm_button.disable()
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_CANCEL if config.is_unlocked() else ui.ICON_LOCK
)
self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel)
self.cancel_button.on_click = self.on_cancel
else:
self.cancel_button = Button(ui.grid(12), "")
self.cancel_button.disable()
self.pin_buttons = [
PinButton(i, d, self) for i, d in enumerate(generate_digits())
]
def dispatch(self, event: int, x: int, y: int) -> None:
self.input.dispatch(event, x, y)
if self.input.pin:
self.reset_button.dispatch(event, x, y)
else:
self.cancel_button.dispatch(event, x, y)
self.confirm_button.dispatch(event, x, y)
for btn in self.pin_buttons:
btn.dispatch(event, x, y)
def assign(self, pin: str) -> None:
if len(pin) > self.maxlength:
return
for btn in self.pin_buttons:
if len(pin) < self.maxlength:
btn.enable()
else:
btn.disable()
if pin:
self.confirm_button.enable()
self.reset_button.enable()
self.cancel_button.disable()
else:
self.confirm_button.disable()
self.reset_button.disable()
self.cancel_button.enable()
self.input.pin = pin
self.input.repaint = True
def on_reset(self) -> None:
self.assign("")
def on_cancel(self) -> None:
raise ui.Result(CANCELLED)
def on_confirm(self) -> None:
if self.input.pin:
raise ui.Result(self.input.pin)
if __debug__:
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
from apps.debug import input_signal
return super().create_tasks() + (input_signal(),)