diff --git a/src/apps/homescreen/layout_homescreen.py b/src/apps/homescreen/layout_homescreen.py index 0c2111b03..b934cc201 100644 --- a/src/apps/homescreen/layout_homescreen.py +++ b/src/apps/homescreen/layout_homescreen.py @@ -1,29 +1,33 @@ from trezor import ui +from trezor.ui import utils as ui_utils +from trezor.ui.swipe import Swipe from trezor import loop +def swipe_to_change_orientation(): + while True: + degrees = yield from Swipe().wait() + ui.display.orientation(degrees) + + def layout_homescreen(): print("Homescreen layout!") - # ui.display.bar(0, 0, 240, 240, ui.WHITE) - f = open('apps/homescreen/trezor.toig', 'rb') def func(foreground): f.seek(0) - ui.display.icon(0, 0, f.read(), foreground, ui.BLACK) - - animation = ui.animate_pulse(func, ui.WHITE, ui.GREY, speed=400000) + ui.display.icon(0, 0, f.read(), foreground, ui_utils.BLACK) + orientation = swipe_to_change_orientation() + animation = ui_utils.animate_pulse(func, ui_utils.WHITE, ui_utils.GREY, speed=400000) timeout = loop.Sleep(5000 * 1000) - yield loop.Wait([animation, timeout]) - print('back to layout') - - # try: - # print(animation.throw(StopIteration())) - # except: - # pass + yield loop.Wait([ + orientation, + animation, + timeout + ]) from apps import playground return playground.layout_tap_to_confirm('1BitkeyP2nDd5oa64x7AjvBbbwST54W5Zmx2', 110.126967, 'BTC') diff --git a/src/apps/playground/__init__.py b/src/apps/playground/__init__.py index bc645e894..fc69c011c 100644 --- a/src/apps/playground/__init__.py +++ b/src/apps/playground/__init__.py @@ -1,222 +1,10 @@ -import utime from trezor import loop from trezor import ui +from trezor.ui.swipe import Swipe +from trezor.ui.pin import PinDialog, PIN_CONFIRMED, PIN_CANCELLED from trezor.utils import unimport_func -def in_area(pos, area): - x, y = pos - ax, ay, aw, ah = area - return ax <= x <= ax + aw and ay <= y <= ay + ah - - -DEFAULT_BUTTON = { - 'bg-color': ui.BLACK, - 'fg-color': ui.WHITE, - 'text-style': ui.NORMAL, - 'border-color': ui.blend(ui.BLACK, ui.WHITE, 0.1), -} -DEFAULT_BUTTON_ACTIVE = { - 'bg-color': ui.GREY, - 'fg-color': ui.BLACK, - 'text-style': ui.BOLD, - 'border-color': ui.GREY, -} - -CANCEL_BUTTON = { - 'bg-color': ui.blend(ui.BLACK, ui.RED, 0.3), - 'fg-color': ui.RED, - 'text-style': ui.NORMAL, - 'border-color': ui.blend(ui.BLACK, ui.RED, 0.6), -} -CANCEL_BUTTON_ACTIVE = { - 'bg-color': ui.RED, - 'fg-color': ui.WHITE, - 'text-style': ui.BOLD, - 'border-color': ui.RED, -} - -CONFIRM_BUTTON = { - 'bg-color': ui.blend(ui.BLACK, ui.GREEN, 0.3), - 'fg-color': ui.GREEN, - 'text-style': ui.NORMAL, - 'border-color': ui.blend(ui.BLACK, ui.GREEN, 0.6), -} -CONFIRM_BUTTON_ACTIVE = { - 'bg-color': ui.GREEN, - 'fg-color': ui.WHITE, - 'text-style': ui.BOLD, - 'border-color': ui.GREEN, -} - - -BTN_CLICKED = const(1) - -BTN_STARTED = const(1) -BTN_ACTIVE = const(2) -BTN_DIRTY = const(4) - - -class Button(): - - def __init__(self, area, text, normal_style=None, active_style=None): - self.area = area - self.text = text - self.normal_style = normal_style or DEFAULT_BUTTON - self.active_style = active_style or DEFAULT_BUTTON_ACTIVE - self.state = BTN_DIRTY - - def render(self): - if not self.state & BTN_DIRTY: - return - state = self.state & ~BTN_DIRTY - style = self.active_style if state & BTN_ACTIVE else self.normal_style - ax, ay, aw, ah = self.area - tx = ax + aw // 2 - ty = ay + ah // 2 + 8 - ui.display.bar(ax, ay, aw, ah, style['border-color']) - ui.display.bar(ax + 1, ay + 1, aw - 2, ah - 2, style['bg-color']) - ui.display.text_center(tx, ty, self.text, - style['text-style'], - style['fg-color'], - style['bg-color']) - self.state = state - - def send(self, event, pos): - if event is loop.TOUCH_START: - if in_area(pos, self.area): - self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE - elif event is loop.TOUCH_MOVE and self.state & BTN_STARTED: - if in_area(pos, self.area): - if not self.state & BTN_ACTIVE: - self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE - else: - if self.state & BTN_ACTIVE: - self.state = BTN_STARTED | BTN_DIRTY - elif event is loop.TOUCH_END and self.state & BTN_STARTED: - self.state = BTN_DIRTY - if in_area(pos, self.area): - return BTN_CLICKED - - -def digit_area(d): - width = const(80) - height = const(60) - x = ((d - 1) % 3) * width - y = ((d - 1) // 3) * height - return (x, y, width, height) - - -PIN_CONFIRMED = const(1) -PIN_CANCELLED = const(2) - - -class PinDialog(): - - def __init__(self, pin=''): - self.pin = pin - self.confirm_button = Button((0, 240 - 60, 120, 60), 'Confirm', - normal_style=CONFIRM_BUTTON, - active_style=CONFIRM_BUTTON_ACTIVE) - self.cancel_button = Button((120, 240 - 60, 120, 60), 'Cancel', - normal_style=CANCEL_BUTTON, - active_style=CANCEL_BUTTON_ACTIVE) - self.pin_buttons = [Button(digit_area(dig), str(dig)) - for dig in range(1, 10)] - - def render(self): - for btn in self.pin_buttons: - btn.render() - self.confirm_button.render() - self.cancel_button.render() - - def send(self, event, pos): - for btn in self.pin_buttons: - if btn.send(event, pos) is BTN_CLICKED: - self.pin += btn.text - if self.confirm_button.send(event, pos) is BTN_CLICKED: - return PIN_CONFIRMED - if self.cancel_button.send(event, pos) is BTN_CLICKED: - return PIN_CANCELLED - - def wait_for_result(self): - while True: - self.render() - event, *pos = yield loop.Select(loop.TOUCH_START, - loop.TOUCH_MOVE, - loop.TOUCH_END) - result = self.send(event, pos) - if result is not None: - return result - - -SWIPE_DISTANCE_THRESHOLD = const(20) # Min pixels in the primary direction -SWIPE_VELOCITY_THRESHOLD = const(200) # Min pixels/second -SWIPE_RATIO_THRESHOLD = const(30) # Max ratio secondary to primary direction in % - -SWIPE_UP = const(180) -SWIPE_DOWN = const(0) -SWIPE_LEFT = const(90) -SWIPE_RIGHT = const(270) - - -class Swipe(): - - def __init__(self, area=None): - self.area = area or (0, 0, 240, 240) - self.start_pos = None - self.start_time = 0 - self.end_pos = None - self.end_time = 0 - - def send(self, event, pos): - - if event is loop.TOUCH_START and in_area(pos, self.area): - self.start_time = utime.time() - self.start_pos = pos - - elif event is loop.TOUCH_END and self.start_pos is not None: - self.end_time = utime.time() - self.end_pos = pos - td = self.end_time - self.start_time - pdx = self.end_pos[0] - self.start_pos[0] - pdy = self.end_pos[1] - self.start_pos[1] - pdxa = abs(pdx) - pdya = abs(pdy) - if pdxa > pdya: - # Horizontal direction - velx = pdx / td - velxa = abs(velx) - ratio = int(pdya / pdxa * 100) if pdxa > 0 else 100 - print('velxa', velxa, 'pdxa', pdxa, 'ratio', ratio) - if (velxa >= SWIPE_VELOCITY_THRESHOLD - and pdxa >= SWIPE_DISTANCE_THRESHOLD - and ratio <= SWIPE_RATIO_THRESHOLD): - return SWIPE_RIGHT if pdx > 0 else SWIPE_LEFT - else: - # Vertical direction - vely = pdy / td - velya = abs(vely) - ratio = int(pdxa / pdya * 100) if pdya > 0 else 100 - print('velya', velya, 'pdya', pdya, 'ratio', ratio) - if (velya >= SWIPE_VELOCITY_THRESHOLD - and pdya >= SWIPE_DISTANCE_THRESHOLD - and ratio <= SWIPE_RATIO_THRESHOLD): - return SWIPE_DOWN if pdy > 0 else SWIPE_UP - # No swipe, reset the state - self.start_pos = None - self.start_time = 0 - self.end_pos = None - self.end_time = 0 - - def wait(self): - while True: - event, *pos = yield loop.Select(loop.TOUCH_START, loop.TOUCH_END) - result = self.send(event, pos) - if result is not None: - return result - - def layout_tap_to_confirm(address, amount, currency): # ui.display.bar(0, 0, 240, 40, ui.GREEN) @@ -239,22 +27,14 @@ def layout_tap_to_confirm(address, amount, currency): # animation = ui.animate_pulse(func, ui.BLACK, ui.GREY, speed=200000) - # pin_dialog = PinDialog() - # pin_result = yield from pin_dialog.wait_for_result() - - # if pin_result is PIN_CONFIRMED: - # print('PIN confirmed:', pin_dialog.pin) - - # elif pin_result is PIN_CANCELLED: - # print('PIN CANCELLED, go home') + pin_dialog = PinDialog() + pin_result = yield from pin_dialog.wait_for_result() - degrees = 0 + if pin_result is PIN_CONFIRMED: + print('PIN confirmed:', pin_dialog.pin) - while True: - ui.display.bar(0, 0, 240, 240, ui.BLACK) - ui.display.text_center(120, 130, 'HELLO WORLD!', ui.NORMAL, ui.WHITE, ui.BLACK) - degrees = yield from Swipe().wait() - ui.display.orientation(degrees) + elif pin_result is PIN_CANCELLED: + print('PIN CANCELLED, go home') @unimport_func diff --git a/src/trezor/ui/__init__.py b/src/trezor/ui/__init__.py new file mode 100644 index 000000000..88a3ff5bf --- /dev/null +++ b/src/trezor/ui/__init__.py @@ -0,0 +1,3 @@ +from TrezorUi import Display + +display = Display() diff --git a/src/trezor/ui/button.py b/src/trezor/ui/button.py new file mode 100644 index 000000000..e0721ac12 --- /dev/null +++ b/src/trezor/ui/button.py @@ -0,0 +1,94 @@ +from . import utils +from . import display +from .utils import in_area +from trezor import loop + + +DEFAULT_BUTTON = { + 'bg-color': utils.BLACK, + 'fg-color': utils.WHITE, + 'text-style': utils.NORMAL, + 'border-color': utils.blend(utils.BLACK, utils.WHITE, 0.1), +} +DEFAULT_BUTTON_ACTIVE = { + 'bg-color': utils.GREY, + 'fg-color': utils.BLACK, + 'text-style': utils.BOLD, + 'border-color': utils.GREY, +} + +CANCEL_BUTTON = { + 'bg-color': utils.blend(utils.BLACK, utils.RED, 0.3), + 'fg-color': utils.RED, + 'text-style': utils.NORMAL, + 'border-color': utils.blend(utils.BLACK, utils.RED, 0.6), +} +CANCEL_BUTTON_ACTIVE = { + 'bg-color': utils.RED, + 'fg-color': utils.WHITE, + 'text-style': utils.BOLD, + 'border-color': utils.RED, +} + +CONFIRM_BUTTON = { + 'bg-color': utils.blend(utils.BLACK, utils.GREEN, 0.3), + 'fg-color': utils.GREEN, + 'text-style': utils.NORMAL, + 'border-color': utils.blend(utils.BLACK, utils.GREEN, 0.6), +} +CONFIRM_BUTTON_ACTIVE = { + 'bg-color': utils.GREEN, + 'fg-color': utils.WHITE, + 'text-style': utils.BOLD, + 'border-color': utils.GREEN, +} + +BTN_CLICKED = const(1) + +BTN_STARTED = const(1) +BTN_ACTIVE = const(2) +BTN_DIRTY = const(4) + + +class Button(): + + + + def __init__(self, area, text, normal_style=None, active_style=None): + self.area = area + self.text = text + self.normal_style = normal_style or DEFAULT_BUTTON + self.active_style = active_style or DEFAULT_BUTTON_ACTIVE + self.state = BTN_DIRTY + + def render(self): + if not self.state & BTN_DIRTY: + return + state = self.state & ~BTN_DIRTY + style = self.active_style if state & BTN_ACTIVE else self.normal_style + ax, ay, aw, ah = self.area + tx = ax + aw // 2 + ty = ay + ah // 2 + 8 + display.bar(ax, ay, aw, ah, style['border-color']) + display.bar(ax + 1, ay + 1, aw - 2, ah - 2, style['bg-color']) + display.text_center(tx, ty, self.text, + style['text-style'], + style['fg-color'], + style['bg-color']) + self.state = state + + def send(self, event, pos): + if event is loop.TOUCH_START: + if in_area(pos, self.area): + self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE + elif event is loop.TOUCH_MOVE and self.state & BTN_STARTED: + if in_area(pos, self.area): + if not self.state & BTN_ACTIVE: + self.state = BTN_STARTED | BTN_DIRTY | BTN_ACTIVE + else: + if self.state & BTN_ACTIVE: + self.state = BTN_STARTED | BTN_DIRTY + elif event is loop.TOUCH_END and self.state & BTN_STARTED: + self.state = BTN_DIRTY + if in_area(pos, self.area): + return BTN_CLICKED diff --git a/src/trezor/ui/pin.py b/src/trezor/ui/pin.py new file mode 100644 index 000000000..48ecb8200 --- /dev/null +++ b/src/trezor/ui/pin.py @@ -0,0 +1,53 @@ +from . import button +from trezor import loop + + +def digit_area(d): + width = const(80) + height = const(60) + x = ((d - 1) % 3) * width + y = ((d - 1) // 3) * height + return (x, y, width, height) + + +PIN_CONFIRMED = const(1) +PIN_CANCELLED = const(2) + + +class PinDialog(): + + def __init__(self, pin=''): + self.pin = pin + self.confirm_button = button.Button((0, 240 - 60, 120, 60), 'Confirm', + normal_style=button.CONFIRM_BUTTON, + active_style=button.CONFIRM_BUTTON_ACTIVE) + self.cancel_button = button.Button((120, 240 - 60, 120, 60), 'Cancel', + normal_style=button.CANCEL_BUTTON, + active_style=button.CANCEL_BUTTON_ACTIVE) + self.pin_buttons = [button.Button(digit_area(dig), str(dig)) + for dig in range(1, 10)] + + def render(self): + for btn in self.pin_buttons: + btn.render() + self.confirm_button.render() + self.cancel_button.render() + + def send(self, event, pos): + for btn in self.pin_buttons: + if btn.send(event, pos) is button.BTN_CLICKED: + self.pin += btn.text + if self.confirm_button.send(event, pos) is button.BTN_CLICKED: + return PIN_CONFIRMED + if self.cancel_button.send(event, pos) is button.BTN_CLICKED: + return PIN_CANCELLED + + def wait_for_result(self): + while True: + self.render() + event, *pos = yield loop.Select(loop.TOUCH_START, + loop.TOUCH_MOVE, + loop.TOUCH_END) + result = self.send(event, pos) + if result is not None: + return result diff --git a/src/trezor/ui/swipe.py b/src/trezor/ui/swipe.py new file mode 100644 index 000000000..eb2bda8ca --- /dev/null +++ b/src/trezor/ui/swipe.py @@ -0,0 +1,67 @@ +import utime + +from .utils import in_area +from trezor import loop + + +SWIPE_DISTANCE_THRESHOLD = const(20) # Min pixels in the primary direction +SWIPE_VELOCITY_THRESHOLD = const(200) # Min pixels/second +SWIPE_RATIO_THRESHOLD = const(30) # Max ratio secondary to primary direction in % + +SWIPE_UP = const(180) +SWIPE_DOWN = const(0) +SWIPE_LEFT = const(90) +SWIPE_RIGHT = const(270) + + +class Swipe(): + + def __init__(self, area=None): + self.area = area or (0, 0, 240, 240) + self.start_pos = None + self.start_time = 0 + self.end_pos = None + self.end_time = 0 + + def send(self, event, pos): + + if event is loop.TOUCH_START and in_area(pos, self.area): + self.start_time = utime.time() + self.start_pos = pos + + elif event is loop.TOUCH_END and self.start_pos is not None: + self.end_time = utime.time() + self.end_pos = pos + td = self.end_time - self.start_time + pdx = self.end_pos[0] - self.start_pos[0] + pdy = self.end_pos[1] - self.start_pos[1] + pdxa = abs(pdx) + pdya = abs(pdy) + if pdxa > pdya: + # Horizontal direction + velxa = abs(pdx / td) + ratio = int(pdya / pdxa * 100) if pdxa > 0 else 100 + if (velxa >= SWIPE_VELOCITY_THRESHOLD + and pdxa >= SWIPE_DISTANCE_THRESHOLD + and ratio <= SWIPE_RATIO_THRESHOLD): + return SWIPE_RIGHT if pdx > 0 else SWIPE_LEFT + else: + # Vertical direction + velya = abs(pdy / td) + ratio = int(pdxa / pdya * 100) if pdya > 0 else 100 + if (velya >= SWIPE_VELOCITY_THRESHOLD + and pdya >= SWIPE_DISTANCE_THRESHOLD + and ratio <= SWIPE_RATIO_THRESHOLD): + return SWIPE_DOWN if pdy > 0 else SWIPE_UP + # No swipe, reset the state + self.start_pos = None + self.start_time = 0 + self.end_pos = None + self.end_time = 0 + + def wait(self): + while True: + event, *pos = yield loop.Select(loop.TOUCH_START, loop.TOUCH_END) + result = self.send(event, pos) + if result is not None: + return result diff --git a/src/trezor/ui.py b/src/trezor/ui/utils.py similarity index 91% rename from src/trezor/ui.py rename to src/trezor/ui/utils.py index 250592d31..706aab06b 100644 --- a/src/trezor/ui.py +++ b/src/trezor/ui/utils.py @@ -1,15 +1,13 @@ import math import utime -from TrezorUi import Display +from trezor import loop -from . import loop - -display = Display() def rgbcolor(r: int, g: int, b: int) -> int: return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3) + RED = rgbcolor(0xF4, 0x43, 0x36) PINK = rgbcolor(0xE9, 0x1E, 0x63) PURPLE = rgbcolor(0x9C, 0x27, 0xB0) @@ -36,6 +34,13 @@ MONO = const(0) NORMAL = const(1) BOLD = const(2) + +def in_area(pos, area): + x, y = pos + ax, ay, aw, ah = area + return ax <= x <= ax + aw and ay <= y <= ay + ah + + def lerpi(a: int, b: int, t: float) -> int: return int(a + t * (b - a))