1
0
Fork 0

Added basic theming engine.

This theming engine uses a bytestring (but supports anything indexable,
as long as the index results are a byte long),
stored as `wasp.system._theme`.
It has a default value, which should not change anything about the way this looks currently.

The theme can be set via `wasp.system.set_theme`,
but this should *ONLY* be used in `main.py`.
`wasp.system.set_theme` will return True if it was successful,
or False if the theme is of an old format.
Using an old format theme will *not* crash the watch,
but will use the default theme instead.

To theme this, one has to use tools/themer.py (use flag -h for complete explanation)
to generate a bytestring that's added in main.py (see diff).

The bytestring is then loaded into 'wasp.system._theme'.
Theme values can be looked up by apps by using `wasp.system.theme("theme-key")`.
Theme keys appear in the function body of `wasp.system.theme()`.

I've took the liberty of converting existing apps to use this method,
and it seems to work well.

A test theme is provided in `tools/test_theme.py`

Signed-off-by: kozova1 <mug66kk@gmail.com>
This commit is contained in:
kozova1 2020-12-05 20:27:55 +02:00 committed by Daniel Thompson
parent 784c9bb36d
commit 2624a6e998
5 changed files with 162 additions and 23 deletions

15
tools/test_theme.py Normal file
View file

@ -0,0 +1,15 @@
from themer import DefaultTheme
class Theme(DefaultTheme):
# These colors were chosen specifically because they're hard to miss.
# Using this theme on an actual device is not advised
# The default theme was generated by removing all the lines below and adding `pass` instead.
BLE_COLOR = 0xfb80
SCROLL_INDICATOR_COLOR = 0xf800
BATTERY_CHARGING_COLOR = 0x07ff
SMALL_CLOCK_COLOR = 0x599f
NOTIFICATION_COLOR = 0x8fe0
ACCENT_MID = 0xf800
ACCENT_LO = 0x001f
ACCENT_HI = 0x07e0
SLIDER_DEFAULT_COLOR = 0x7777

