2026-10-15 19:43:59 +02:00
|
|
|
#include "logic/page_render.h"
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
#include <stdio.h>
|
2026-10-15 19:43:59 +02:00
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
#include "logic/bitmap_format.h"
|
|
|
|
|
#include "logic/builtin_fonts.h"
|
|
|
|
|
|
|
|
|
|
static Font usableFont(PageResources &resources, const char *name)
|
|
|
|
|
{
|
|
|
|
|
Font font = resources.font(name);
|
|
|
|
|
if (validFont(font.data, font.size) && font.scale >= 1)
|
|
|
|
|
return font;
|
|
|
|
|
return builtinFont(*findBuiltinFont(FONT_NORMAL_NAME));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void clearBox(uint8_t *bitmap, int x, int y, int width, int height)
|
|
|
|
|
{
|
|
|
|
|
for (int yy = y; yy < y + height; yy++)
|
|
|
|
|
for (int xx = x; xx < x + width; xx++)
|
|
|
|
|
setBitmapPixel(bitmap, xx, yy, false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
// The earlier of two "next change" times (0 = none)
|
|
|
|
|
static uint32_t sooner(uint32_t a, uint32_t b)
|
|
|
|
|
{
|
|
|
|
|
if (a == 0)
|
|
|
|
|
return b;
|
|
|
|
|
if (b == 0)
|
|
|
|
|
return a;
|
|
|
|
|
return a < b ? a : b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Image: a bitmap drawn with its top left corner at x, y. With an entity the name is the
|
|
|
|
|
// element's text followed by the state, or followed by "default" when there is no bitmap
|
|
|
|
|
// for the state.
|
|
|
|
|
static void drawImage(const PageElement &e, PageResources &resources, uint8_t *bitmap, ElementBox &box)
|
|
|
|
|
{
|
|
|
|
|
uint8_t image[BITMAP_BYTES];
|
|
|
|
|
bool found;
|
|
|
|
|
if (e.entity[0])
|
|
|
|
|
{
|
|
|
|
|
char name[64];
|
|
|
|
|
const char *state = resources.haValue(e);
|
|
|
|
|
snprintf(name, sizeof(name), "%s%s", e.text, state ? state : "");
|
|
|
|
|
found = state && resources.background(name, image);
|
|
|
|
|
if (!found)
|
|
|
|
|
{
|
|
|
|
|
snprintf(name, sizeof(name), "%sdefault", e.text);
|
|
|
|
|
found = resources.background(name, image);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
found = resources.background(e.text, image);
|
|
|
|
|
|
|
|
|
|
box = ElementBox();
|
|
|
|
|
box.x = e.x;
|
|
|
|
|
box.y = e.y;
|
|
|
|
|
if (!found)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// The box of its dots
|
|
|
|
|
int left = BITMAP_WIDTH, right = -1, top = BITMAP_HEIGHT, bottom = -1;
|
|
|
|
|
for (int y = 0; y < BITMAP_HEIGHT; y++)
|
|
|
|
|
for (int x = 0; x < BITMAP_WIDTH; x++)
|
|
|
|
|
if (bitmapPixel(image, x, y))
|
|
|
|
|
{
|
|
|
|
|
left = x < left ? x : left;
|
|
|
|
|
right = x > right ? x : right;
|
|
|
|
|
top = y < top ? y : top;
|
|
|
|
|
bottom = y > bottom ? y : bottom;
|
|
|
|
|
}
|
|
|
|
|
if (right < 0)
|
|
|
|
|
return;
|
|
|
|
|
box.x = e.x + left;
|
|
|
|
|
box.y = e.y + top;
|
|
|
|
|
box.width = right - left + 1;
|
|
|
|
|
box.height = bottom - top + 1;
|
|
|
|
|
|
|
|
|
|
if (e.overlay == Overlay::Box)
|
|
|
|
|
clearBox(bitmap, box.x - 1, box.y - 1, box.width + 2, box.height + 2);
|
|
|
|
|
for (int y = top; y <= bottom; y++)
|
|
|
|
|
for (int x = left; x <= right; x++)
|
|
|
|
|
if (bitmapPixel(image, x, y))
|
|
|
|
|
{
|
|
|
|
|
int bx = e.x + x, by = e.y + y;
|
|
|
|
|
bool on = e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true;
|
|
|
|
|
setBitmapPixel(bitmap, bx, by, on);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A text element; returns the ms until it scrolls on (0: it doesn't scroll)
|
|
|
|
|
static uint32_t drawTextElement(const PageElement &e, const char *text, uint32_t now_ms, PageResources &resources,
|
|
|
|
|
uint8_t *bitmap, ElementBox &box)
|
|
|
|
|
{
|
|
|
|
|
Font font = usableFont(resources, e.font);
|
|
|
|
|
int width = textWidth(font, text), height = fontHeight(font);
|
|
|
|
|
DrawMode mode = e.overlay == Overlay::Invert ? DrawMode::Invert : DrawMode::Set;
|
|
|
|
|
// Without scrolling the element is as wide as its text, otherwise the window
|
|
|
|
|
int area = e.scroll_width ? e.scroll_width : width;
|
|
|
|
|
int left = e.x;
|
|
|
|
|
if (e.align == Align::Center)
|
|
|
|
|
left -= area / 2;
|
|
|
|
|
else if (e.align == Align::Right)
|
|
|
|
|
left -= area;
|
|
|
|
|
|
|
|
|
|
box.x = left;
|
|
|
|
|
box.y = e.y;
|
|
|
|
|
box.width = area;
|
|
|
|
|
box.height = height;
|
|
|
|
|
if (e.overlay == Overlay::Box)
|
|
|
|
|
clearBox(bitmap, left - 1, e.y - 1, area + 2, height + 2);
|
|
|
|
|
|
|
|
|
|
if (width <= area)
|
|
|
|
|
{
|
|
|
|
|
// Fits: aligned in the window like the element
|
|
|
|
|
int x = left;
|
|
|
|
|
if (e.align == Align::Center)
|
|
|
|
|
x += (area - width) / 2;
|
|
|
|
|
else if (e.align == Align::Right)
|
|
|
|
|
x += area - width;
|
|
|
|
|
drawText(bitmap, font, x, e.y, text, mode);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scrolls from right to left, the next round follows after SCROLL_GAP dots
|
|
|
|
|
uint32_t period = width + SCROLL_GAP;
|
|
|
|
|
uint64_t steps = (uint64_t)now_ms * e.scroll_speed / 1000;
|
|
|
|
|
int offset = (int)(steps % period);
|
|
|
|
|
drawText(bitmap, font, left - offset, e.y, text, mode, left, left + area);
|
|
|
|
|
drawText(bitmap, font, left - offset + (int)period, e.y, text, mode, left, left + area);
|
|
|
|
|
// Until the next step
|
|
|
|
|
uint64_t next_step_ms = ((steps + 1) * 1000 + e.scroll_speed - 1) / e.scroll_speed;
|
|
|
|
|
uint32_t wait = (uint32_t)(next_step_ms - now_ms);
|
|
|
|
|
return wait ? wait : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now_ms, PageResources &resources,
|
|
|
|
|
uint8_t *bitmap, ElementBox *boxes)
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
|
|
|
|
memset(bitmap, 0, BITMAP_BYTES);
|
|
|
|
|
const PageSlot *slot = activeSlot(page, time.hour * 60 + time.minute);
|
|
|
|
|
if (slot && slot->bitmap[0] && !resources.background(slot->bitmap, bitmap))
|
|
|
|
|
memset(bitmap, 0, BITMAP_BYTES);
|
|
|
|
|
|
2026-10-15 21:01:14 +02:00
|
|
|
uint32_t next = 0;
|
|
|
|
|
bool running = resources.stopwatchRunning();
|
|
|
|
|
uint32_t stopwatch_ms = resources.stopwatchMs();
|
2026-10-15 19:43:59 +02:00
|
|
|
for (int i = 0; i < page.element_count && i < PAGE_ELEMENTS; i++)
|
|
|
|
|
{
|
|
|
|
|
const PageElement &e = page.elements[i];
|
2026-10-15 21:01:14 +02:00
|
|
|
ElementBox box;
|
|
|
|
|
if (e.type == ElementType::Image)
|
|
|
|
|
drawImage(e, resources, bitmap, box);
|
|
|
|
|
else
|
2026-10-15 19:43:59 +02:00
|
|
|
{
|
2026-10-15 21:01:14 +02:00
|
|
|
ElementValues values;
|
|
|
|
|
if (e.type == ElementType::HaValue)
|
|
|
|
|
values.ha_value = resources.haValue(e);
|
|
|
|
|
values.stopwatch_ms = stopwatch_ms;
|
|
|
|
|
char text[80];
|
|
|
|
|
elementText(e, time, values, text, sizeof(text));
|
|
|
|
|
next = sooner(next, drawTextElement(e, text, now_ms, resources, bitmap, box));
|
|
|
|
|
|
|
|
|
|
if (elementShowsSeconds(e, time, running)) // the stopwatch's seconds count from its start
|
|
|
|
|
next = sooner(next, e.type == ElementType::Stopwatch ? 1000 - stopwatch_ms % 1000
|
|
|
|
|
: 1000 - (time.millisecond % 1000));
|
|
|
|
|
else if (e.type == ElementType::Stopwatch && running) // H:MM: the next stopwatch minute
|
|
|
|
|
next = sooner(next, 60000 - stopwatch_ms % 60000);
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
2026-10-15 21:01:14 +02:00
|
|
|
if (boxes)
|
|
|
|
|
boxes[i] = box;
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|
2026-10-15 21:01:14 +02:00
|
|
|
return next;
|
2026-10-15 19:43:59 +02:00
|
|
|
}
|