File syn_imgui.c¶
File List > src > syntropic > ui > syn_imgui.c
Go to the documentation of this file
#if __has_include("syn_config.h")
#include "syn_config.h"
#endif
#if !defined(SYN_USE_IMGUI) || SYN_USE_IMGUI
#include "../util/syn_assert.h"
#include "../util/syn_fmt.h"
#include "syn_imgui.h"
#include <string.h>
/* Forward declarations for functions defined later in this file */
static void layout_resolve(SYN_IMGUI_Context *ctx, int16_t *x, int16_t *y, int16_t *w, int16_t *h,
int16_t default_h);
void syn_imgui_init(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
memset(ctx, 0, sizeof(*ctx));
ctx->focused_id = 1;
ctx->style = syn_imgui_default_style();
}
void syn_imgui_begin(SYN_IMGUI_Context *ctx, SYN_GfxContext gfx, bool select, bool back,
int32_t enc_delta, bool touch_down, int16_t touch_x, int16_t touch_y)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(gfx != NULL);
ctx->gfx = gfx;
ctx->btn_select = select;
ctx->btn_back = back;
ctx->enc_delta = enc_delta;
ctx->touch_down = touch_down;
ctx->touch_x = touch_x;
ctx->touch_y = touch_y;
ctx->next_id = 0;
/* Focus navigation by encoder only if screen is NOT being touched and no slider is active */
if (ctx->active_id == 0 && !touch_down && enc_delta != 0 && ctx->last_max_id > 0) {
int32_t count = (int32_t)ctx->last_max_id;
int32_t idx = (int32_t)ctx->focused_id - 1;
if (idx < 0) {
idx = 0;
}
idx = (idx + (enc_delta % count)) % count;
if (idx < 0) {
idx += count;
}
ctx->focused_id = (uint16_t)(idx + 1);
}
}
void syn_imgui_end(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
/* Bug #2: validate stack balance (catch forgotten end calls) */
SYN_ASSERT(!ctx->layout.in_layout); /* forgot layout_end? */
SYN_ASSERT(!ctx->scroll.in_scroll); /* forgot scroll_end? */
SYN_ASSERT(ctx->disabled_depth == 0); /* forgot end_disabled? */
ctx->last_max_id = ctx->next_id;
/* Bug #3: clear focus if the focused widget was not visited this frame */
if (!ctx->updated_focus && ctx->focused_id > 0) {
ctx->focused_id = (ctx->last_max_id > 0) ? 1 : 0;
}
ctx->updated_focus = false;
/* Safely cap focus if the number of widgets decreased */
if (ctx->focused_id > ctx->last_max_id) {
ctx->focused_id = (ctx->last_max_id > 0) ? 1 : 0;
}
}
static bool is_hit_test(const SYN_IMGUI_Context *ctx, int16_t x, int16_t y, int16_t w, int16_t h)
{
return (ctx->touch_down && ctx->touch_x >= x && ctx->touch_x < x + w && ctx->touch_y >= y &&
ctx->touch_y < y + h);
}
static inline void track_focus(SYN_IMGUI_Context *ctx)
{
if (ctx->focused_id == ctx->next_id) {
ctx->updated_focus = true;
}
}
bool syn_imgui_button(SYN_IMGUI_Context *ctx, const char *label, int16_t x, int16_t y, int16_t w,
int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + 2 * ctx->style.padding);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
int16_t text_w = (int16_t)syn_gfx_text_width(ctx->gfx, label);
int16_t tx = (int16_t)(x + (w - text_w) / 2);
int16_t ty = (int16_t)(y + (h - (int16_t)fh) / 2);
if (is_focused && ctx->disabled_depth == 0) {
/* Draw filled background */
syn_gfx_rect_round_fill(ctx->gfx, x, y, w, h, 4, fg);
/* Draw text in background color */
syn_gfx_text(ctx->gfx, tx, ty, label, ctx->style.bg);
} else {
/* Draw outline only */
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 4, fg);
/* Draw text in foreground */
syn_gfx_text(ctx->gfx, tx, ty, label, fg);
}
return ctx->disabled_depth == 0 && (touch_clicked || (is_focused && ctx->btn_select));
}
bool syn_imgui_checkbox(SYN_IMGUI_Context *ctx, const char *label, bool *checked, int16_t x,
int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(checked != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + 2 * ctx->style.padding);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool toggled = false;
if (ctx->disabled_depth == 0 && (touch_clicked || (is_focused && ctx->btn_select))) {
*checked = !(*checked);
toggled = true;
}
/* Focus outline around row */
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Render Box (10x10) */
int16_t box_size = 10;
int16_t bx = x + 4;
int16_t by = (int16_t)(y + (h - box_size) / 2);
syn_gfx_rect(ctx->gfx, bx, by, box_size, box_size, fg);
if (*checked) {
/* Checked inner box (6x6) */
syn_gfx_rect_fill(ctx->gfx, bx + 2, by + 2, box_size - 4, box_size - 4, fg);
}
/* Label text */
int16_t tx = x + 18;
int16_t ty = (int16_t)(y + (h - (int16_t)fh) / 2);
syn_gfx_text(ctx->gfx, tx, ty, label, fg);
return toggled;
}
void syn_imgui_progress_bar(SYN_IMGUI_Context *ctx, int32_t value, int32_t min, int32_t max,
int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
syn_gfx_rect(ctx->gfx, x, y, w, h, fg);
if (max > min) {
int32_t val = value;
if (val < min)
val = min;
if (val > max)
val = max;
int32_t range = max - min;
int32_t current = val - min;
int16_t fill_w = (int16_t)((current * (w - 4)) / range);
if (fill_w > 0) {
syn_gfx_rect_fill(ctx->gfx, x + 2, y + 2, fill_w, h - 4, fg);
}
}
}
bool syn_imgui_slider(SYN_IMGUI_Context *ctx, const char *label, int32_t *value, int32_t min,
int32_t max, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(value != NULL);
ctx->next_id++;
track_focus(ctx);
/* Resolve geometry from layout cursor before using x/y/w/h */
uint8_t fh_sl = syn_gfx_text_height(ctx->gfx);
int16_t default_h_sl = (int16_t)(fh_sl + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h_sl);
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool is_active = (ctx->active_id == ctx->next_id);
bool changed = false;
int16_t bar_w = (int16_t)(w / 2 - 8);
int16_t bar_x = (int16_t)(x + w / 2 + 4);
int16_t bar_h = 6;
int16_t bar_y = (int16_t)(y + (h - bar_h) / 2);
/* Handle direct touch settings */
if (ctx->disabled_depth == 0 && touch_clicked) {
ctx->active_id = ctx->next_id;
is_active = true;
/* Calculate absolute value position if touch fell inside slider bar bounds */
if (ctx->touch_x >= bar_x && ctx->touch_x <= bar_x + bar_w && max > min) {
int16_t relative_x = ctx->touch_x - bar_x;
int32_t new_val = min + (relative_x * (max - min)) / bar_w;
if (new_val < min)
new_val = min;
if (new_val > max)
new_val = max;
if (new_val != *value) {
*value = new_val;
changed = true;
}
}
}
if (is_active) {
/* If we are actively editing, encoder delta adjusts values */
if (ctx->disabled_depth == 0 && !touch_clicked && ctx->enc_delta != 0) {
int32_t new_val = *value + ctx->enc_delta;
if (new_val < min)
new_val = min;
if (new_val > max)
new_val = max;
if (new_val != *value) {
*value = new_val;
changed = true;
}
}
/* Exit edit mode on select or back press (or if touch release happens outside row) */
if ((ctx->btn_select || ctx->btn_back) && !touch_clicked) {
ctx->active_id = 0;
}
} else if (is_focused) {
if (ctx->btn_select) {
ctx->active_id = ctx->next_id;
}
}
/* Row highlight if focused and not actively editing */
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
if (is_focused && !is_active) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Draw label */
int16_t tx = x + 4;
int16_t ty = (int16_t)(y + (h - 7) / 2);
syn_gfx_text(ctx->gfx, tx, ty, label, fg);
/* Draw progress bar inside using helper */
syn_imgui_progress_bar(ctx, *value, min, max, bar_x, bar_y, bar_w, bar_h);
/* Edit mode indicator around the bar */
if (is_active) {
syn_gfx_rect(ctx->gfx, bar_x - 2, bar_y - 2, bar_w + 4, bar_h + 4, fg);
}
return changed;
}
bool syn_imgui_radio(SYN_IMGUI_Context *ctx, const char *label, int32_t *selection,
int32_t button_val, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(selection != NULL);
ctx->next_id++;
track_focus(ctx);
/* Resolve geometry from layout cursor */
uint8_t fh_r = syn_gfx_text_height(ctx->gfx);
int16_t default_h_r = (int16_t)(fh_r + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h_r);
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool changed = false;
if (ctx->disabled_depth == 0 && (touch_clicked || (is_focused && ctx->btn_select))) {
if (*selection != button_val) {
*selection = button_val;
changed = true;
}
}
/* Focus outline around row */
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Render Radio Circle (radius = 5, center x = x + 9, y = y + h/2) */
int16_t cx = x + 9;
int16_t cy = y + h / 2;
syn_gfx_circle(ctx->gfx, cx, cy, 5, fg);
if (*selection == button_val) {
/* Selected center dot (radius = 2) */
syn_gfx_circle_fill(ctx->gfx, cx, cy, 2, fg);
}
/* Label text */
int16_t tx = x + 20;
int16_t ty = (int16_t)(y + (h - 7) / 2);
syn_gfx_text(ctx->gfx, tx, ty, label, fg);
return changed;
}
bool syn_imgui_combo(SYN_IMGUI_Context *ctx, const char *label, const char **options, size_t count,
int32_t *selected, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(options != NULL);
SYN_ASSERT(selected != NULL);
if (count == 0)
return false;
ctx->next_id++;
track_focus(ctx);
/* Resolve geometry from layout cursor */
uint8_t fh_c = syn_gfx_text_height(ctx->gfx);
int16_t default_h_c = (int16_t)(fh_c + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h_c);
bool touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool is_active = (ctx->active_id == ctx->next_id);
bool changed = false;
int16_t left_x = (int16_t)(x + w / 2 + 2);
int16_t right_x = (int16_t)(x + w - 14);
/* Handle direct touch button hits on arrows */
if (ctx->disabled_depth == 0 && touch_clicked) {
bool hit_left = (ctx->touch_x >= left_x && ctx->touch_x < left_x + 12);
bool hit_right = (ctx->touch_x >= right_x && ctx->touch_x < right_x + 12);
if (hit_left) {
int32_t idx = *selected - 1;
if (idx < 0)
idx = (int32_t)count - 1;
*selected = idx;
changed = true;
} else if (hit_right) {
int32_t idx = *selected + 1;
if (idx >= (int32_t)count)
idx = 0;
*selected = idx;
changed = true;
} else {
/* Touch in middle toggles active editing */
ctx->active_id = is_active ? 0 : ctx->next_id;
is_active = !is_active;
}
}
if (is_active) {
if (!touch_clicked && ctx->enc_delta != 0) {
int32_t idx = *selected + ctx->enc_delta;
/* Modulo wrapping for negative/positive index */
while (idx < 0)
idx += (int32_t)count;
while (idx >= (int32_t)count)
idx -= (int32_t)count;
if (idx != *selected) {
*selected = idx;
changed = true;
}
}
if ((ctx->btn_select || ctx->btn_back) && !touch_clicked) {
ctx->active_id = 0;
}
} else if (is_focused) {
if (ctx->btn_select) {
ctx->active_id = ctx->next_id;
}
}
/* Highlight focused (not active) row */
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
if (is_focused && !is_active) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Draw label */
int16_t tx = x + 4;
int16_t ty = (int16_t)(y + (h - 7) / 2);
syn_gfx_text(ctx->gfx, tx, ty, label, fg);
/* Draw "< option_text >" centered in the right half */
int16_t val_w = w / 2;
int16_t val_x = x + w / 2;
const char *opt_str = options[*selected];
int16_t opt_w = (int16_t)syn_gfx_text_width(ctx->gfx, opt_str);
int16_t opt_x = (int16_t)(val_x + (val_w - opt_w) / 2);
/* Draw left/right arrows */
syn_gfx_text(ctx->gfx, left_x + 2, ty, "<", fg);
syn_gfx_text(ctx->gfx, right_x + 2, ty, ">", fg);
/* Draw the active option text */
syn_gfx_text(ctx->gfx, opt_x, ty, opt_str, fg);
/* Draw active edit frame around option */
if (is_active) {
syn_gfx_rect(ctx->gfx, left_x, (int16_t)(y + 1), (int16_t)(right_x - left_x + 12),
(int16_t)(h - 2), fg);
}
return changed;
}
void syn_imgui_graph(SYN_IMGUI_Context *ctx, const char *title, const int32_t *data, size_t count,
int32_t min_val, int32_t max_val, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
/* Resolve geometry from layout cursor; graph needs explicit w/h when used inline */
layout_resolve(ctx, &x, &y, &w, &h, h > 0 ? h : (int16_t)32);
if (w <= 0 || h <= 0)
return;
uint16_t fg = ctx->style.fg;
/* Draw container border */
syn_gfx_rect(ctx->gfx, x, y, w, h, fg);
int16_t plot_x = x + 2;
int16_t plot_w = w - 4;
int16_t plot_y = y + 2;
int16_t plot_h = h - 4;
/* Header text if title is provided */
if (title != NULL) {
syn_gfx_text(ctx->gfx, x + 4, y + 4, title, fg);
plot_y += 12;
plot_h -= 12;
}
if (plot_h <= 0 || plot_w <= 0)
return;
/* Draw midpoint horizontal reference grid line */
syn_gfx_line(ctx->gfx, plot_x, (int16_t)(plot_y + plot_h / 2), (int16_t)(plot_x + plot_w - 1),
(int16_t)(plot_y + plot_h / 2), fg);
if (data == NULL || count == 0)
return;
int32_t range = max_val - min_val;
if (range <= 0)
range = 1;
int16_t prev_x = 0;
int16_t prev_y = 0;
for (size_t i = 0; i < count; i++) {
/* X position calculation */
int16_t x_pos = plot_x;
if (count > 1) {
x_pos += (int16_t)((i * (size_t)(plot_w - 1)) / (count - 1));
}
/* Y position calculation */
int32_t val = data[i];
if (val < min_val)
val = min_val;
if (val > max_val)
val = max_val;
int16_t y_pos =
(int16_t)(plot_y + plot_h - 1 - (int16_t)(((val - min_val) * (plot_h - 1)) / range));
if (i > 0) {
syn_gfx_line(ctx->gfx, prev_x, prev_y, x_pos, y_pos, fg);
}
prev_x = x_pos;
prev_y = y_pos;
}
}
static void draw_semi_circle(SYN_GfxContext c, int16_t cx, int16_t cy, int16_t r, uint16_t color)
{
int16_t x = r;
int16_t y = 0;
int16_t err = 1 - r;
while (x >= y) {
syn_gfx_pixel(c, cx - x, cy - y, color);
syn_gfx_pixel(c, cx - y, cy - x, color);
syn_gfx_pixel(c, cx + y, cy - x, color);
syn_gfx_pixel(c, cx + x, cy - y, color);
y++;
if (err < 0) {
err = (int16_t)(err + 2 * y + 1);
} else {
x--;
err = (int16_t)(err + 2 * (y - x) + 1);
}
}
}
void syn_imgui_gauge(SYN_IMGUI_Context *ctx, const char *label, int32_t value, int32_t min,
int32_t max, int16_t cx, int16_t cy, int16_t radius)
{
SYN_ASSERT(ctx != NULL);
if (radius <= 0)
return;
/* Resolve geometry: gauge uses center+radius; synthesize a bounding box
* for the layout cursor so it advances past the gauge's footprint. */
int16_t gx = (int16_t)(cx - radius);
int16_t gy = (int16_t)(cy - radius);
int16_t gw = (int16_t)(radius * 2);
int16_t gh = (int16_t)(radius + 10); /* semi-circle + label */
layout_resolve(ctx, &gx, &gy, &gw, &gh, gh);
uint16_t fg = ctx->style.fg;
/* Draw arc gauge */
draw_semi_circle(ctx->gfx, cx, cy, radius, fg);
/* Draw base reference line */
syn_gfx_line(ctx->gfx, cx - radius, cy, cx + radius, cy, fg);
/* 9-point fixed-point sin/cos tables (0 to 180 degrees, scaled by 256) */
static const int16_t sin_tbl[9] = {0, 98, 181, 236, 256, 236, 181, 98, 0};
static const int16_t cos_tbl[9] = {256, 236, 181, 98, 0, -98, -181, -236, -256};
/* Calculate value percentage (0 to 1000) */
int32_t val = value;
if (val < min)
val = min;
if (val > max)
val = max;
int32_t range = max - min;
int32_t val_percent = (range > 0) ? ((val - min) * 1000) / range : 0;
/* Map to table index and interpolation remainder */
int32_t temp = (1000 - val_percent) * 8;
int32_t idx = temp / 1000;
int32_t sin_val;
int32_t cos_val;
if (idx >= 8) {
sin_val = sin_tbl[8];
cos_val = cos_tbl[8];
} else {
int32_t rem = temp % 1000;
sin_val = sin_tbl[idx] + (rem * (sin_tbl[idx + 1] - sin_tbl[idx])) / 1000;
cos_val = cos_tbl[idx] + (rem * (cos_tbl[idx + 1] - cos_tbl[idx])) / 1000;
}
/* Draw needle */
int16_t needle_len = radius - 4;
int16_t nx = cx + (int16_t)((cos_val * needle_len) / 256);
int16_t ny = cy - (int16_t)((sin_val * needle_len) / 256);
syn_gfx_line(ctx->gfx, cx, cy, nx, ny, fg);
/* Draw label centered under the gauge base */
if (label != NULL) {
int16_t text_w = (int16_t)syn_gfx_text_width(ctx->gfx, label);
syn_gfx_text(ctx->gfx, cx - text_w / 2, cy + 2, label, fg);
}
}
bool syn_imgui_dialog(SYN_IMGUI_Context *ctx, const char *message, bool *ok_clicked, int16_t x,
int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(message != NULL);
SYN_ASSERT(ok_clicked != NULL);
/* Resolve geometry from layout cursor */
layout_resolve(ctx, &x, &y, &w, &h, h > 0 ? h : (int16_t)40);
uint16_t ok_id = ctx->next_id + 1;
uint16_t cancel_id = ctx->next_id + 2;
ctx->next_id += 2;
/* Track focus for stale-focus detection */
if (ctx->focused_id == ok_id || ctx->focused_id == cancel_id) {
ctx->updated_focus = true;
}
/* Force focus inside dialog button bounds if it is currently outside */
if (ctx->focused_id != ok_id && ctx->focused_id != cancel_id) {
ctx->focused_id = ok_id;
}
/* Hijack navigation input while modal is open */
if (ctx->enc_delta != 0) {
ctx->focused_id = (ctx->focused_id == ok_id) ? cancel_id : ok_id;
ctx->enc_delta = 0; /* Consume input */
}
/* Modal layout coordinates */
int16_t btn_w = 30;
int16_t btn_h = 13;
int16_t ok_x = (int16_t)(x + w / 4 - btn_w / 2);
int16_t canc_x = (int16_t)(x + (3 * w) / 4 - btn_w / 2);
int16_t btn_y = (int16_t)(y + h - 18);
/* Hit testing */
bool hit_ok = is_hit_test(ctx, ok_x, btn_y, btn_w, btn_h);
bool hit_canc = is_hit_test(ctx, canc_x, btn_y, btn_w, btn_h);
bool select_ok = (ctx->focused_id == ok_id && ctx->btn_select);
bool select_cancel = (ctx->focused_id == cancel_id && ctx->btn_select) || ctx->btn_back;
bool dismissed = false;
if (hit_ok || select_ok) {
*ok_clicked = true;
dismissed = true;
ctx->focused_id = 1; /* Reset focus */
} else if (hit_canc || select_cancel) {
*ok_clicked = false;
dismissed = true;
ctx->focused_id = 1; /* Reset focus */
}
/* Consume touches outside the modal dialog to prevent background interaction */
if (ctx->touch_down && !is_hit_test(ctx, x, y, w, h)) {
ctx->touch_down = false;
}
/* Draw modal background outline box (clears underlying content) */
uint16_t fg = ctx->style.fg;
syn_gfx_rect_round_fill(ctx->gfx, x, y, w, h, 4, ctx->style.bg);
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 4, fg);
/* Draw message centered at top */
int16_t msg_w = (int16_t)syn_gfx_text_width(ctx->gfx, message);
syn_gfx_text(ctx->gfx, (int16_t)(x + (w - msg_w) / 2), (int16_t)(y + 6), message, fg);
/* Render OK button */
if (ctx->focused_id == ok_id) {
syn_gfx_rect_round_fill(ctx->gfx, ok_x, btn_y, btn_w, btn_h, 3, fg);
syn_gfx_text(ctx->gfx, (int16_t)(ok_x + (btn_w - 11) / 2), (int16_t)(btn_y + 3), "OK",
ctx->style.bg);
} else {
syn_gfx_rect_round(ctx->gfx, ok_x, btn_y, btn_w, btn_h, 3, fg);
syn_gfx_text(ctx->gfx, (int16_t)(ok_x + (btn_w - 11) / 2), (int16_t)(btn_y + 3), "OK", fg);
}
/* Render CANCEL button */
int16_t cn_w = (int16_t)syn_gfx_text_width(ctx->gfx, "CANC");
if (ctx->focused_id == cancel_id) {
syn_gfx_rect_round_fill(ctx->gfx, canc_x, btn_y, btn_w, btn_h, 3, fg);
syn_gfx_text(ctx->gfx, (int16_t)(canc_x + (btn_w - cn_w) / 2), (int16_t)(btn_y + 3), "CANC",
ctx->style.bg);
} else {
syn_gfx_rect_round(ctx->gfx, canc_x, btn_y, btn_w, btn_h, 3, fg);
syn_gfx_text(ctx->gfx, (int16_t)(canc_x + (btn_w - cn_w) / 2), (int16_t)(btn_y + 3), "CANC",
fg);
}
return dismissed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Style
* ═══════════════════════════════════════════════════════════════════════════ */
SYN_IMGUI_Style syn_imgui_default_style(void)
{
SYN_IMGUI_Style s;
s.fg = SYN_COLOR_WHITE;
s.bg = SYN_COLOR_BLACK;
s.highlight = SYN_COLOR_WHITE;
s.fg_inv = SYN_COLOR_BLACK;
s.accent = SYN_COLOR_WHITE;
s.disabled = 0x7BEFu; /* mid-grey in RGB565 */
s.padding = 2;
s.spacing = 3;
return s;
}
void syn_imgui_set_style(SYN_IMGUI_Context *ctx, const SYN_IMGUI_Style *style)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(style != NULL);
ctx->style = *style;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Layout cursor
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_layout_begin(SYN_IMGUI_Context *ctx, int16_t x, int16_t y, int16_t w)
{
SYN_ASSERT(ctx != NULL);
ctx->layout.in_layout = true;
ctx->layout.cx = x;
ctx->layout.cy = y;
ctx->layout.origin_x = x;
ctx->layout.width = w;
ctx->layout.row_h = 0;
ctx->layout.same_line = false;
ctx->layout.row_x = x;
ctx->layout.row_y = y;
ctx->layout.prev_row_h = 0;
}
void syn_imgui_layout_end(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
if (ctx == NULL)
return;
ctx->layout.in_layout = false;
}
void syn_imgui_same_line(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
if (!ctx->layout.in_layout)
return;
ctx->layout.cx = ctx->layout.row_x;
ctx->layout.cy = ctx->layout.row_y;
ctx->layout.row_h = ctx->layout.prev_row_h;
ctx->layout.same_line = true;
}
void syn_imgui_spacing(SYN_IMGUI_Context *ctx, int16_t pixels)
{
SYN_ASSERT(ctx != NULL);
if (ctx->layout.in_layout) {
ctx->layout.cy = (int16_t)(ctx->layout.cy + pixels);
}
}
static void layout_resolve(SYN_IMGUI_Context *ctx, int16_t *x, int16_t *y, int16_t *w, int16_t *h,
int16_t default_h)
{
if (ctx == NULL || !ctx->layout.in_layout)
return;
if (ctx->layout.same_line) {
/* consumed by syn_imgui_same_line cursor restoration */
ctx->layout.same_line = false;
} else {
/* If the previous widget(s) left the row unwrapped (e.g. manually manipulated),
* wrap it now before resolving this widget. */
if (ctx->layout.row_h > 0) {
ctx->layout.cy = (int16_t)(ctx->layout.row_y + ctx->layout.row_h + ctx->style.spacing);
ctx->layout.cx = ctx->layout.origin_x;
ctx->layout.row_h = 0;
}
ctx->layout.row_y = ctx->layout.cy;
}
if (*x == 0 && *y == 0 && *w == 0 && *h == 0) {
/* Use cursor position */
*x = ctx->layout.cx;
*y = ctx->layout.cy;
*w = ctx->layout.width;
*h = default_h;
}
/* Track tallest widget in row */
if (*h > ctx->layout.row_h) {
ctx->layout.row_h = *h;
}
/* Save state for same_line to restore/use if called next */
ctx->layout.row_x = (int16_t)(*x + *w + ctx->style.spacing);
ctx->layout.row_y = *y;
ctx->layout.prev_row_h = ctx->layout.row_h;
/* Move to next row by default (same_line will undo this if called) */
ctx->layout.cy = (int16_t)(ctx->layout.row_y + ctx->layout.row_h + ctx->style.spacing);
ctx->layout.cx = ctx->layout.origin_x;
ctx->layout.row_h = 0;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Label
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_label(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
int16_t lx = x;
int16_t ly = y;
int16_t lw = 0;
int16_t lh = 0;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
layout_resolve(ctx, &lx, &ly, &lw, &lh, fh);
syn_gfx_text(ctx->gfx, lx, ly, text, ctx->style.fg);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Separator
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_separator(SYN_IMGUI_Context *ctx, int16_t x, int16_t y, int16_t w)
{
SYN_ASSERT(ctx != NULL);
int16_t sx = x;
int16_t sy = y;
int16_t sw = w;
if (ctx->layout.in_layout) {
if (sx == 0 && sy == 0) {
sx = ctx->layout.cx;
sy = ctx->layout.cy;
}
if (sw == 0) {
sw = ctx->layout.width;
}
/* Advance cursor */
ctx->layout.cy = (int16_t)(sy + 1 + ctx->style.spacing);
ctx->layout.cx = ctx->layout.origin_x;
ctx->layout.row_h = 0;
}
syn_gfx_hline(ctx->gfx, sx, sy, sw, ctx->style.fg);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Spinner
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_spinner(SYN_IMGUI_Context *ctx, const char *label, int32_t *value, int32_t min,
int32_t max, int32_t step, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(value != NULL);
ctx->next_id++;
track_focus(ctx);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + 2 * ctx->style.padding);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
if (ctx->disabled_depth == 0) {
if (is_hit_test(ctx, x, y, w, h)) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool changed = false;
/* Encoder changes value when this widget is focused and not disabled */
if (ctx->disabled_depth == 0 && is_focused && ctx->enc_delta != 0) {
int32_t new_val = *value + ctx->enc_delta * step;
if (new_val > max)
new_val = min; /* wrap */
if (new_val < min)
new_val = max;
if (new_val != *value) {
*value = new_val;
changed = true;
}
}
/* ── Render ─────────────────────────────────────────────────────────── */
/* Focus outline */
if (is_focused) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Label on the left */
int16_t label_w = (int16_t)syn_gfx_text_width(ctx->gfx, label);
int16_t ty = (int16_t)(y + (h - fh) / 2);
syn_gfx_text(ctx->gfx, x + ctx->style.padding, ty, label, ctx->style.fg);
/* Value box on the right: "< 42 >" */
char val_str[16];
{
int32_t v = *value;
uint8_t i = 0;
if (v < 0) {
val_str[i++] = '-';
v = -v;
}
char tmp[12];
uint8_t j = 0;
do {
tmp[j++] = (char)('0' + (v % 10));
v /= 10;
} while (v > 0 && j < 11u);
while (j > 0u) {
val_str[i++] = tmp[--j];
}
val_str[i] = '\0';
}
int16_t val_w = (int16_t)syn_gfx_text_width(ctx->gfx, val_str);
int16_t arr_w = (int16_t)syn_gfx_font_width(ctx->gfx);
int16_t box_w = (int16_t)(arr_w + ctx->style.padding + val_w + ctx->style.padding + arr_w);
int16_t box_x = (int16_t)(x + w - box_w - ctx->style.padding);
(void)label_w; /* used for layout reference; value box is right-aligned */
/* "<" arrow */
syn_gfx_text(ctx->gfx, box_x, ty, "<", ctx->style.fg);
/* value */
syn_gfx_text(ctx->gfx, (int16_t)(box_x + arr_w + ctx->style.padding), ty, val_str,
ctx->style.fg);
/* ">" arrow */
syn_gfx_text(ctx->gfx,
(int16_t)(box_x + arr_w + ctx->style.padding + val_w + ctx->style.padding), ty,
">", ctx->style.fg);
return changed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Scroll Region
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_scroll_begin(SYN_IMGUI_Context *ctx, int16_t x, int16_t y, int16_t w, int16_t h,
int16_t *scroll)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(scroll != NULL);
ctx->scroll.in_scroll = true;
ctx->scroll.vp_x = x;
ctx->scroll.vp_y = y;
ctx->scroll.vp_w = w;
ctx->scroll.vp_h = h;
ctx->scroll.scroll_p = scroll;
ctx->scroll.focus_y = 0;
ctx->scroll.focus_h = 0;
/* Set clip rect to viewport */
syn_gfx_set_clip(ctx->gfx, x, y, w, h);
/* Start a layout region inside the viewport, shifted by scroll offset */
syn_imgui_layout_begin(ctx, x, (int16_t)(y - *scroll), w);
/* Remember where content starts for height computation */
ctx->scroll.content_start = ctx->layout.cy;
}
void syn_imgui_scroll_end(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
int16_t content_h = (int16_t)(ctx->layout.cy - ctx->scroll.content_start);
int16_t vp_h = ctx->scroll.vp_h;
int16_t max_scroll = (content_h > vp_h) ? (int16_t)(content_h - vp_h) : 0;
int16_t *sp = ctx->scroll.scroll_p;
/* Auto-scroll to keep focused widget visible */
if (ctx->scroll.focus_h > 0) {
int16_t fy = (int16_t)(ctx->scroll.focus_y + *sp);
int16_t fh = (int16_t)ctx->scroll.focus_h;
/* Widget is below viewport */
if (fy + fh > ctx->scroll.vp_y + vp_h) {
*sp = (int16_t)(ctx->scroll.focus_y + fh - vp_h);
}
/* Widget is above viewport */
if (fy < ctx->scroll.vp_y) {
*sp = (int16_t)ctx->scroll.focus_y;
}
}
/* Clamp scroll */
if (*sp < 0)
*sp = 0;
if (*sp > max_scroll)
*sp = max_scroll;
syn_imgui_layout_end(ctx);
/* Reset clip rect to full display */
syn_gfx_reset_clip(ctx->gfx);
/* Draw scroll indicator if content overflows */
if (content_h > vp_h && content_h > 0) {
int16_t bar_x = (int16_t)(ctx->scroll.vp_x + ctx->scroll.vp_w - 2);
int16_t track_h = vp_h;
int16_t thumb_h = (int16_t)((int32_t)vp_h * vp_h / content_h);
if (thumb_h < 3)
thumb_h = 3;
int16_t thumb_y =
(int16_t)(ctx->scroll.vp_y + (int32_t)*sp * (track_h - thumb_h) / max_scroll);
syn_gfx_rect_fill(ctx->gfx, bar_x, thumb_y, 2, thumb_h, ctx->style.fg);
}
ctx->scroll.in_scroll = false;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Toggle Switch
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_toggle(SYN_IMGUI_Context *ctx, const char *label, bool *state, int16_t x, int16_t y,
int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(state != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
/* Track focused widget position for scroll auto-follow */
if (ctx->focused_id == ctx->next_id && ctx->scroll.in_scroll) {
ctx->scroll.focus_y = (uint16_t)(y + *ctx->scroll.scroll_p - ctx->scroll.vp_y);
ctx->scroll.focus_h = (uint16_t)h;
}
/* Skip drawing if fully outside scroll viewport */
if (ctx->scroll.in_scroll) {
if (y + h <= ctx->scroll.vp_y || y >= ctx->scroll.vp_y + ctx->scroll.vp_h)
return false;
}
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool toggled = false;
/* Input */
if (ctx->disabled_depth == 0) {
bool touch_hit = is_hit_test(ctx, x, y, w, h);
if (touch_hit) {
ctx->focused_id = ctx->next_id;
*state = !*state;
toggled = true;
}
if (ctx->focused_id == ctx->next_id && ctx->btn_select) {
*state = !*state;
toggled = true;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
/* Draw focus outline */
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Draw label */
int16_t ty = (int16_t)(y + (h - fh) / 2);
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), ty, label, fg);
/* Draw toggle track (rounded rect) on the right side */
int16_t track_w = 16;
int16_t track_h = 8;
int16_t track_x = (int16_t)(x + w - track_w - ctx->style.padding);
int16_t track_y = (int16_t)(y + (h - track_h) / 2);
syn_gfx_rect_round(ctx->gfx, track_x, track_y, track_w, track_h, 3, fg);
/* Draw knob (filled circle) */
int16_t knob_r = 2;
int16_t knob_x =
*state ? (int16_t)(track_x + track_w - knob_r - 3) : (int16_t)(track_x + knob_r + 3);
int16_t knob_y = (int16_t)(track_y + track_h / 2);
if (*state) {
syn_gfx_circle_fill(ctx->gfx, knob_x, knob_y, knob_r, fg);
} else {
syn_gfx_circle(ctx->gfx, knob_x, knob_y, knob_r, fg);
}
return toggled;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Disabled State
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_begin_disabled(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
ctx->disabled_depth++;
}
void syn_imgui_end_disabled(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(ctx->disabled_depth > 0); /* unbalanced end_disabled */
ctx->disabled_depth--;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Text Alignment Helpers
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_label_colored(SYN_IMGUI_Context *ctx, const char *text, uint16_t color, int16_t x,
int16_t y)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
int16_t lx = x, ly = y, lw = 0, lh = 0;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
layout_resolve(ctx, &lx, &ly, &lw, &lh, fh);
syn_gfx_text(ctx->gfx, lx, ly, text, color);
}
void syn_imgui_label_right(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y,
int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
int16_t lx = x, ly = y, lw = w, lh = 0;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
layout_resolve(ctx, &lx, &ly, &lw, &lh, fh);
int16_t tw = (int16_t)syn_gfx_text_width(ctx->gfx, text);
syn_gfx_text(ctx->gfx, (int16_t)(lx + lw - tw), ly, text, ctx->style.fg);
}
void syn_imgui_label_centered(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y,
int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
int16_t lx = x, ly = y, lw = w, lh = 0;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
layout_resolve(ctx, &lx, &ly, &lw, &lh, fh);
int16_t tw = (int16_t)syn_gfx_text_width(ctx->gfx, text);
syn_gfx_text(ctx->gfx, (int16_t)(lx + (lw - tw) / 2), ly, text, ctx->style.fg);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Group Box
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_group_begin(SYN_IMGUI_Context *ctx, const char *title, int16_t x, int16_t y,
int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
/* Draw the border */
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, fg);
/* Draw title with background to "cut" the border */
if (title != NULL && title[0] != '\0') {
int16_t tw = (int16_t)syn_gfx_text_width(ctx->gfx, title);
int16_t tx = (int16_t)(x + ctx->style.padding + 2);
/* Clear the border behind the title text */
syn_gfx_hline(ctx->gfx, tx - 1, y, (int16_t)(tw + 2), ctx->style.bg);
syn_gfx_text(ctx->gfx, tx, (int16_t)(y - fh / 2), title, fg);
}
/* Push a sub-layout inside the group box */
int16_t inset = (int16_t)(ctx->style.padding + 2);
syn_imgui_layout_begin(ctx, (int16_t)(x + inset), (int16_t)(y + fh / 2 + ctx->style.spacing),
(int16_t)(w - inset * 2));
}
void syn_imgui_group_end(SYN_IMGUI_Context *ctx)
{
SYN_ASSERT(ctx != NULL);
syn_imgui_layout_end(ctx);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Tab Bar
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_tabs(SYN_IMGUI_Context *ctx, const char **labels, size_t count, int32_t *active,
int16_t x, int16_t y, int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(labels != NULL);
SYN_ASSERT(active != NULL);
SYN_ASSERT(count > 0);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t tab_h = (int16_t)(fh + ctx->style.padding * 2);
/* Resolve layout (use full width, tab height) */
int16_t lx = x, ly = y, lw = w, lh = 0;
layout_resolve(ctx, &lx, &ly, &lw, &lh, tab_h);
if (!ctx->layout.in_layout) {
lh = tab_h;
}
int16_t tab_w = (int16_t)(lw / (int16_t)count);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool is_focused = (ctx->focused_id == ctx->next_id);
bool changed = false;
/* Input — like combo: encoder cycles tabs when focused */
if (ctx->disabled_depth == 0 && is_focused && ctx->active_id == ctx->next_id) {
if (ctx->enc_delta != 0) {
int32_t new_val = *active + ctx->enc_delta;
if (new_val >= (int32_t)count)
new_val = 0;
if (new_val < 0)
new_val = (int32_t)count - 1;
if (new_val != *active) {
*active = new_val;
changed = true;
}
}
/* Select exits edit mode */
if (ctx->btn_select) {
ctx->active_id = 0;
}
} else if (ctx->disabled_depth == 0 && is_focused && ctx->btn_select) {
/* Enter edit mode */
ctx->active_id = ctx->next_id;
}
/* Touch input: detect which tab was tapped */
if (ctx->disabled_depth == 0 && ctx->touch_down) {
for (size_t i = 0; i < count; i++) {
int16_t tx = (int16_t)(lx + (int16_t)i * tab_w);
if (is_hit_test(ctx, tx, ly, tab_w, lh)) {
ctx->focused_id = ctx->next_id;
if ((int32_t)i != *active) {
*active = (int32_t)i;
changed = true;
}
break;
}
}
}
/* Draw tabs */
for (size_t i = 0; i < count; i++) {
int16_t tx = (int16_t)(lx + (int16_t)i * tab_w);
int16_t tw = (int16_t)syn_gfx_text_width(ctx->gfx, labels[i]);
int16_t text_y = (int16_t)(ly + ctx->style.padding);
/* Clip text to tab slot and center if it fits, else left-align with 1px margin */
if (tw <= tab_w - 2) {
int16_t text_x = (int16_t)(tx + (tab_w - tw) / 2);
syn_gfx_text(ctx->gfx, text_x, text_y, labels[i], fg);
} else {
/* Text too wide: use text_clipped to truncate within the tab */
syn_imgui_text_clipped(ctx, labels[i], (int16_t)(tx + 1), text_y, (int16_t)(tx + 1), ly,
(int16_t)(tab_w - 2), lh);
}
/* Active tab: underline */
if ((int32_t)i == *active) {
syn_gfx_hline(ctx->gfx, tx, (int16_t)(ly + lh - 1), tab_w, fg);
}
/* Divider between tabs (not after the last) */
if (i + 1 < count) {
int16_t div_x = (int16_t)(tx + tab_w - 1);
syn_gfx_vline(ctx->gfx, div_x, ly, lh, fg);
}
}
/* Focus highlight */
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect(ctx->gfx, lx, ly, lw, lh, ctx->style.highlight);
}
/* Draw bottom divider */
syn_gfx_hline(ctx->gfx, lx, (int16_t)(ly + lh), lw, fg);
return changed;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Bar Chart
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_bar_chart(SYN_IMGUI_Context *ctx, const char *title, const int32_t *data,
size_t count, int32_t min_val, int32_t max_val, int16_t x, int16_t y,
int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(data != NULL);
/* Resolve geometry from layout cursor */
layout_resolve(ctx, &x, &y, &w, &h, h > 0 ? h : (int16_t)32);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
/* Border */
syn_gfx_rect(ctx->gfx, x, y, w, h, fg);
/* Title */
if (title != NULL && title[0] != '\0') {
syn_gfx_text(ctx->gfx, (int16_t)(x + 4), (int16_t)(y + 4), title, fg);
}
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t plot_y = (int16_t)(y + fh + 6);
int16_t plot_h = (int16_t)(h - fh - 8);
int16_t plot_x = (int16_t)(x + 2);
int16_t plot_w = (int16_t)(w - 4);
if (count == 0 || plot_h <= 0 || plot_w <= 0)
return;
int32_t range = max_val - min_val;
if (range <= 0)
range = 1;
int16_t bar_w = (int16_t)(plot_w / (int16_t)count);
if (bar_w < 1)
bar_w = 1;
int16_t gap = (bar_w > 2) ? 1 : 0;
for (size_t i = 0; i < count; i++) {
int32_t val = data[i];
if (val < min_val)
val = min_val;
if (val > max_val)
val = max_val;
int16_t bar_h = (int16_t)((int32_t)(val - min_val) * plot_h / range);
if (bar_h < 1 && val > min_val)
bar_h = 1;
int16_t bx = (int16_t)(plot_x + (int16_t)i * bar_w + gap);
int16_t by = (int16_t)(plot_y + plot_h - bar_h);
syn_gfx_rect_fill(ctx->gfx, bx, by, (int16_t)(bar_w - gap), bar_h, fg);
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Icon Button
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_icon_button(SYN_IMGUI_Context *ctx, const uint8_t *icon, int16_t icon_w,
int16_t icon_h, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(icon != NULL);
int16_t default_h = (int16_t)(icon_h + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool clicked = false;
if (ctx->disabled_depth == 0) {
bool touch_hit = is_hit_test(ctx, x, y, w, h);
if (touch_hit) {
ctx->focused_id = ctx->next_id;
clicked = true;
}
if (ctx->focused_id == ctx->next_id && ctx->btn_select) {
clicked = true;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect_round_fill(ctx->gfx, x, y, w, h, 4, fg);
/* Draw icon inverted */
int16_t ix = (int16_t)(x + (w - icon_w) / 2);
int16_t iy = (int16_t)(y + (h - icon_h) / 2);
syn_gfx_bitmap(ctx->gfx, ix, iy, icon, icon_w, icon_h, ctx->style.bg);
} else {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 4, fg);
int16_t ix = (int16_t)(x + (w - icon_w) / 2);
int16_t iy = (int16_t)(y + (h - icon_h) / 2);
syn_gfx_bitmap(ctx->gfx, ix, iy, icon, icon_w, icon_h, fg);
}
return clicked;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Status Bar
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_status_bar(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y, int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
/* Resolve geometry from layout cursor */
uint8_t fh_sb = syn_gfx_text_height(ctx->gfx);
int16_t default_h_sb = (int16_t)(fh_sb + 4); /* separator line + text + padding */
int16_t lh_sb = 0;
layout_resolve(ctx, &x, &y, &w, &lh_sb, default_h_sb);
if (!ctx->layout.in_layout) {
lh_sb = default_h_sb;
}
uint16_t fg = ctx->style.fg;
/* Separator line above the text */
syn_gfx_hline(ctx->gfx, x, y, w, fg);
/* Text below the separator */
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), (int16_t)(y + 2), text, fg);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Separator Text (inspired by Dear ImGui SeparatorText)
* ─── Section Title ───────────────────────────────────────────────────────
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_separator_text(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y,
int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
uint16_t fg = ctx->style.fg;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t h = (int16_t)(fh + 2); /* height of this row */
/* Participate in the layout: resolve (x,y,w) from cursor, advance it */
int16_t lh = 0;
layout_resolve(ctx, &x, &y, &w, &lh, h);
if (!ctx->layout.in_layout) {
lh = h;
}
int16_t text_w = (int16_t)syn_gfx_text_width(ctx->gfx, text);
int16_t mid_y = (int16_t)(y + fh / 2);
int16_t gap = 4; /* pixels between line end and text */
/* Left line segment */
syn_gfx_hline(ctx->gfx, x, mid_y, gap, fg);
/* Text */
int16_t text_x = (int16_t)(x + gap * 2);
syn_gfx_text(ctx->gfx, text_x, y, text, fg);
/* Right line segment */
int16_t right_start = (int16_t)(text_x + text_w + gap);
int16_t right_end = (int16_t)(x + w);
if (right_end > right_start) {
syn_gfx_hline(ctx->gfx, right_start, mid_y, (int16_t)(right_end - right_start), fg);
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Checkbox Flags (inspired by Dear ImGui CheckboxFlags / Nuklear
* nk_checkbox_flags_label)
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_checkbox_flags(SYN_IMGUI_Context *ctx, const char *label, uint32_t *flags,
uint32_t mask, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(flags != NULL);
bool checked = (*flags & mask) == mask;
bool toggled = syn_imgui_checkbox(ctx, label, &checked, x, y, w, h);
if (toggled) {
if (checked) {
*flags |= mask;
} else {
*flags &= ~mask;
}
}
return toggled;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Value display (inspired by Dear ImGui Value / Nuklear nk_value_int)
* Draws "label: value" as a non-interactive text pair.
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_value_int(SYN_IMGUI_Context *ctx, const char *label, int32_t value, int16_t x,
int16_t y)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
/* Participate in layout: resolve position and advance cursor */
uint8_t fh_vi = syn_gfx_text_height(ctx->gfx);
int16_t lw_vi = 0, lh_vi = 0;
int16_t default_h_vi = (int16_t)(fh_vi + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &lw_vi, &lh_vi, default_h_vi);
(void)lw_vi;
uint16_t fg = ctx->style.fg;
/* Draw label */
syn_gfx_text(ctx->gfx, x, y, label, fg);
int16_t label_w = (int16_t)syn_gfx_text_width(ctx->gfx, label);
/* Format value */
char buf[16];
int32_t v = value;
uint8_t i = 0;
if (v < 0) {
buf[i++] = '-';
v = -v;
}
char tmp[12];
uint8_t j = 0;
do {
tmp[j++] = (char)('0' + (v % 10));
v /= 10;
} while (v > 0 && j < 11u);
while (j > 0u) {
buf[i++] = tmp[--j];
}
buf[i] = '\0';
/* Draw ": value" */
int16_t colon_x = (int16_t)(x + label_w);
syn_gfx_text(ctx->gfx, colon_x, y, ": ", fg);
int16_t colon_w = (int16_t)syn_gfx_text_width(ctx->gfx, ": ");
syn_gfx_text(ctx->gfx, (int16_t)(colon_x + colon_w), y, buf, fg);
}
/* ═══════════════════════════════════════════════════════════════════════════
* Progress bar with overlay text (inspired by Dear ImGui ProgressBar)
*
* overlay == NULL → auto "XX%" text
* overlay == "" → no text
* value < min → indeterminate mode (bouncing bar)
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_progress_bar_ex(SYN_IMGUI_Context *ctx, int32_t value, int32_t min, int32_t max,
const char *overlay, int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
/* Resolve geometry from layout cursor */
uint8_t fh_pb = syn_gfx_text_height(ctx->gfx);
int16_t default_h_pb = (int16_t)(fh_pb + ctx->style.padding * 2);
layout_resolve(ctx, &x, &y, &w, &h, default_h_pb);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool indeterminate = (value < min);
/* Outer frame */
syn_gfx_rect(ctx->gfx, x, y, w, h, fg);
if (max > min) {
int16_t inner_w = (int16_t)(w - 4);
if (indeterminate) {
/* Bouncing 20% wide bar based on low bits of value */
int32_t phase = (value < 0 ? -value : value) % (inner_w * 2);
int16_t bar_w = (int16_t)(inner_w / 5);
if (bar_w < 2)
bar_w = 2;
int16_t bar_x;
if (phase < inner_w) {
bar_x = (int16_t)(x + 2 + phase);
} else {
bar_x = (int16_t)(x + 2 + (inner_w * 2 - phase));
}
/* Clamp to bounds */
if (bar_x + bar_w > x + w - 2)
bar_w = (int16_t)(x + w - 2 - bar_x);
if (bar_w > 0) {
syn_gfx_rect_fill(ctx->gfx, bar_x, (int16_t)(y + 2), bar_w, (int16_t)(h - 4), fg);
}
} else {
/* Normal fill */
int32_t val = value;
if (val < min)
val = min;
if (val > max)
val = max;
int32_t range = max - min;
int32_t current = val - min;
int16_t fill_w = (int16_t)((current * inner_w) / range);
if (fill_w > 0) {
syn_gfx_rect_fill(ctx->gfx, (int16_t)(x + 2), (int16_t)(y + 2), fill_w,
(int16_t)(h - 4), fg);
}
}
}
/* Overlay text */
if (!indeterminate) {
char auto_buf[8];
const char *text = overlay;
if (text == NULL && max > min) {
/* Auto-generate "XX%" */
int32_t pct = SYN_CLAMP(((value - min) * 100) / (max - min), 0, 100);
size_t len = syn_fmt_int(auto_buf, sizeof(auto_buf) - 2, pct);
auto_buf[len++] = '%';
auto_buf[len] = '\0';
text = auto_buf;
}
if (text != NULL && text[0] != '\0') {
int16_t tw = (int16_t)syn_gfx_text_width(ctx->gfx, text);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t tx = (int16_t)(x + (w - tw) / 2);
int16_t ty = (int16_t)(y + (h - fh) / 2);
/* Draw text in inverted color if fill is under the text */
syn_gfx_text(ctx->gfx, tx, ty, text, fg);
}
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Selectable (inspired by Dear ImGui Selectable / microui mu_button with
* MU_OPT_NOFRAME)
* Full-width clickable row — no border, only a highlight fill when focused.
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_selectable(SYN_IMGUI_Context *ctx, const char *label, bool *selected, int16_t x,
int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(selected != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + 2 * ctx->style.padding);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool clicked = false;
if (ctx->disabled_depth == 0 && (touch_clicked || (is_focused && ctx->btn_select))) {
*selected = !(*selected);
clicked = true;
}
/* Draw filled background when selected or focused */
if (*selected) {
syn_gfx_rect_fill(ctx->gfx, x, y, w, h, fg);
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), (int16_t)(y + (h - fh) / 2),
label, ctx->style.bg);
} else if (is_focused && ctx->disabled_depth == 0) {
/* Dotted/outline highlight for focus without selection */
syn_gfx_rect(ctx->gfx, x, y, w, h, fg);
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), (int16_t)(y + (h - fh) / 2),
label, fg);
} else {
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), (int16_t)(y + (h - fh) / 2),
label, fg);
}
return clicked;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Collapsing Header (feature #5, inspired by all three libs)
*
* Renders "▸ Label" (collapsed) or "▾ Label" (expanded).
* Toggles *expanded on click. User gates the body:
* if (*expanded) { ... widgets ... }
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_collapsing_header(SYN_IMGUI_Context *ctx, const char *label, bool *expanded,
int16_t x, int16_t y, int16_t w, int16_t h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(label != NULL);
SYN_ASSERT(expanded != NULL);
uint8_t fh = syn_gfx_text_height(ctx->gfx);
int16_t default_h = (int16_t)(fh + 2 * ctx->style.padding);
layout_resolve(ctx, &x, &y, &w, &h, default_h);
ctx->next_id++;
track_focus(ctx);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
bool touch_clicked = false;
if (ctx->disabled_depth == 0) {
touch_clicked = is_hit_test(ctx, x, y, w, h);
if (touch_clicked) {
ctx->focused_id = ctx->next_id;
}
}
bool is_focused = (ctx->focused_id == ctx->next_id);
bool toggled = false;
if (ctx->disabled_depth == 0 && (touch_clicked || (is_focused && ctx->btn_select))) {
*expanded = !(*expanded);
toggled = true;
}
/* Draw focus outline */
if (is_focused && ctx->disabled_depth == 0) {
syn_gfx_rect_round(ctx->gfx, x, y, w, h, 3, ctx->style.highlight);
}
/* Draw arrow + label */
int16_t ty = (int16_t)(y + (h - fh) / 2);
const char *arrow = *expanded ? "-" : "+";
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding), ty, arrow, fg);
int16_t arrow_w = (int16_t)syn_gfx_text_width(ctx->gfx, arrow);
syn_gfx_text(ctx->gfx, (int16_t)(x + ctx->style.padding + arrow_w + 2), ty, label, fg);
/* Separator line below */
syn_gfx_hline(ctx->gfx, x, (int16_t)(y + h - 1), w, fg);
return toggled;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Text Word-Wrap (feature #6, inspired by microui mu_text)
*
* Renders multi-line word-wrapped text within width.
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_text_wrapped(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y,
int16_t w)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
uint8_t fh_tw = syn_gfx_text_height(ctx->gfx);
int16_t default_h_tw = (int16_t)(fh_tw + 1);
int16_t lh_tw = 0;
layout_resolve(ctx, &x, &y, &w, &lh_tw, default_h_tw);
if (!ctx->layout.in_layout) {
lh_tw = default_h_tw;
}
uint16_t fg = ctx->style.fg;
uint8_t fh = syn_gfx_text_height(ctx->gfx);
uint8_t fw = syn_gfx_font_width(ctx->gfx);
int16_t cx = x;
int16_t cy = y;
if (fw == 0 || w <= 0)
return;
const char *p = text;
while (*p) {
/* Find end of next word */
const char *word_start = p;
while (*p && *p != ' ' && *p != '\n')
p++;
int16_t word_len = (int16_t)(p - word_start);
int16_t word_w = (int16_t)(word_len * fw);
/* Wrap if this word doesn't fit on the current line */
if (cx + word_w > x + w && cx > x) {
cx = x;
cy = (int16_t)(cy + fh + 1);
}
/* Render character by character */
for (const char *c = word_start; c < word_start + word_len; c++) {
const char ch[2] = {*c, '\0'};
syn_gfx_text(ctx->gfx, cx, cy, ch, fg);
cx = (int16_t)(cx + fw);
}
/* Handle space / newline */
if (*p == ' ') {
cx = (int16_t)(cx + fw);
p++;
} else if (*p == '\n') {
cx = x;
cy = (int16_t)(cy + fh + 1);
p++;
}
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Layout Row (feature #7, inspired by microui mu_layout_row)
*
* Define N columns with specific widths. Subsequent widgets that use
* auto-layout (all coords 0) will fill them left-to-right.
* Positive width = fixed pixels. Negative width = fill remainder.
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_layout_row(SYN_IMGUI_Context *ctx, int items, const int16_t *widths, int16_t height)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(ctx->layout.in_layout);
/* Start a new row */
if (ctx->layout.row_h > 0) {
ctx->layout.cy = (int16_t)(ctx->layout.cy + ctx->layout.row_h + ctx->style.spacing);
ctx->layout.row_h = 0;
}
ctx->layout.cx = ctx->layout.origin_x;
ctx->layout.same_line = false;
/* Store row params in layout for widgets to consume */
ctx->layout.row_items = items;
ctx->layout.row_item_idx = 0;
ctx->layout.row_height = height;
if (widths != NULL && items > 0) {
int max = items;
if (max > SYN_IMGUI_MAX_ROW_COLS)
max = SYN_IMGUI_MAX_ROW_COLS;
for (int i = 0; i < max; i++) {
ctx->layout.row_widths[i] = widths[i];
}
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Visibility Culling (feature #8)
*
* Returns false if the widget rect is fully outside the scroll viewport.
* Widgets should still increment next_id for focus tracking.
* ═══════════════════════════════════════════════════════════════════════════ */
bool syn_imgui_widget_visible(const SYN_IMGUI_Context *ctx, int16_t y, int16_t h)
{
SYN_ASSERT(ctx != NULL);
if (!ctx->scroll.in_scroll)
return true; /* no scroll = always visible */
int16_t vp_top = ctx->scroll.vp_y;
int16_t vp_bot = (int16_t)(ctx->scroll.vp_y + ctx->scroll.vp_h);
/* Fully below viewport or fully above */
if (y >= vp_bot || y + h <= vp_top)
return false;
return true;
}
/* ═══════════════════════════════════════════════════════════════════════════
* Text Clipped (feature #9, inspired by microui mu_draw_control_text)
*
* Draws text, but clips it to a bounding rectangle using the canvas
* clip rect mechanism.
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_text_clipped(SYN_IMGUI_Context *ctx, const char *text, int16_t x, int16_t y,
int16_t clip_x, int16_t clip_y, int16_t clip_w, int16_t clip_h)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
/* text_clipped is a lower-level primitive used by other widgets; it does
* NOT participate in the layout cursor since clip_x/y/w/h are explicit.
* Callers that want layout advancement should call a higher-level widget. */
SYN_Canvas *c = (SYN_Canvas *)ctx->gfx;
/* Save current clip rect */
int16_t old_cx = c->clip_x;
int16_t old_cy = c->clip_y;
int16_t old_cw = c->clip_w;
int16_t old_ch = c->clip_h;
/* Set clip to widget bounds */
syn_canvas_set_clip(c, clip_x, clip_y, clip_w, clip_h);
/* Draw text (canvas will clip at pixel level) */
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
syn_gfx_text(ctx->gfx, x, y, text, fg);
/* Restore clip rect */
if (old_cw > 0 && old_ch > 0) {
syn_canvas_set_clip(c, old_cx, old_cy, old_cw, old_ch);
} else {
syn_canvas_reset_clip(c);
}
}
/* ═══════════════════════════════════════════════════════════════════════════
* Text Marquee
*
* Scrolling text label. If the text fits, it's drawn statically.
* If it overflows, it scrolls left with pauses at start and end.
*
* The caller owns the scroll state — just declare:
* static int16_t offset = 0;
*
* speed: pixels per call (1-2 typical). Pause ticks at each end = 2/speed
* cycles worth.
* ═══════════════════════════════════════════════════════════════════════════ */
void syn_imgui_text_marquee(SYN_IMGUI_Context *ctx, const char *text, int16_t *offset, int16_t x,
int16_t y, int16_t w, int16_t speed)
{
SYN_ASSERT(ctx != NULL);
SYN_ASSERT(text != NULL);
SYN_ASSERT(offset != NULL);
uint16_t fg = ctx->disabled_depth > 0 ? ctx->style.disabled : ctx->style.fg;
int16_t text_w = (int16_t)syn_gfx_text_width(ctx->gfx, text);
if (speed <= 0)
speed = 1;
/* Resolve geometry from layout cursor before any drawing */
uint8_t fh_mq = syn_gfx_text_height(ctx->gfx);
int16_t default_h_mq = (int16_t)(fh_mq + 2);
int16_t lh_mq = 0;
layout_resolve(ctx, &x, &y, &w, &lh_mq, default_h_mq);
if (!ctx->layout.in_layout) {
lh_mq = default_h_mq;
}
/* Text fits — draw statically, reset offset */
if (text_w <= w) {
syn_gfx_text(ctx->gfx, x, y, text, fg);
*offset = 0;
return;
}
/* Overflow amount (how far we need to scroll) */
int16_t overflow = (int16_t)(text_w - w);
/* Pause duration at each end (in calls). ~30 calls at speed=1 */
int16_t pause = (int16_t)(30 / speed);
if (pause < 4)
pause = 4;
/* Total cycle: pause + scroll_right + pause + scroll_left */
int16_t total_cycle = (int16_t)(pause + overflow + pause + overflow);
/* Normalize offset into cycle */
int16_t pos = *offset;
if (pos < 0)
pos = 0;
if (pos >= total_cycle)
pos = 0;
/* Determine text_x shift */
int16_t shift;
if (pos < pause) {
/* Phase 1: paused at start */
shift = 0;
} else if (pos < pause + overflow) {
/* Phase 2: scrolling left */
shift = (int16_t)(pos - pause);
} else if (pos < pause + overflow + pause) {
/* Phase 3: paused at end */
shift = overflow;
} else {
/* Phase 4: scrolling right (back) */
shift = (int16_t)(overflow - (pos - pause - overflow - pause));
}
/* Clip to widget bounds and draw */
SYN_Canvas *c = (SYN_Canvas *)ctx->gfx;
int16_t old_cx = c->clip_x;
int16_t old_cy = c->clip_y;
int16_t old_cw = c->clip_w;
int16_t old_ch = c->clip_h;
syn_canvas_set_clip(c, x, y, w, lh_mq);
syn_gfx_text(ctx->gfx, (int16_t)(x - shift), y, text, fg);
if (old_cw > 0 && old_ch > 0) {
syn_canvas_set_clip(c, old_cx, old_cy, old_cw, old_ch);
} else {
syn_canvas_reset_clip(c);
}
/* Advance offset */
*offset = (int16_t)(pos + speed);
if (*offset >= total_cycle)
*offset = 0;
}
#endif /* SYN_USE_IMGUI */