81
tools/themer.py Executable file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Compiles themes for wasp-os"""
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from typing import Tuple
class DefaultTheme():
"""This represents the default theme.
Import this file and extend the Theme class, only changing the variables.
Export the resulting class as 'Theme'.
serialize() should NEVER be overriden!
"""
BLE_COLOR = 0x7bef
SCROLL_INDICATOR_COLOR = 0x7bef
BATTERY_CHARGING_COLOR = 0x7bef
SMALL_CLOCK_COLOR = 0xe73c
NOTIFICATION_COLOR = 0x7bef
ACCENT_MID = 0xb5b6
ACCENT_LO = 0xbdb6
ACCENT_HI = 0xffff
SLIDER_DEFAULT_COLOR = 0x39ff
def serialize(self) -> bytes:
"""Serializes the theme for use in wasp-os"""
def split_bytes(x: int) -> Tuple[int, int]:
return (x & 0xFF, (x >> 8) & 0xFF)
theme_bytes = bytes([
*split_bytes(self.BLE_COLOR),
*split_bytes(self.SCROLL_INDICATOR_COLOR),
*split_bytes(self.BATTERY_CHARGING_COLOR),
*split_bytes(self.SMALL_CLOCK_COLOR),
*split_bytes(self.NOTIFICATION_COLOR),
*split_bytes(self.ACCENT_MID),
*split_bytes(self.ACCENT_LO),
*split_bytes(self.ACCENT_HI),
*split_bytes(self.SLIDER_DEFAULT_COLOR),
])
return theme_bytes
if __name__ == '__main__':
parser = ArgumentParser(
description='''Compiles themes into a format understood by wasp-os.
The resulting string should be put in main.py like this:
theme_string = THEME_STRING_GOES_HERE
for the theme to take effect.
''',
epilog=''' To create a theme,
import this file and extend the DefaultTheme class, only changing the variables.
Export the resulting class as 'Theme'.
Example:
--------
theme.py:
from themer import DefaultTheme
class Theme(DefaultTheme):
BLE_ICON_COLOR = 0x041F
shell:
$ ./themer.py theme # NOTE: do not include .py at end of file!
> b'\xef{\xef{\xef{<\xe7\xef{\xb6\xb5\xb6\xbd\xff\xff\xff9'
main.py:
...
wasp.system.set_theme(b'\xef{\xef{\xef{<\xe7\xef{\xb6\xb5\xb6\xbd\xff\xff\xff9')
...
''',
formatter_class=RawTextHelpFormatter
)
parser.add_argument('input_file', type=str, nargs=1)
args = parser.parse_args()
theme = DefaultTheme()
theme = import_module(args.input_file[0]).Theme()
print(theme.serialize())

View file

@ -76,7 +76,8 @@ class ClockApp():
# Clear the display and draw that static parts of the watch face # Clear the display and draw that static parts of the watch face
draw.fill() draw.fill()
draw.rleblit(digits.clock_colon, pos=(2*48, 80), fg=0xb5b6) draw.rleblit(digits.clock_colon, pos=(2*48, 80),
fg=wasp.system.theme('accent-mid'))
# Redraw the status bar # Redraw the status bar
wasp.system.bar.draw() wasp.system.bar.draw()
@ -95,10 +96,14 @@ class ClockApp():
month = MONTH[month*3:(month+1)*3] month = MONTH[month*3:(month+1)*3]
# Draw the changeable parts of the watch face # Draw the changeable parts of the watch face
draw.rleblit(DIGITS[now[4] % 10], pos=(4*48, 80)) draw.rleblit(DIGITS[now[4] % 10], pos=(4*48, 80),
draw.rleblit(DIGITS[now[4] // 10], pos=(3*48, 80), fg=0xbdb6) fg=wasp.system.theme('accent-hi'))
draw.rleblit(DIGITS[now[3] % 10], pos=(1*48, 80)) draw.rleblit(DIGITS[now[4] // 10], pos=(3*48, 80),
draw.rleblit(DIGITS[now[3] // 10], pos=(0*48, 80), fg=0xbdb6) fg=wasp.system.theme('accent-lo'))
draw.rleblit(DIGITS[now[3] % 10], pos=(1*48, 80),
fg=wasp.system.theme('accent-hi'))
draw.rleblit(DIGITS[now[3] // 10], pos=(0*48, 80),
fg=wasp.system.theme('accent-lo'))
draw.string('{} {} {}'.format(now[2], month, now[0]), draw.string('{} {} {}'.format(now[2], month, now[0]),
0, 180, width=240) 0, 180, width=240)

View file

@ -14,7 +14,6 @@
wasp.watch is an import of :py:mod:`watch` and is simply provided as a wasp.watch is an import of :py:mod:`watch` and is simply provided as a
shortcut (and to reduce memory by keeping it out of other namespaces). shortcut (and to reduce memory by keeping it out of other namespaces).
""" """
import gc import gc
import machine import machine
import micropython import micropython
@ -118,6 +117,8 @@ class Manager():
self.musicstate = {} self.musicstate = {}
self.musicinfo = {} self.musicinfo = {}
self._theme = b'\xef{\xef{\xef{<\xe7\xef{\xb6\xb5\xb6\xbd\xff\xff\xff9'
self.blank_after = 15 self.blank_after = 15
self._alarms = [] self._alarms = []
@ -514,4 +515,30 @@ class Manager():
self._scheduling = enable self._scheduling = enable
def set_theme(self, new_theme) -> bool:
"""Sets the system theme.
Accepts anything that supports indexing,
and has a len() equivalent to the default theme."""
if len(self._theme) != len(new_theme):
return False
self._theme = new_theme
return True
def theme(self, theme_part: str) -> int:
"""Returns the relevant part of theme. For more see ../tools/themer.py"""
theme_parts = ("ble",
"scroll-indicator",
"battery-charging",
"status-clock",
"notify-icon",
"accent-mid",
"accent-lo",
"accent-hi",
"slider-default")
if theme_part not in theme_parts:
raise IndexError('Theme part {} does not exist'.format(theme_part))
idx = theme_parts.index(theme_part) * 2
return self._theme[idx] | (self._theme[idx+1] << 8)
system = Manager() system = Manager()

View file

@ -39,7 +39,8 @@ class BatteryMeter:
if watch.battery.charging(): if watch.battery.charging():
if self.level != -1: if self.level != -1:
draw.rleblit(icon, pos=(239-icon[0], 0), fg=0x7bef) draw.rleblit(icon, pos=(239-icon[0], 0),
fg=wasp.system.theme('battery-charging'))
self.level = -1 self.level = -1
else: else:
level = watch.battery.level() level = watch.battery.level()
@ -108,7 +109,7 @@ class Clock:
draw = wasp.watch.drawable draw = wasp.watch.drawable
draw.set_font(fonts.sans28) draw.set_font(fonts.sans28)
draw.set_color(0xe73c) draw.set_color(wasp.system.theme('status-clock'))
draw.string(t1, 52, 12, 138) draw.string(t1, 52, 12, 138)
self.on_screen = now self.on_screen = now
@ -137,13 +138,15 @@ class NotificationBar:
(x, y) = self._pos (x, y) = self._pos
if wasp.watch.connected(): if wasp.watch.connected():
draw.blit(icons.blestatus, x, y, fg=0x7bef) draw.blit(icons.blestatus, x, y, fg=wasp.system.theme('ble'))
if wasp.system.notifications: if wasp.system.notifications:
draw.blit(icons.notification, x+22, y, fg=0x7bef) draw.blit(icons.notification, x+22, y,
fg=wasp.system.theme('notify-icon'))
else: else:
draw.fill(0, x+22, y, 30, 32) draw.fill(0, x+22, y, 30, 32)
elif wasp.system.notifications: elif wasp.system.notifications:
draw.blit(icons.notification, x, y, fg=0x7bef) draw.blit(icons.notification, x, y,
fg=wasp.system.theme('notify-icon'))
draw.fill(0, x+30, y, 22, 32) draw.fill(0, x+30, y, 22, 32)
else: else:
draw.fill(0, x, y, 52, 32) draw.fill(0, x, y, 52, 32)
@ -206,10 +209,13 @@ class ScrollIndicator:
def update(self): def update(self):
"""Update from scrolling indicator.""" """Update from scrolling indicator."""
draw = watch.drawable draw = watch.drawable
color = wasp.system.theme('scroll-indicator')
if self.up: if self.up:
draw.rleblit(icons.up_arrow, pos=self._pos, fg=0x7bef) draw.rleblit(icons.up_arrow, pos=self._pos, fg=color)
if self.down: if self.down:
draw.rleblit(icons.down_arrow, pos=(self._pos[0], self._pos[1] + 13), fg=0x7bef) draw.rleblit(icons.down_arrow, pos=(self._pos[0], self._pos[1] + 13),
fg=color)
_SLIDER_KNOB_DIAMETER = const(40) _SLIDER_KNOB_DIAMETER = const(40)
_SLIDER_KNOB_RADIUS = const(_SLIDER_KNOB_DIAMETER // 2) _SLIDER_KNOB_RADIUS = const(_SLIDER_KNOB_DIAMETER // 2)
@ -221,14 +227,25 @@ _SLIDER_TRACK_Y2 = const(_SLIDER_TRACK_Y1 + _SLIDER_TRACK_HEIGHT)
class Slider(): class Slider():
"""A slider to select values.""" """A slider to select values."""
def __init__(self, steps, x=10, y=90, color=0x39ff): def __init__(self, steps, x=10, y=90, color=None):
self.value = 0 self.value = 0
self._steps = steps self._steps = steps
self._stepsize = _SLIDER_TRACK / (steps-1) self._stepsize = _SLIDER_TRACK / (steps-1)
self._x = x self._x = x
self._y = y self._y = y
self._color = color self._color = color
self._lowlight = None
def draw(self):
"""Draw the slider."""
draw = watch.drawable
x = self._x
y = self._y
color = self._color
if self._color is None:
self._color = wasp.system.theme('slider-default')
color = self._color
if self._lowlight is None:
# Automatically generate a lowlight color # Automatically generate a lowlight color
if color < 0b10110_000000_00000: if color < 0b10110_000000_00000:
color = (color | 0b10110_000000_00000) & 0b10110_111111_11111 color = (color | 0b10110_000000_00000) & 0b10110_111111_11111
@ -237,12 +254,6 @@ class Slider():
if (color & 0b11111) < 0b10110: if (color & 0b11111) < 0b10110:
color = (color | 0b11000) & 0b11111_111111_10110 color = (color | 0b11000) & 0b11111_111111_10110
self._lowlight = color self._lowlight = color
def draw(self):
"""Draw the slider."""
draw = watch.drawable
x = self._x
y = self._y
color = self._color color = self._color
light = self._lowlight light = self._lowlight