Files

486 lines
15 KiB
C

/**
MIT License
Copyright (c) 2025 Benjamin Wiegand
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
#include "graphics.h"
#include "display.h"
#include "font.h"
#include "pico/stdlib.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
union g_object_ptr {
g_text_box_t* text_box;
g_rectangle_t* rectangle;
g_line_t* line;
};
typedef union g_object_ptr g_object_ptr_t;
enum g_object_type {
GRAPHICS_TEXT_BOX,
GRAPHICS_RECTANGLE,
GRAPHICS_LINE,
};
typedef enum g_object_type g_object_type_t;
struct g_object_holder {
g_object_type_t type;
g_object_ptr_t ptr;
};
typedef struct g_object_holder g_object_holder_t;
g_text_box_t graphics_text_box_pool[GRAPHICS_MAX_TEXT_BOXES];
g_rectangle_t graphics_rectangle_pool[GRAPHICS_MAX_RECTS];
g_line_t graphics_line_pool[GRAPHICS_MAX_LINES];
size_t graphics_text_box_alloc_index = 0;
size_t graphics_rectangle_alloc_index = 0;
size_t graphics_line_alloc_index = 0;
g_object_holder_t graphics_objects_internal[GRAPHICS_MAX_OBJECTS];
size_t graphics_object_count = 0;
void init_g_text_box(g_text_box_t* inst) {
inst->enabled = true;
if (inst->text != NULL && inst->length > 0) free(inst->text);
inst->text = NULL;
inst->length = 0;
inst->x1 = 0;
inst->y1 = 0;
inst->x2 = 0;
inst->max_lines = 1;
inst->line_spacing = 3;
inst->scale_factor = 1;
inst->color = COLOR_WHITE;
inst->alignment_mode = TEXT_ALIGN_LEFT;
inst->truncation_mode = TEXT_WRAP;
inst->_marquee = (_g_marquee_state_t){
index: 0,
last_updated: 0,
};
}
void init_g_rectangle(g_rectangle_t* inst) {
inst->enabled = true;
inst->x1 = 0;
inst->y1 = 0;
inst->x2 = 0;
inst->y2 = 0;
inst->color = COLOR_WHITE;
inst->filled = true;
}
void init_g_line(g_line_t* inst) {
inst->enabled = true;
inst->x1 = 0;
inst->y1 = 0;
inst->x2 = 0;
inst->y2 = 0;
inst->color = COLOR_WHITE;
}
coord_t g_text_box_chars_per_line(g_text_box_t* inst) {
coord_t char_width = FONT_WIDTH * inst->scale_factor;
coord_t tb_width = inst->x2 - inst->x1 + 1;
coord_t chars_per_line = 1 + ((tb_width - char_width) / (char_width + inst->scale_factor)); // accounts for spacing
return chars_per_line < 1 ? 1 : chars_per_line; // at least 1 char will be rendered
}
void graphics_render_text_line_internal(coord_t x, coord_t y, coord_t x_limit, g_text_alignment_mode_t alignment_mode, coord_t scale_factor, color_t color, char* text, size_t length, size_t line_chars, bool marquee_start, bool marquee_end) {
if (line_chars == 0) line_chars = length;
coord_t line_length = FONT_WIDTH * scale_factor * line_chars + scale_factor * (line_chars - 1);
switch (alignment_mode) {
case TEXT_ALIGN_CENTER:
x += (x_limit - x + 1 - line_length) / 2;
break;
case TEXT_ALIGN_RIGHT:
x = x_limit - line_length + 1;
break;
case TEXT_ALIGN_LEFT:
default:
break;
}
if (marquee_start) {
display_draw_char(x, y, scale_factor, color, '<');
x += FONT_WIDTH * scale_factor + scale_factor;
}
for (size_t i = marquee_start; i < length - marquee_end; i++) {
display_draw_char(x, y, scale_factor, color, text[i]);
x += FONT_WIDTH * scale_factor + scale_factor;
}
// pad out the rest of line_chars
x += (FONT_WIDTH * scale_factor + scale_factor) * (line_chars - length);
if (marquee_end) {
display_draw_char(x, y, scale_factor, color, '>');
}
}
void render_g_text_box(g_text_box_t* inst) {
if (!inst->enabled) return;
if (inst->text == NULL || inst->length == 0) return;
coord_t chars_per_line = g_text_box_chars_per_line(inst);
coord_t line_number = 1;
coord_t y = inst->y1 + FONT_HEIGHT * inst->scale_factor;
bool render_line;
size_t marquee_index = 0;
if (inst->truncation_mode == TEXT_MARQUEE) {
inst->_marquee.enabled = chars_per_line < inst->length;
if (inst->_marquee.enabled) {
marquee_index = inst->_marquee.index;
}
}
size_t marquee_length = chars_per_line;
if (inst->length - marquee_index < chars_per_line) {
marquee_length = inst->length - marquee_index;
}
size_t line_start_i = marquee_index;
size_t i;
for (i = line_start_i; i < inst->length + 1; i++) {
render_line =
i == inst->length || // end of string
(inst->truncation_mode != TEXT_MARQUEE && inst->text[i] == '\n') || // LF (force marquees to single line)
(inst->truncation_mode == TEXT_WRAP && i - line_start_i >= chars_per_line); // text wrap
if (!render_line) continue;
if (marquee_index == 0 && i - line_start_i <= chars_per_line) {
// the line fits
graphics_render_text_line_internal(
inst->x1, y, inst->x2, inst->alignment_mode, inst->scale_factor,
inst->color, &inst->text[line_start_i], i - line_start_i, 0, false, false);
} else {
// the line is truncated
switch (inst->truncation_mode) {
case TEXT_MARQUEE:
graphics_render_text_line_internal(
inst->x1, y, inst->x2, inst->alignment_mode, inst->scale_factor,
inst->color, &inst->text[line_start_i],
marquee_length,
chars_per_line,
marquee_index > 0,
i - line_start_i > chars_per_line);
break;
case TEXT_TRUNCATE:
default:
graphics_render_text_line_internal(
inst->x1, y, inst->x2, inst->alignment_mode, inst->scale_factor,
inst->color, &inst->text[line_start_i], chars_per_line, chars_per_line, false, false);
break;
}
}
line_start_i = i + (inst->text[i] == '\n' ? 1 : 0);
y += FONT_HEIGHT * inst->scale_factor + inst->line_spacing;
line_number++;
if (inst->max_lines != 0 && line_number > inst->max_lines) break;
}
}
void render_g_rectangle(g_rectangle_t* inst) {
if (!inst->enabled) return;
if (inst->filled) {
display_draw_rectangle(inst->x1, inst->y1, inst->x2, inst->y2, inst->color);
} else {
display_draw_rectangle_outline(inst->x1, inst->y1, inst->x2, inst->y2, inst->color);
}
}
void render_g_line(g_line_t* inst) {
if (!inst->enabled) return;
display_draw_line(inst->x1, inst->y1, inst->x2, inst->y2, inst->color);
}
void graphics_add_object(g_object_holder_t holder) {
if (graphics_object_count >= GRAPHICS_MAX_OBJECTS) {
printf("ERROR: can't add any more graphics objects\n");
return;
}
graphics_objects_internal[graphics_object_count++] = holder;
}
coord_t g_text_box_height(g_text_box_t* inst) {
if (inst->text == NULL || inst->length == 0 || inst->max_lines == 1 || inst->truncation_mode == TEXT_MARQUEE) return FONT_HEIGHT * inst->scale_factor;
coord_t max_line_len = inst->x2 - inst->x1 + 1;
coord_t cur_line_len = 0;
coord_t height = FONT_HEIGHT * inst->scale_factor;
coord_t line_number = 1;
char c;
bool newline;
// loop to properly handle LF chars
for (size_t i = 0; i < inst->length; i++) {
c = inst->text[i];
if (c == '\n') {
newline = true;
} else {
cur_line_len += FONT_WIDTH * inst->scale_factor + inst->scale_factor;
newline = inst->truncation_mode == TEXT_WRAP && cur_line_len + FONT_WIDTH * inst->scale_factor > max_line_len;
}
if (newline) {
line_number++;
if (inst->max_lines != 0 && line_number > inst->max_lines) break;
height += FONT_HEIGHT * inst->scale_factor + inst->line_spacing;
cur_line_len = 0;
}
}
return height;
}
void g_text_box_print_internal(g_text_box_t* inst, char* text, size_t length) {
if (inst->text != NULL && inst->length != 0) free(inst->text);
inst->text = text;
inst->length = length;
}
void g_text_box_print(g_text_box_t* inst, char* text) {
size_t length = strlen(text);
char* text_ptr = malloc(length);
for (size_t i = 0; i < length; i++)
text_ptr[i] = text[i];
g_text_box_print_internal(inst, text_ptr, length);
}
void g_text_box_printf(g_text_box_t* inst, char* text, ...) {
va_list args;
va_start(args, text);
// this is terrible, but I don't like wasting ram.
size_t length = vsprintf(NULL, text, args);
char* text_ptr = malloc(length);
vsprintf(text_ptr, text, args);
va_end(args);
g_text_box_print_internal(inst, text_ptr, length);
}
g_text_box_t* get_g_text_box_inst() {
if (graphics_text_box_alloc_index == GRAPHICS_MAX_TEXT_BOXES) {
// this should be a panic, but crashing would be sub-optimal
printf("FATAL: not enough text box instances!!!!\n");
return &graphics_text_box_pool[graphics_text_box_alloc_index - 1];
}
g_text_box_t* inst = &graphics_text_box_pool[graphics_text_box_alloc_index++];
init_g_text_box(inst);
return inst;
}
g_rectangle_t* get_g_rectangle_inst() {
if (graphics_rectangle_alloc_index == GRAPHICS_MAX_RECTS) {
// this should be a panic, but crashing would be sub-optimal
printf("FATAL: not enough rectangle instances!!!!\n");
return &graphics_rectangle_pool[graphics_rectangle_alloc_index - 1];
}
g_rectangle_t* inst = &graphics_rectangle_pool[graphics_rectangle_alloc_index++];
init_g_rectangle(inst);
return inst;
}
g_line_t* get_g_line_inst() {
if (graphics_line_alloc_index == GRAPHICS_MAX_LINES) {
// this should be a panic, but crashing would be sub-optimal
printf("FATAL: not enough line instances!!!!\n");
return &graphics_line_pool[graphics_line_alloc_index - 1];
}
g_line_t* inst = &graphics_line_pool[graphics_line_alloc_index++];
init_g_line(inst);
return inst;
}
void setup_g_text_box(g_text_box_t* inst, coord_t x1, coord_t y1, coord_t x2, coord_t max_lines, color_t color) {
inst->x1 = x1;
inst->y1 = y1;
inst->x2 = x2;
inst->max_lines = max_lines;
inst->color = color;
}
void setup_g_rectangle(g_rectangle_t* inst, coord_t x1, coord_t y1, coord_t x2, coord_t y2, color_t color, bool filled) {
inst->x1 = x1;
inst->y1 = y1;
inst->x2 = x2;
inst->y2 = y2;
inst->color = color;
inst->filled = filled;
}
void setup_g_line(g_line_t* inst, coord_t x1, coord_t y1, coord_t x2, coord_t y2, color_t color) {
inst->x1 = x1;
inst->y1 = y1;
inst->x2 = x2;
inst->y2 = y2;
inst->color = color;
}
void graphics_add_text_box(g_text_box_t* inst) {
g_object_ptr_t ptr;
ptr.text_box = inst;
g_object_holder_t holder = {
type: GRAPHICS_TEXT_BOX,
ptr: ptr,
};
graphics_add_object(holder);
}
void graphics_add_rectangle(g_rectangle_t* inst) {
g_object_ptr_t ptr;
ptr.rectangle = inst;
g_object_holder_t holder = {
type: GRAPHICS_RECTANGLE,
ptr: ptr,
};
graphics_add_object(holder);
}
void graphics_add_line(g_line_t* inst) {
g_object_ptr_t ptr;
ptr.line = inst;
g_object_holder_t holder = {
type: GRAPHICS_LINE,
ptr: ptr,
};
graphics_add_object(holder);
}
coord_t graphics_calculate_text_width(size_t chars, coord_t scale_factor) {
if (chars == 0) return 0;
return FONT_WIDTH * chars * scale_factor + (chars - 1) * scale_factor;
}
color_t graphics_calculate_foreground_color(color_t background_color) {
uint color_sum = (background_color >> 11) + ((background_color >> 5) & 0x3F) + (background_color & 0x1F);
if (color_sum > 62) return COLOR_BLACK;
return COLOR_WHITE;
}
void graphics_render() {
g_object_holder_t* holder;
display_clear();
for (size_t i = 0; i < graphics_object_count; i++) {
holder = &graphics_objects_internal[i];
switch (holder->type) {
case GRAPHICS_TEXT_BOX:
render_g_text_box(holder->ptr.text_box);
break;
case GRAPHICS_RECTANGLE:
render_g_rectangle(holder->ptr.rectangle);
break;
case GRAPHICS_LINE:
render_g_line(holder->ptr.line);
break;
}
}
display_refresh();
}
void graphics_update() {
g_object_holder_t* holder;
g_text_box_t* text_box;
uint64_t timestamp = time_us_64();
bool marquee_updates = false;
// update marquees
for (size_t i = 0; i < graphics_object_count; i++) {
holder = &graphics_objects_internal[i];
if (holder->type != GRAPHICS_TEXT_BOX) continue;
text_box = holder->ptr.text_box;
if (!text_box->_marquee.enabled || text_box->truncation_mode != TEXT_MARQUEE) continue;
// init timestamp
if (text_box->_marquee.last_updated == 0) {
text_box->_marquee.last_updated = timestamp;
continue;
}
// handle delays
if (text_box->_marquee.index == 0) {
if (text_box->_marquee.last_updated + MARQUEE_START_DELAY > timestamp) continue;
} else {
if (text_box->_marquee.last_updated + MARQUEE_INTERVAL > timestamp) continue;
}
text_box->_marquee.last_updated = timestamp;
marquee_updates = true;
text_box->_marquee.index++;
if (text_box->_marquee.index >= text_box->length) {
text_box->_marquee.index = 0;
}
}
if (marquee_updates) graphics_render();
}
void graphics_reset() {
// free strings from the heap
for (size_t i = 0; i < graphics_text_box_alloc_index; i++) {
init_g_text_box(&graphics_text_box_pool[i]);
}
graphics_text_box_alloc_index = 0;
graphics_rectangle_alloc_index = 0;
graphics_line_alloc_index = 0;
graphics_object_count = 0;
}
void init_graphics() {
graphics_reset();
}