Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/deploy_to_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def deploy(arch=""):
deploy_py_files(Path("src/tempe/colors"), ":/lib/tempe/colors", arch=arch)
deploy_py_files(Path("src/tempe/fonts"), ":/lib/tempe/fonts", arch=arch)
deploy_py_files(Path("src/tempe/colormaps"), ":/lib/tempe/colormaps", arch=arch)
deploy_py_files(Path("src/tempe_components"), ":/lib/tempe_components", arch=arch)
deploy_py_files(Path("src/tempe_displays"), ":/lib/tempe_displays", arch=arch)
deploy_py_files(
Path("src/tempe_displays/st7789"), ":/lib/tempe_displays/st7789", arch=arch
Expand Down
4 changes: 2 additions & 2 deletions ci/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ def test():
"""Run unit tests in micropython"""
print("Running Tests")
failures = []
test_dir = Path("tests/tempe")
test_dir = Path("tests")
os.environ["MICROPYPATH"] = "src:" + os.environ.get(
"MICROPYPATH", ":examples:.frozen:~/.micropython/lib:/usr/lib/micropython"
)
for path in sorted(test_dir.glob("test_*.py")):
for path in sorted(test_dir.glob("*/test_*.py")):
print(path.name, "... ", end="", flush=True)
result = run_test(path)
if result:
Expand Down
82 changes: 82 additions & 0 deletions examples/component_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: 2024-present Unital Software <info@unital.dev>
#
# SPDX-License-Identifier: MIT

"""Example showing basic display of text."""

import time
import asyncio
import gc

from tempe.colors import grey_6, grey_a
from tempe.surface import Surface, BACKGROUND, DRAWING
from tempe.text import Text
from tempe.shapes import Rectangles
from tempe.font import TempeFont
from tempe.fonts import ubuntu16
from tempe_components.component import Box, Component, component_style
from tempe_components.label import Label, label_style
from tempe_components.style import Style, StateColor

# maximize available memory before allocating buffer
gc.collect()

# A buffer one half the size of a 320x240 screen
# NOTE: If you get MemoryErrors, make this smaller
working_buffer = bytearray(2 * 320 * 121)


# create the surface
surface = Surface()

# fill the background with white pixels
bounds = (0, 0, 320, 240)

background = Component(
surface=surface,
style=component_style,
box=Box(x=0, y=0, content_width=320, content_height=240),
)

label = Label(
surface=surface,
style=Style(
parent=label_style,
font=TempeFont(ubuntu16),
shadow=StateColor(enabled=grey_a),
border=StateColor(enabled=grey_6),
),
box=Box(x=100, y=100, content_width=100, content_height=50, pad_top=4, pad_left=4, pad_bottom=4, pad_right=4),
text="Hello Tempe\nComponents",
)

def main(display=None):
"""Render the surface and return the display object."""
if display is None:
try:
from tempe_config import init_display

display = asyncio.run(init_display())
except ImportError:
print(
"Could not find tempe_config.init_display.\n\n"
"To run examples, you must create a top-level tempe_config module containing\n"
"an async init_display function that returns a display.\n\n"
"See https://unital.github.io/tempe more information.\n\n"
"Defaulting to file-based display.\n"
)
from tempe.display import FileDisplay

display = FileDisplay("component_example.rgb565", (320, 240))
with display:
display.clear()
surface.refresh(display, working_buffer)

start = time.ticks_us()
surface.refresh(display, working_buffer)
print(time.ticks_diff(time.ticks_us(), start))
return display


if __name__ == '__main__':
display = main()
2 changes: 2 additions & 0 deletions src/tempe/colors/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def normalize_color(color):
return rgb_to_rgb565(*color)
else:
return rgb24_to_rgb565(*color)
elif color is None:
return None
else:
raise ValueError(f"Unknown color {color!r}")

Expand Down
6 changes: 5 additions & 1 deletion src/tempe/colors/convert.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

"""Color conversion routines."""

from typing import overload
from collections.abc import Iterable
from .types import color, rgb, rgb565


@overload
def normalize_color(color: color) -> rgb565: ...

@overload
def normalize_color(color: None) -> None: ...

def rgb_sequence_to_rgb565(colors: Iterable[rgb]) -> list[rgb565]: ...

def rgb444_to_rgb565(r: int, g: int, b: int, big_endian: bool = True) -> rgb565: ...
Expand Down
20 changes: 15 additions & 5 deletions src/tempe/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,14 @@ class RoundedRectangles(Rectangles):
Geometry should produce x, y, w, h arrays.
"""

def __init__(self, geometry, colors, *, radius=4, fill=True, surface=None, clip=None):
def __init__(self, geometry, colors, *, radius=4, fill=True, fill_center=True, surface=None, clip=None):
super().__init__(geometry, colors, fill=fill, surface=surface, clip=clip)
self.radius = radius
self.fill_center = fill_center

def draw(self, buffer, x=0, y=0):
fill = self.fill
fill_center = self.fill_center
for rect, color in self:
px = rect[0] - x
py = rect[1] - y
Expand All @@ -333,9 +335,15 @@ def draw(self, buffer, x=0, y=0):
h -= 1
r = min(self.radius, w // 2, h // 2)
if fill:
buffer.rect(px + r, py, w - 2*r, h + 1, color, fill)
buffer.rect(px, py + r, r, h - 2*r, color, fill)
buffer.rect(px + w - r, py + r, r + 1, h - 2*r, color, fill)
if fill_center:
buffer.rect(px + r, py, w - 2*r, h + 1, color, fill)
buffer.rect(px, py + r, r, h - 2*r, color, fill)
buffer.rect(px + w - r, py + r, r + 1, h - 2*r, color, fill)
else:
buffer.rect(px + r, py, w - 2*r, r, color, fill)
buffer.rect(px + r, py + h - r + 1 , w - 2*r, r, color, fill)
buffer.rect(px, py + r, r, h - 2*r, color, fill)
buffer.rect(px + w - r + 1, py + r, r, h - 2*r, color, fill)
else:
buffer.hline(px + r, py, w - 2*r, color)
buffer.hline(px + r, py + h, w - 2*r, color)
Expand All @@ -353,9 +361,11 @@ def draw(self, buffer, x=0, y=0):
buffer.ellipse(px + r, py + h - r, r, r, color, fill, 4)
buffer.ellipse(px + r, py + r, r, r, color, fill, 2)

def update(self, geometry=None, colors=None, fill=None, radius=None):
def update(self, geometry=None, colors=None, fill=None, radius=None, fill_center=None):
if radius is not None:
self.radius = radius
if fill_center is not None:
self.fill_center = fill_center
super().update(geometry=geometry, colors=colors, fill=fill)


Expand Down
4 changes: 4 additions & 0 deletions src/tempe/shapes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ class RoundedRectangles(Rectangles):
*,
radius: int = 4,
fill: bool = True,
fill_center: bool = True,
surface: "tempe.surface.Surface | None" = None,
clip: rectangle | None = None,
): ...
Expand All @@ -387,6 +388,7 @@ class RoundedRectangles(Rectangles):
geometry = Iterable[rectangle] | None,
colors = Iterable[rgb565] | None,
fill = bool | None,
fill_center: bool = True,
radius = int | None
) -> None:
"""Update the state of the Shape, marking a redraw as needed.
Expand All @@ -399,6 +401,8 @@ class RoundedRectangles(Rectangles):
The sequence of colors for each geometry.
fill : bool | None
Whether to fill the shape or to draw the outline.
fill : bool | None
Whether to fill the central rectangle (only applies when fill=True).
radius : int | None
The corner radii for the rectangles.
"""
Expand Down
Empty file.
168 changes: 168 additions & 0 deletions src/tempe_components/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# SPDX-FileCopyrightText: 2024-present Unital Software <info@unital.dev>
#
# SPDX-License-Identifier: MIT

import asyncio

from tempe.colors import grey_e, grey_a
from tempe.shapes import RoundedRectangles
from tempe.surface import Surface, BACKGROUND, OVERLAY

from .observable import Observable, Field, undefined
from .style import Style, StateColor, ENABLED


component_style = Style(
parent=None,
background=StateColor(enabled=grey_e)
)


class Box(Observable):
x: int = Field(cls=int)
y: int = Field(cls=int)
content_width: int = Field(cls=int)
content_height: int = Field(cls=int)
pad_top: int = Field(1, cls=int)
pad_bottom: int = Field(1, cls=int)
pad_left: int = Field(1, cls=int)
pad_right: int = Field(1, cls=int)
margin_top: int = Field(0, cls=int)
margin_bottom: int = Field(0, cls=int)
margin_left: int = Field(0, cls=int)
margin_right: int = Field(0, cls=int)

@property
def bounds(self):
return (
self.x + self.margin_left,
self.y + self.margin_top,
self.content_width + self.pad_left + self.pad_right,
self.content_height + self.pad_top + self.pad_bottom,
)

@property
def shadow_bounds(self):
return (
self.x + self.margin_left - 1,
self.y + self.margin_top + 1,
self.content_width + self.pad_left + self.pad_right + 2,
self.content_height + self.pad_top + self.pad_bottom,
)

@property
def content_bounds(self):
return (
self.x + self.margin_left + self.pad_left,
self.y + self.margin_top + self.pad_top,
self.content_width,
self.content_height,
)


class Component(Observable):

surface: "Surface"

box: Box

style = Field(component_style, cls=Style)

state = Field(ENABLED, cls=str)

_background_shape = None
_shadow_shape = None
_border_shape = None

def __init__(self, surface, **kwargs):
self.surface = surface
super().__init__(**kwargs)

def _update_background_shape(self):
if self.style.background is None:
background = None
else:
background = self.style.background[self.state]
if background is None:
if self._background_shape is not None:
if self._shadow_shape is not None:
self.surface.remove_shape(BACKGROUND, self._shadow_shape)
self.surface.remove_shape(BACKGROUND, self._background_shape)
self._background_shape = None
elif self._background_shape is None:
if self.style.shadow is not None:
self._shadow_shape = RoundedRectangles(
[self.box.shadow_bounds],
[self.style.shadow[self.state]],
radius=self.style.radius + 1,
fill=True,
fill_center=False,
)
self.surface.add_shape(BACKGROUND, self._shadow_shape)
self._background_shape = RoundedRectangles(
[self.box.bounds],
[background],
radius=self.style.radius,
fill=True,
)
self.surface.add_shape(BACKGROUND, self._background_shape)
else:
if self.style.shadow is None:
if self._shadow_shape is not None:
self.surface.remove_shape(BACKGROUND, self._shadow_shape)
self._shadow_shape = None
elif self._shadow_shape is None:
self._shadow_shape = RoundedRectangles(
[self.box.shadow_bounds],
[self.style.shadow[self.state]],
radius=self.style.radius + 1,
fill=True,
fill_center=False,
)
# Ensure proper layering
self.surface.remove_shape(BACKGROUND, self._background_shape)
self.surface.add_shape(BACKGROUND, self._shadow_shape)
self.surface.add_shape(BACKGROUND, self._background_shape)
else:
self._shadow_shape.update(
geometry=[self.box.shadow_bounds],
colors=[self.style.shadow[self.state]],
radius=self.style.radius,
)

self._background_shape.update(
geometry=[self.box.bounds],
colors=[background],
radius=self.style.radius,
)

def _update_border_shape(self):
if self.style.border is None:
border = None
else:
border = self.style.border[self.state]
if border is None:
if self._border_shape is not None:
self.surface.remove_shape(OVERLAY, self._border_shape)
self._border_shape = None
elif self._border_shape is None:
self._border_shape = RoundedRectangles(
[self.box.bounds],
[border],
radius=self.style.radius,
fill=False,
)
self.surface.add_shape(OVERLAY, self._border_shape)
else:
self._border_shape.update(
geometry=[self.box.bounds],
colors=[border],
radius=self.style.radius,
)

def update(
self, update=None, **kwargs
):
super().update(update, **kwargs)
self._update_background_shape()
self._update_border_shape()
Loading
Loading