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:
parent
784c9bb36d
commit
2624a6e998
5 changed files with 162 additions and 23 deletions
15
tools/test_theme.py
Normal file
15
tools/test_theme.py
Normal 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
81
tools/themer.py
Executable 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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
29
wasp/wasp.py
29
wasp/wasp.py
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue