feat: Fix USB mouse support, GUI wkipedia app, icon refresh, file manager improvements, more

This commit is contained in:
2026-02-20 11:33:06 +01:00
parent 359ef3ba53
commit 4c6b783832
21 changed files with 1513 additions and 160 deletions
+5
View File
@@ -45,6 +45,11 @@ struct DesktopState {
SvgIcon icon_save;
SvgIcon icon_home;
SvgIcon icon_exec;
SvgIcon icon_wikipedia;
SvgIcon icon_folder_lg;
SvgIcon icon_file_lg;
SvgIcon icon_exec_lg;
bool net_popup_open;
Zenith::NetCfg cached_net_cfg;
+367 -20
View File
@@ -31,10 +31,51 @@ struct SvgEdge {
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
static constexpr int SVG_MAX_EDGES = 4096;
static constexpr int SVG_MAX_PATH_LEN = 4096;
static constexpr int SVG_MAX_EDGES = 8192;
static constexpr int SVG_MAX_PATH_LEN = 8192;
static constexpr int SVG_MAX_FILE_SIZE = 32768;
static constexpr int SVG_BEZIER_STEPS = 8;
static constexpr int SVG_MAX_GRADIENTS = 8;
// Gradient color table — stores first stop color for url(#id) resolution
struct SvgGradient {
char id[32];
Color color; // first stop-color
};
struct SvgGradientTable {
SvgGradient entries[SVG_MAX_GRADIENTS];
int count;
void clear() { count = 0; }
void add(const char* id, Color c) {
if (count >= SVG_MAX_GRADIENTS) return;
int i = 0;
while (id[i] && i < 31) { entries[count].id[i] = id[i]; i++; }
entries[count].id[i] = '\0';
entries[count].color = c;
count++;
}
// Look up gradient by id. Returns true if found.
bool lookup(const char* id, Color* out) const {
for (int i = 0; i < count; i++) {
const char* a = entries[i].id;
const char* b = id;
bool match = true;
while (*a && *b) {
if (*a != *b) { match = false; break; }
a++; b++;
}
if (match && *a == '\0' && *b == '\0') {
*out = entries[i].color;
return true;
}
}
return false;
}
};
// ---------------------------------------------------------------------------
// Fixed-point number parser (NO floating point)
@@ -196,7 +237,7 @@ inline int svg_parse_int(const char* s) {
return v;
}
// Parse a hex color like "#5c616c" into a Color.
// Parse a hex color like "#5c616c" or "#fff" into a Color.
inline Color svg_parse_hex_color(const char* s) {
if (*s == '#') ++s;
auto hexval = [](char c) -> uint8_t {
@@ -205,6 +246,20 @@ inline Color svg_parse_hex_color(const char* s) {
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
return 0;
};
// Count hex digits
int len = 0;
for (const char* p = s; *p; ++p) {
if ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))
++len;
else break;
}
if (len == 3) {
// 3-digit shorthand: #rgb → #rrggbb
uint8_t r = hexval(s[0]); r = (r << 4) | r;
uint8_t g = hexval(s[1]); g = (g << 4) | g;
uint8_t b = hexval(s[2]); b = (b << 4) | b;
return Color::from_rgb(r, g, b);
}
uint8_t r = (hexval(s[0]) << 4) | hexval(s[1]);
uint8_t g = (hexval(s[2]) << 4) | hexval(s[3]);
uint8_t b = (hexval(s[4]) << 4) | hexval(s[5]);
@@ -225,6 +280,8 @@ struct SvgEdgeList {
capacity = cap;
}
void clear() { count = 0; }
void add(fixed_t x0, fixed_t y0, fixed_t x1, fixed_t y1) {
if (count >= capacity) return;
// skip horizontal edges (they don't contribute to scanline crossings)
@@ -758,6 +815,160 @@ inline void svg_rasterize(const SvgEdgeList& el, uint32_t* pixels, int w, int h,
zenith::free(isect);
}
// ---------------------------------------------------------------------------
// Resolve a fill value that might be url(#id) against gradient table
// Returns: 1 = resolved, 0 = not a url() ref, -1 = fill="none"
// ---------------------------------------------------------------------------
inline int svg_resolve_fill_value(const char* val, Color* out_color, const SvgGradientTable* grads) {
if (svg_strncmp(val, "none", 4)) return -1;
if (*val == '#') { *out_color = svg_parse_hex_color(val); return 1; }
if (grads && svg_strncmp(val, "url(#", 5)) {
// Extract id from url(#id)
const char* id_start = val + 5;
char id_buf[32];
int i = 0;
while (id_start[i] && id_start[i] != ')' && i < 31) {
id_buf[i] = id_start[i];
i++;
}
id_buf[i] = '\0';
if (grads->lookup(id_buf, out_color)) return 1;
}
return 0; // currentColor or unresolved
}
// ---------------------------------------------------------------------------
// Per-element fill color extraction
// Returns: 1 = color found in out_color, 0 = use default, -1 = fill="none"
// ---------------------------------------------------------------------------
inline int svg_get_element_fill(const char* elem, int elemLen, Color* out_color,
const SvgGradientTable* grads = nullptr) {
char buf[128];
// Check style="..." first (higher CSS priority)
int sLen = svg_get_attr(elem, elemLen, " style", buf, sizeof(buf));
if (sLen > 0) {
// Search for "fill:" that isn't part of "fill-rule:" or "fill-opacity:"
const char* fp = svg_strstr(buf, sLen, "fill:");
if (fp) {
// Verify this is standalone "fill:" not "-fill:" or similar
if (fp > buf && *(fp - 1) != ';' && *(fp - 1) != ' ' && *(fp - 1) != '\t') {
// Part of another property name, skip
} else {
fp += 5;
while (*fp == ' ') ++fp;
return svg_resolve_fill_value(fp, out_color, grads);
}
}
}
// Check fill="..." attribute
int fLen = svg_get_attr(elem, elemLen, " fill", buf, sizeof(buf));
if (fLen > 0) {
return svg_resolve_fill_value(buf, out_color, grads);
}
return 0; // no fill specified
}
// ---------------------------------------------------------------------------
// Per-element opacity (0-255)
// ---------------------------------------------------------------------------
inline int svg_get_element_opacity(const char* elem, int elemLen) {
char buf[32];
if (svg_get_attr(elem, elemLen, " opacity", buf, sizeof(buf)) > 0) {
fixed_t val;
svg_parse_fixed(buf, &val);
int alpha = (int)(((int64_t)val * 255) >> 16);
if (alpha < 0) alpha = 0;
if (alpha > 255) alpha = 255;
return alpha;
}
return 255;
}
// ---------------------------------------------------------------------------
// Check if element references a filter (shadow/blur layers)
// ---------------------------------------------------------------------------
inline bool svg_element_has_filter(const char* elem, int elemLen) {
char buf[64];
return svg_get_attr(elem, elemLen, " filter", buf, sizeof(buf)) > 0;
}
// ---------------------------------------------------------------------------
// Scanline rasterizer with alpha blending (for multi-color SVGs)
// ---------------------------------------------------------------------------
inline void svg_rasterize_blend(const SvgEdgeList& el, uint32_t* pixels, int w, int h,
uint32_t fill, int alpha) {
if (el.count == 0) return;
int maxIsect = el.count + 16;
fixed_t* isect = (fixed_t*)zenith::alloc(maxIsect * sizeof(fixed_t));
uint32_t fr = (fill >> 16) & 0xFF;
uint32_t fg = (fill >> 8) & 0xFF;
uint32_t fb = fill & 0xFF;
for (int y = 0; y < h; ++y) {
fixed_t scanY = int_to_fixed(y) + (1 << 15);
int isectCount = 0;
for (int i = 0; i < el.count; ++i) {
const SvgEdge& e = el.edges[i];
fixed_t ey0 = e.y0, ey1 = e.y1;
fixed_t emin = ey0 < ey1 ? ey0 : ey1;
fixed_t emax = ey0 > ey1 ? ey0 : ey1;
if (scanY < emin || scanY >= emax) continue;
fixed_t dy = ey1 - ey0;
if (dy == 0) continue;
fixed_t dx = e.x1 - e.x0;
fixed_t t_num = scanY - ey0;
fixed_t x_int = e.x0 + (int32_t)(((int64_t)dx * t_num) / dy);
if (isectCount < maxIsect)
isect[isectCount++] = x_int;
}
for (int i = 1; i < isectCount; ++i) {
fixed_t key = isect[i];
int j = i - 1;
while (j >= 0 && isect[j] > key) {
isect[j + 1] = isect[j];
--j;
}
isect[j + 1] = key;
}
for (int i = 0; i + 1 < isectCount; i += 2) {
int x0 = fixed_to_int(isect[i]);
int x1 = fixed_to_int(isect[i + 1]);
if (x0 < 0) x0 = 0;
if (x1 > w) x1 = w;
if (alpha >= 255) {
for (int x = x0; x < x1; ++x)
pixels[y * w + x] = fill;
} else {
uint32_t sa = (uint32_t)alpha;
uint32_t inv_sa = 255 - sa;
for (int x = x0; x < x1; ++x) {
uint32_t dst = pixels[y * w + x];
uint32_t da = (dst >> 24) & 0xFF;
uint32_t dr = (dst >> 16) & 0xFF;
uint32_t dg = (dst >> 8) & 0xFF;
uint32_t db = dst & 0xFF;
uint32_t out_a = sa + (da * inv_sa + 127) / 255;
uint32_t rr = (fr * sa + dr * inv_sa + 128) / 255;
uint32_t gg = (fg * sa + dg * inv_sa + 128) / 255;
uint32_t bb = (fb * sa + db * inv_sa + 128) / 255;
pixels[y * w + x] = (out_a << 24) | (rr << 16) | (gg << 8) | bb;
}
}
}
}
zenith::free(isect);
}
// ---------------------------------------------------------------------------
// SVG document parser: extract paths, circles, rects and rasterize
// ---------------------------------------------------------------------------
@@ -769,8 +980,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
// Clear to transparent
svg_memset(icon.pixels, 0, target_w * target_h * sizeof(uint32_t));
uint32_t fill_px = fill_color.to_pixel();
// Parse SVG dimensions: width and height
int svg_w = 16, svg_h = 16;
fixed_t vb_x = 0, vb_y = 0, vb_w = 0, vb_h = 0;
@@ -818,11 +1027,55 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
fixed_t scale_x = vb_w > 0 ? fixed_div(int_to_fixed(target_w), vb_w) : int_to_fixed(1);
fixed_t scale_y = vb_h > 0 ? fixed_div(int_to_fixed(target_h), vb_h) : int_to_fixed(1);
// Edge list
// Pre-parse gradient definitions from <defs> for url(#id) fill resolution
SvgGradientTable grads;
grads.clear();
{
const char* dp = svg_data;
const char* dend = svg_data + svg_len;
while (dp < dend) {
const char* gp = svg_strstr(dp, (int)(dend - dp), "<linearGradient");
if (!gp) {
gp = svg_strstr(dp, (int)(dend - dp), "<radialGradient");
}
if (!gp) break;
// Find end of gradient block (</linearGradient> or </radialGradient>)
const char* gend = svg_strstr(gp, (int)(dend - gp), "</linearGradient>");
if (!gend) gend = svg_strstr(gp, (int)(dend - gp), "</radialGradient>");
if (!gend) gend = svg_strstr(gp, (int)(dend - gp), "/>");
if (!gend) break;
// Extract gradient id
int gtag_end = 0;
while (gp + gtag_end < dend && gp[gtag_end] != '>') ++gtag_end;
char grad_id[32];
int id_len = svg_get_attr(gp, gtag_end + 1, " id", grad_id, sizeof(grad_id));
if (id_len > 0) {
// Find first <stop and extract stop-color
const char* stop = svg_strstr(gp, (int)(gend - gp), "<stop");
if (stop) {
int stop_end = 0;
while (stop + stop_end < dend && stop[stop_end] != '>') ++stop_end;
char sc_buf[32];
if (svg_get_attr(stop, stop_end + 1, " stop-color", sc_buf, sizeof(sc_buf)) > 0) {
if (sc_buf[0] == '#') {
grads.add(grad_id, svg_parse_hex_color(sc_buf));
}
}
}
}
dp = gend + 1;
}
}
// Shared edge list (cleared per element for multi-color support)
SvgEdgeList el;
el.init(SVG_MAX_EDGES);
// Scan for <path, <circle, <rect elements
// Scan for <path, <circle, <rect elements — rasterize each individually
// Skip elements inside <defs> blocks (they define reusable items, not rendered directly)
const char* p = svg_data;
const char* end = svg_data + svg_len;
@@ -833,20 +1086,46 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
int remaining = (int)(end - p);
// Skip <defs>...</defs> blocks entirely
if (remaining > 5 && svg_strncmp(p, "<defs", 5) && (svg_char_is_ws(p[5]) || p[5] == '>')) {
const char* defs_end = svg_strstr(p, remaining, "</defs>");
if (defs_end) {
p = defs_end + 7; // skip past </defs>
} else {
p += 5;
}
continue;
}
// Check for <path
if (remaining > 5 && svg_strncmp(p, "<path", 5) && (svg_char_is_ws(p[5]) || p[5] == '/')) {
// Find end of this element
const char* elem_start = p;
const char* elem_end = p;
while (elem_end < end && *elem_end != '>') ++elem_end;
if (elem_end < end) ++elem_end; // include '>'
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
// Extract d attribute
// Skip filter-referenced elements (shadow/blur layers)
if (svg_element_has_filter(elem_start, elem_len)) {
p = elem_end;
continue;
}
// Determine fill color for this element
Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads);
if (fillResult == -1) { p = elem_end; continue; } // fill="none"
int alpha = svg_get_element_opacity(elem_start, elem_len);
// Extract and rasterize path
char d_buf[SVG_MAX_PATH_LEN];
int d_len = svg_get_attr(elem_start, elem_len, " d", d_buf, SVG_MAX_PATH_LEN);
if (d_len > 0) {
el.clear();
svg_path_to_edges(el, d_buf, d_len, scale_x, scale_y, vb_x, vb_y);
if (el.count > 0)
svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha);
}
p = elem_end;
@@ -861,6 +1140,17 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
if (svg_element_has_filter(elem_start, elem_len)) {
p = elem_end;
continue;
}
Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads);
if (fillResult == -1) { p = elem_end; continue; }
int alpha = svg_get_element_opacity(elem_start, elem_len);
char attr_buf[32];
fixed_t cx = 0, cy = 0, r = 0;
if (svg_get_attr(elem_start, elem_len, " cx", attr_buf, sizeof(attr_buf)) > 0)
@@ -870,15 +1160,16 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
if (svg_get_attr(elem_start, elem_len, " r", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &r);
// Scale to target coordinates
fixed_t scx = fixed_mul(cx - vb_x, scale_x);
fixed_t scy = fixed_mul(cy - vb_y, scale_y);
fixed_t srx = fixed_mul(r, scale_x);
fixed_t sry = fixed_mul(r, scale_y);
// Use average of scaled radii
fixed_t sr = (srx + sry) >> 1;
el.clear();
svg_circle_edges(el, scx, scy, sr);
if (el.count > 0)
svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha);
p = elem_end;
continue;
@@ -892,6 +1183,17 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
if (elem_end < end) ++elem_end;
int elem_len = (int)(elem_end - elem_start);
if (svg_element_has_filter(elem_start, elem_len)) {
p = elem_end;
continue;
}
Color elem_color = fill_color;
int fillResult = svg_get_element_fill(elem_start, elem_len, &elem_color, &grads);
if (fillResult == -1) { p = elem_end; continue; }
int alpha = svg_get_element_opacity(elem_start, elem_len);
char attr_buf[32];
fixed_t rx_val = 0, ry_val = 0, rw = 0, rh = 0, rrx = 0, rry = 0;
@@ -908,7 +1210,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
if (svg_get_attr(elem_start, elem_len, " ry", attr_buf, sizeof(attr_buf)) > 0)
svg_parse_fixed(attr_buf, &rry);
// Scale all to target pixel space
fixed_t sx = fixed_mul(rx_val - vb_x, scale_x);
fixed_t sy = fixed_mul(ry_val - vb_y, scale_y);
fixed_t sw = fixed_mul(rw, scale_x);
@@ -916,7 +1217,10 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
fixed_t srx = fixed_mul(rrx, scale_x);
fixed_t sry = fixed_mul(rry, scale_y);
el.clear();
svg_rect_edges(el, sx, sy, sw, sh, srx, sry);
if (el.count > 0)
svg_rasterize_blend(el, icon.pixels, target_w, target_h, elem_color.to_pixel(), alpha);
p = elem_end;
continue;
@@ -925,11 +1229,6 @@ inline SvgIcon svg_render(const char* svg_data, int svg_len, int target_w, int t
++p;
}
// Rasterize all accumulated edges
if (el.count > 0) {
svg_rasterize(el, icon.pixels, target_w, target_h, fill_px);
}
zenith::free(el.edges);
return icon;
}
@@ -954,9 +1253,57 @@ inline SvgIcon svg_load(const char* vfs_path, int target_w, int target_h, Color
zenith::close(fd);
buf[size] = '\0';
SvgIcon icon = svg_render(buf, (int)size, target_w, target_h, fill_color);
// 4x supersampling: render at 4x resolution, then downsample with box filter
static constexpr int SS = 4;
int hi_w = target_w * SS;
int hi_h = target_h * SS;
SvgIcon hi = svg_render(buf, (int)size, hi_w, hi_h, fill_color);
zenith::free(buf);
return icon;
if (!hi.pixels) return {nullptr, 0, 0};
// Allocate final icon at target resolution
uint32_t* out = (uint32_t*)zenith::alloc(target_w * target_h * 4);
for (int i = 0; i < target_w * target_h; i++) out[i] = 0;
// Downsample: average each SSxSS block using premultiplied alpha
for (int dy = 0; dy < target_h; dy++) {
for (int dx = 0; dx < target_w; dx++) {
uint32_t sum_a = 0, sum_pr = 0, sum_pg = 0, sum_pb = 0;
for (int sy = 0; sy < SS; sy++) {
for (int sx = 0; sx < SS; sx++) {
uint32_t px = hi.pixels[(dy * SS + sy) * hi_w + (dx * SS + sx)];
uint32_t a = (px >> 24) & 0xFF;
uint32_t r = (px >> 16) & 0xFF;
uint32_t g = (px >> 8) & 0xFF;
uint32_t b = px & 0xFF;
// Premultiply before averaging (rasterizer outputs straight alpha)
sum_a += a;
sum_pr += r * a;
sum_pg += g * a;
sum_pb += b * a;
}
}
uint32_t avg_a = sum_a / (SS * SS);
// Un-premultiply for final straight-alpha output
uint32_t avg_r = 0, avg_g = 0, avg_b = 0;
if (sum_a > 0) {
avg_r = sum_pr / sum_a;
avg_g = sum_pg / sum_a;
avg_b = sum_pb / sum_a;
if (avg_r > 255) avg_r = 255;
if (avg_g > 255) avg_g = 255;
if (avg_b > 255) avg_b = 255;
}
out[dy * target_w + dx] = (avg_a << 24) | (avg_r << 16) | (avg_g << 8) | avg_b;
}
}
zenith::free(hi.pixels);
return {out, target_w, target_h};
}
// ---------------------------------------------------------------------------