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
+107 -2
View File
@@ -13,9 +13,28 @@
#include <Memory/HHDM.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp>
using namespace Kt;
static void BusyWaitMs(uint64_t ms) {
uint64_t flags;
asm volatile("pushfq; pop %0" : "=r"(flags));
if (flags & (1 << 9)) {
// Interrupts enabled — use timer-based delay
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < ms) {
asm volatile("pause" ::: "memory");
}
} else {
// Interrupts disabled (e.g. timer tick context) — use I/O port delay
// Each outb to port 0x80 takes ~1µs on x86
for (uint64_t i = 0; i < ms * 1000; i++) {
asm volatile("outb %%al, $0x80" ::: "memory");
}
}
}
// Access xHCI internal state needed during enumeration
namespace Drivers::USB::Xhci {
extern volatile uint32_t g_cmdCompletionSlotId;
@@ -163,16 +182,99 @@ namespace Drivers::USB::UsbDevice {
inputCtx->EP[0].Field2 = 8;
// -----------------------------------------------------------------
// Step 4: Address Device command
// Step 4a: Address Device (BSR=1) — initialize slot without SET_ADDRESS
// -----------------------------------------------------------------
Xhci::TRB addrTrb = {};
uint64_t inputCtxPhys = Memory::SubHHDM(inputCtx);
addrTrb.Parameter0 = (uint32_t)(inputCtxPhys & 0xFFFFFFFF);
addrTrb.Parameter1 = (uint32_t)(inputCtxPhys >> 32);
addrTrb.Control = (Xhci::TRB_ADDRESS_DEVICE << Xhci::TRB_TYPE_SHIFT)
| Xhci::TRB_BSR
| ((uint32_t)slotId << 24);
cc = Xhci::SendCommand(addrTrb);
if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(ERROR, "USB") << "Address Device (BSR=1) failed, slot="
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
dev->Active = false;
return 0;
}
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " initialized (BSR=1)";
// -----------------------------------------------------------------
// Step 4b: GET_DESCRIPTOR (Device, 8 bytes) — read bMaxPacketSize0
// -----------------------------------------------------------------
uint8_t partialDesc[8] = {};
cc = Xhci::ControlTransfer(slotId, REQTYPE_DEV_TO_HOST, REQ_GET_DESCRIPTOR,
(DESC_DEVICE << 8), 0, 8,
partialDesc, true);
if (cc != Xhci::CC_SUCCESS && cc != Xhci::CC_SHORT_PACKET) {
KernelLogStream(ERROR, "USB") << "GET_DESCRIPTOR(8-byte) failed, cc=" << (uint64_t)cc;
dev->Active = false;
return 0;
}
uint8_t bMaxPacketSize0 = partialDesc[7];
if (bMaxPacketSize0 == 0) bMaxPacketSize0 = maxPacket; // fallback
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
<< ": bMaxPacketSize0=" << (uint64_t)bMaxPacketSize0;
// -----------------------------------------------------------------
// Step 4c: Evaluate Context — update EP0 max packet size if needed
// -----------------------------------------------------------------
if (bMaxPacketSize0 != maxPacket) {
auto* evalCtx = (Xhci::InputContext*)Memory::g_pfa->AllocateZeroed();
// Only updating EP0 — set AddFlags bit 1 (EP0), no slot context needed
evalCtx->ICC.AddFlags = (1 << 1);
// Copy current EP0 context and update max packet size
evalCtx->EP[0] = dev->OutputContext->EP[0];
evalCtx->EP[0].Field1 = (evalCtx->EP[0].Field1 & 0x0000FFFF)
| ((uint32_t)bMaxPacketSize0 << 16);
Xhci::TRB evalTrb = {};
uint64_t evalCtxPhys = Memory::SubHHDM(evalCtx);
evalTrb.Parameter0 = (uint32_t)(evalCtxPhys & 0xFFFFFFFF);
evalTrb.Parameter1 = (uint32_t)(evalCtxPhys >> 32);
evalTrb.Control = (Xhci::TRB_EVALUATE_CONTEXT << Xhci::TRB_TYPE_SHIFT)
| ((uint32_t)slotId << 24);
cc = Xhci::SendCommand(evalTrb);
if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(WARNING, "USB") << "Evaluate Context failed, slot="
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
// Non-fatal: continue with original max packet size
} else {
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId
<< ": EP0 max packet updated to " << (uint64_t)bMaxPacketSize0;
}
}
// -----------------------------------------------------------------
// Step 4d: Address Device (BSR=0) — actually send SET_ADDRESS
// -----------------------------------------------------------------
// Update input context EP0 to current ring position and actual max
// packet size. BSR=0 re-initializes the output EP0 context from the
// input context, so both fields must reflect reality.
uint64_t curDeq = dev->EP0RingPhys + (uint64_t)dev->EP0RingEnqueue * sizeof(Xhci::TRB);
if (dev->EP0RingCCS) {
curDeq |= 1; // DCS bit
}
inputCtx->EP[0].TRDequeuePtr = curDeq;
inputCtx->EP[0].Field1 = (3 << 1)
| (Xhci::EP_TYPE_CONTROL << 3)
| ((uint32_t)bMaxPacketSize0 << 16);
Xhci::TRB addrTrb2 = {};
addrTrb2.Parameter0 = (uint32_t)(inputCtxPhys & 0xFFFFFFFF);
addrTrb2.Parameter1 = (uint32_t)(inputCtxPhys >> 32);
addrTrb2.Control = (Xhci::TRB_ADDRESS_DEVICE << Xhci::TRB_TYPE_SHIFT)
| ((uint32_t)slotId << 24);
cc = Xhci::SendCommand(addrTrb2);
if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(ERROR, "USB") << "Address Device failed, slot="
<< (uint64_t)slotId << " cc=" << (uint64_t)cc;
@@ -180,10 +282,13 @@ namespace Drivers::USB::UsbDevice {
return 0;
}
// Set-address recovery time (USB spec requires >= 2ms, use 10ms for safety)
BusyWaitMs(10);
KernelLogStream(INFO, "USB") << "Slot " << (uint64_t)slotId << " addressed";
// -----------------------------------------------------------------
// Step 5: GET_DESCRIPTOR (Device)
// Step 5: GET_DESCRIPTOR (Device, full 18 bytes)
// -----------------------------------------------------------------
DeviceDescriptor devDesc = {};
cc = Xhci::ControlTransfer(slotId, REQTYPE_DEV_TO_HOST, REQ_GET_DESCRIPTOR,
+28 -4
View File
@@ -16,9 +16,28 @@
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Timekeeping/ApicTimer.hpp>
using namespace Kt;
static void BusyWaitMs(uint64_t ms) {
uint64_t flags;
asm volatile("pushfq; pop %0" : "=r"(flags));
if (flags & (1 << 9)) {
// Interrupts enabled — use timer-based delay
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < ms) {
asm volatile("pause" ::: "memory");
}
} else {
// Interrupts disabled (e.g. timer tick context) — use I/O port delay
// Each outb to port 0x80 takes ~1µs on x86
for (uint64_t i = 0; i < ms * 1000; i++) {
asm volatile("outb %%al, $0x80" ::: "memory");
}
}
}
namespace Drivers::USB::Xhci {
// -------------------------------------------------------------------------
@@ -568,8 +587,9 @@ namespace Drivers::USB::Xhci {
if (alreadyActive) continue;
if (portsc & PORTSC_PED) {
// Already enabled — enumerate directly
// Already enabled — enumerate after recovery delay
uint32_t speed = (portsc >> 10) & 0xF;
BusyWaitMs(10);
UsbDevice::EnumerateDevice(port + 1, speed);
} else {
// Need port reset first
@@ -602,6 +622,9 @@ namespace Drivers::USB::Xhci {
WriteOp(OP_PORTSC_BASE + port * OP_PORTSC_STRIDE,
(portsc & PORTSC_PRESERVE) | PORTSC_CHANGE_BITS);
// Post-reset recovery delay (USB spec requires >= 10ms)
BusyWaitMs(10);
UsbDevice::EnumerateDevice(port + 1, speed);
}
} else {
@@ -901,9 +924,7 @@ namespace Drivers::USB::Xhci {
}
}
// Wait for port power to stabilize (~20ms)
for (int i = 0; i < 20000000; i++) {
asm volatile("" ::: "memory");
}
BusyWaitMs(20);
KernelLogStream(OK, "xHCI") << "All ports powered";
// -----------------------------------------------------------------
@@ -962,6 +983,9 @@ namespace Drivers::USB::Xhci {
KernelLogStream(OK, "xHCI") << "Port " << base::dec << (uint64_t)(port + 1)
<< ": reset complete, speed=" << speedStr;
// Post-reset recovery delay (USB spec requires >= 10ms)
BusyWaitMs(10);
// Enumerate the device (port IDs are 1-based)
UsbDevice::EnumerateDevice(port + 1, speed);
}
+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};
}
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -63,7 +63,7 @@ LDFLAGS := \
# ---- Source files ----
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
+281 -111
View File
@@ -26,6 +26,7 @@ struct FileManagerState {
uint64_t last_click_time;
Scrollbar scrollbar;
DesktopState* desktop;
bool grid_view;
};
static constexpr int FM_TOOLBAR_H = 32;
@@ -33,6 +34,10 @@ static constexpr int FM_PATHBAR_H = 24;
static constexpr int FM_HEADER_H = 20;
static constexpr int FM_ITEM_H = 24;
static constexpr int FM_SCROLLBAR_W = 12;
static constexpr int FM_GRID_CELL_W = 80;
static constexpr int FM_GRID_CELL_H = 80;
static constexpr int FM_GRID_ICON = 48;
static constexpr int FM_GRID_PAD = 4;
// ============================================================================
// File type detection
@@ -287,6 +292,32 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
}
}
// Toggle view button (5th toolbar button)
{
int bx = 120, by = 4;
uint32_t btn_px = Color::from_rgb(0xE8, 0xE8, 0xE8).to_pixel();
for (int dy = 0; dy < 24 && by + dy < ch; dy++)
for (int dx = 0; dx < 24 && bx + dx < cw; dx++)
pixels[(by + dy) * cw + (bx + dx)] = btn_px;
uint32_t glyph_px = colors::TEXT_COLOR.to_pixel();
if (fm->grid_view) {
// Draw 4 small squares to indicate grid mode
for (int r = 0; r < 2; r++)
for (int c = 0; c < 2; c++)
for (int dy = 0; dy < 6; dy++)
for (int dx = 0; dx < 6; dx++)
if (by + 5 + r * 8 + dy < ch && bx + 5 + c * 8 + dx < cw)
pixels[(by + 5 + r * 8 + dy) * cw + (bx + 5 + c * 8 + dx)] = glyph_px;
} else {
// Draw 3 horizontal lines to indicate list mode
for (int r = 0; r < 3; r++)
for (int dx = 0; dx < 14; dx++)
for (int dy = 0; dy < 2; dy++)
if (by + 5 + r * 5 + dy < ch && bx + 5 + dx < cw)
pixels[(by + 5 + r * 5 + dy) * cw + (bx + 5 + dx)] = glyph_px;
}
}
// Toolbar separator
int sep_y = FM_TOOLBAR_H - 1;
if (sep_y < ch) {
@@ -310,100 +341,171 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
pixels[sep_y * cw + x] = sep_px;
}
// ---- Column headers ----
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel();
for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = header_px;
if (fm->grid_view) {
// ---- Grid View ----
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
int list_h = ch - list_y;
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int rows = (fm->entry_count + cols - 1) / cols;
int content_h = rows * FM_GRID_CELL_H;
uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel();
int name_col_x = 8;
int size_col_x = cw - FM_SCROLLBAR_W - 120;
int type_col_x = cw - FM_SCROLLBAR_W - 60;
// Update scrollbar
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
fm->scrollbar.content_height = content_h;
fm->scrollbar.view_height = list_h;
draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px);
if (size_col_x > 100)
draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px);
if (type_col_x > 160)
draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px);
for (int i = 0; i < fm->entry_count; i++) {
int col = i % cols;
int row = i / cols;
int cell_x = col * FM_GRID_CELL_W;
int cell_y = list_y + row * FM_GRID_CELL_H - fm->scrollbar.scroll_offset;
// Header separator
sep_y = header_y + FM_HEADER_H - 1;
if (sep_y < ch) {
for (int x = 0; x < cw; x++)
pixels[sep_y * cw + x] = sep_px;
}
// Skip if entirely off-screen
if (cell_y + FM_GRID_CELL_H <= list_y || cell_y >= ch) continue;
// Column separator lines
if (size_col_x > 100) {
for (int y = header_y; y < ch; y++)
if (size_col_x - 4 >= 0 && size_col_x - 4 < cw)
pixels[y * cw + (size_col_x - 4)] = sep_px;
}
// Selection highlight
if (i == fm->selected) {
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
for (int y = gui_max(cell_y, list_y); y < gui_min(cell_y + FM_GRID_CELL_H, ch); y++)
for (int x = cell_x; x < gui_min(cell_x + FM_GRID_CELL_W, cw - FM_SCROLLBAR_W); x++)
pixels[y * cw + x] = sel_px;
}
// ---- File entries ----
int list_y = header_y + FM_HEADER_H;
int list_h = ch - list_y;
int visible_items = list_h / FM_ITEM_H;
int content_h = fm->entry_count * FM_ITEM_H;
// Large icon centered horizontally
int icon_x = cell_x + (FM_GRID_CELL_W - FM_GRID_ICON) / 2;
int icon_y = cell_y + FM_GRID_PAD;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder_lg);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec_lg);
} else if (ds && ds->icon_file_lg.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file_lg);
} else {
uint32_t icon_px = fm->is_dir[i]
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
for (int dy = 0; dy < FM_GRID_ICON && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
for (int dx = 0; dx < FM_GRID_ICON && icon_x + dx < cw; dx++)
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
}
// Update scrollbar
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
fm->scrollbar.content_height = content_h;
fm->scrollbar.view_height = list_h;
// Filename centered below icon, truncated if needed
char label[16];
int nlen = zenith::slen(fm->entry_names[i]);
if (nlen > 9) {
for (int k = 0; k < 9; k++) label[k] = fm->entry_names[i][k];
label[9] = '.';
label[10] = '.';
label[11] = '\0';
} else {
zenith::strncpy(label, fm->entry_names[i], 15);
}
int tw = text_width(label);
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x) tx = cell_x;
int ty = icon_y + FM_GRID_ICON + 2;
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
draw_text_to_pixels(pixels, cw, ch, tx, ty, label, text_px);
}
} else {
// ---- List View ----
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
// ---- Column headers ----
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
uint32_t header_px = Color::from_rgb(0xF8, 0xF8, 0xF8).to_pixel();
for (int y = header_y; y < header_y + FM_HEADER_H && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = header_px;
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
if (iy + FM_ITEM_H <= list_y || iy >= ch) continue;
uint32_t dim_px = Color::from_rgb(0x88, 0x88, 0x88).to_pixel();
int name_col_x = 8;
int size_col_x = cw - FM_SCROLLBAR_W - 120;
int type_col_x = cw - FM_SCROLLBAR_W - 60;
// Highlight selected
if (i == fm->selected) {
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++)
for (int x = 0; x < cw - FM_SCROLLBAR_W; x++)
pixels[y * cw + x] = sel_px;
draw_text_to_pixels(pixels, cw, ch, name_col_x, header_y + 2, "Name", dim_px);
if (size_col_x > 100)
draw_text_to_pixels(pixels, cw, ch, size_col_x, header_y + 2, "Size", dim_px);
if (type_col_x > 160)
draw_text_to_pixels(pixels, cw, ch, type_col_x, header_y + 2, "Type", dim_px);
// Header separator
sep_y = header_y + FM_HEADER_H - 1;
if (sep_y < ch) {
for (int x = 0; x < cw; x++)
pixels[sep_y * cw + x] = sep_px;
}
// Icon
int icon_x = 8;
int icon_y = iy + (FM_ITEM_H - 16) / 2;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec);
} else if (ds && ds->icon_file.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file);
} else {
uint32_t icon_px = fm->is_dir[i]
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
// Column separator lines
if (size_col_x > 100) {
for (int y = header_y; y < ch; y++)
if (size_col_x - 4 >= 0 && size_col_x - 4 < cw)
pixels[y * cw + (size_col_x - 4)] = sep_px;
}
// Name
int tx = 30;
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px);
// ---- File entries ----
int list_y = header_y + FM_HEADER_H;
int list_h = ch - list_y;
int visible_items = list_h / FM_ITEM_H;
int content_h = fm->entry_count * FM_ITEM_H;
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px);
}
// Update scrollbar
fm->scrollbar.bounds = {cw - FM_SCROLLBAR_W, list_y, FM_SCROLLBAR_W, list_h};
fm->scrollbar.content_height = content_h;
fm->scrollbar.view_height = list_h;
// Type
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) {
const char* type_str = "File";
if (fm->entry_types[i] == 1) type_str = "Dir";
else if (fm->entry_types[i] == 2) type_str = "Exec";
draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px);
int scroll_items = fm->scrollbar.scroll_offset / FM_ITEM_H;
for (int i = scroll_items; i < fm->entry_count && (i - scroll_items) < visible_items + 1; i++) {
int iy = list_y + (i - scroll_items) * FM_ITEM_H - (fm->scrollbar.scroll_offset % FM_ITEM_H);
if (iy + FM_ITEM_H <= list_y || iy >= ch) continue;
// Highlight selected
if (i == fm->selected) {
uint32_t sel_px = colors::MENU_HOVER.to_pixel();
for (int y = gui_max(iy, list_y); y < gui_min(iy + FM_ITEM_H, ch); y++)
for (int x = 0; x < cw - FM_SCROLLBAR_W; x++)
pixels[y * cw + x] = sel_px;
}
// Icon
int icon_x = 8;
int icon_y = iy + (FM_ITEM_H - 16) / 2;
if (ds && fm->entry_types[i] == 1 && ds->icon_folder.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_folder);
} else if (ds && fm->entry_types[i] == 2 && ds->icon_exec.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_exec);
} else if (ds && ds->icon_file.pixels) {
blit_icon_to_pixels(pixels, cw, ch, icon_x, icon_y, ds->icon_file);
} else {
uint32_t icon_px = fm->is_dir[i]
? Color::from_rgb(0xFF, 0xBD, 0x2E).to_pixel()
: Color::from_rgb(0x90, 0x90, 0x90).to_pixel();
for (int dy = 0; dy < 16 && icon_y + dy < ch && icon_y + dy >= list_y; dy++)
for (int dx = 0; dx < 16 && icon_x + dx < cw; dx++)
pixels[(icon_y + dy) * cw + (icon_x + dx)] = icon_px;
}
// Name
int tx = 30;
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
if (ty >= list_y && ty + FONT_HEIGHT <= ch)
draw_text_to_pixels(pixels, cw, ch, tx, ty, fm->entry_names[i], text_px);
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= ch) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
draw_text_to_pixels(pixels, cw, ch, size_col_x, ty, size_str, dim_px);
}
// Type
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= ch) {
const char* type_str = "File";
if (fm->entry_types[i] == 1) type_str = "Dir";
else if (fm->entry_types[i] == 2) type_str = "Exec";
draw_text_to_pixels(pixels, cw, ch, type_col_x, ty, type_str, dim_px);
}
}
}
@@ -457,42 +559,86 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
else if (local_x >= 32 && local_x < 56) filemanager_go_forward(fm);
else if (local_x >= 60 && local_x < 84) filemanager_go_up(fm);
else if (local_x >= 88 && local_x < 112) filemanager_go_home(fm);
else if (local_x >= 120 && local_x < 144) {
fm->grid_view = !fm->grid_view;
fm->scrollbar.scroll_offset = 0;
}
return;
}
// File list clicks
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
int clicked_idx = rel_y / FM_ITEM_H;
// File clicks (grid vs list)
if (fm->grid_view) {
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
int col = local_x / FM_GRID_CELL_W;
int row = (local_y - list_y + fm->scrollbar.scroll_offset) / FM_GRID_CELL_H;
int clicked_idx = row * cols + col;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = zenith::get_milliseconds();
if (clicked_idx >= 0 && clicked_idx < fm->entry_count && col < cols) {
uint64_t now = zenith::get_milliseconds();
// Double-click detection
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
char fullpath[512];
zenith::strcpy(fullpath, fm->current_path);
int plen = zenith::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/') {
str_append(fullpath, "/", 512);
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
// Open file in text editor
char fullpath[512];
zenith::strcpy(fullpath, fm->current_path);
int plen = zenith::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/') {
str_append(fullpath, "/", 512);
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
}
}
} else {
// List view clicks
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
int clicked_idx = rel_y / FM_ITEM_H;
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
uint64_t now = zenith::get_milliseconds();
// Double-click detection
if (fm->last_click_item == clicked_idx &&
(now - fm->last_click_time) < 400) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
// Open file in text editor
char fullpath[512];
zenith::strcpy(fullpath, fm->current_path);
int plen = zenith::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/') {
str_append(fullpath, "/", 512);
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
fm->last_click_item = -1;
fm->last_click_time = 0;
} else {
fm->selected = clicked_idx;
fm->last_click_item = clicked_idx;
fm->last_click_time = now;
}
}
}
@@ -500,9 +646,12 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
// Scroll handling
if (ev.scroll != 0) {
int list_y_start = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int list_y_start = fm->grid_view
? FM_TOOLBAR_H + FM_PATHBAR_H
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
int scroll_step = fm->grid_view ? FM_GRID_CELL_H : FM_ITEM_H;
if (local_y >= list_y_start) {
fm->scrollbar.scroll_offset -= ev.scroll * FM_ITEM_H;
fm->scrollbar.scroll_offset -= ev.scroll * scroll_step;
int ms = fm->scrollbar.max_scroll();
if (fm->scrollbar.scroll_offset < 0) fm->scrollbar.scroll_offset = 0;
if (fm->scrollbar.scroll_offset > ms) fm->scrollbar.scroll_offset = ms;
@@ -522,9 +671,29 @@ static void filemanager_on_key(Window* win, const Zenith::KeyEvent& key) {
filemanager_go_up(fm);
} else if (key.scancode == 0x48) {
// Up arrow
if (fm->selected > 0) fm->selected--;
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected >= cols) fm->selected -= cols;
} else {
if (fm->selected > 0) fm->selected--;
}
} else if (key.scancode == 0x50) {
// Down arrow
if (fm->grid_view) {
Rect cr = win->content_rect();
int cols = (cr.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
if (cols < 1) cols = 1;
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
} else {
if (fm->selected < fm->entry_count - 1) fm->selected++;
}
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
// Left arrow (grid view only)
if (fm->selected > 0) fm->selected--;
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
// Right arrow (grid view only)
if (fm->selected < fm->entry_count - 1) fm->selected++;
} else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
@@ -565,6 +734,7 @@ void open_filemanager(DesktopState* ds) {
fm->history_pos = -1;
fm->history_count = 0;
fm->desktop = ds;
fm->grid_view = true;
fm->scrollbar.init(0, 0, FM_SCROLLBAR_W, 100);
+625
View File
@@ -0,0 +1,625 @@
/*
* app_wiki.cpp
* ZenithOS Desktop - Wikipedia client application
* Spawns wiki.elf -d as a child process for non-blocking network I/O
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Constants
// ============================================================================
static constexpr int WIKI_RESP_MAX = 131072; // 128 KB
static constexpr int WIKI_MAX_LINES = 4096;
static constexpr int WIKI_TOOLBAR_H = 36;
static constexpr int WIKI_SCROLLBAR_W = 12;
// ============================================================================
// Wiki state
// ============================================================================
struct WikiDisplayLine {
char text[256];
uint32_t color;
};
struct WikiState {
DesktopState* ds;
enum Mode { IDLE, FETCHING, DONE, WIKI_ERROR };
Mode mode;
char searchQuery[256];
WikiDisplayLine* lines;
int lineCount;
int lineCap;
int scrollY; // scroll offset in lines
// Child process for non-blocking fetch
int child_pid;
char* respBuf; // accumulated child output
int respPos; // current position in respBuf
char statusMsg[128];
};
// ============================================================================
// JSON string extraction (for parsing child output)
// ============================================================================
static int slen(const char* s) { int n = 0; while (s[n]) n++; return n; }
static bool memeq(const char* a, const char* b, int n) {
for (int i = 0; i < n; i++) if (a[i] != b[i]) return false;
return true;
}
static int wiki_extract_json_string(const char* buf, int len, const char* key,
char* out, int maxOut) {
int klen = slen(key);
for (int i = 0; i < len - klen - 3; i++) {
if (buf[i] != '"') continue;
if (!memeq(buf + i + 1, key, klen)) continue;
if (buf[i + 1 + klen] != '"') continue;
if (buf[i + 2 + klen] != ':') continue;
int p = i + 3 + klen;
while (p < len && (buf[p] == ' ' || buf[p] == '\t')) p++;
if (p >= len || buf[p] != '"') continue;
p++;
int j = 0;
while (p < len && j < maxOut - 4) {
if (buf[p] == '"') break;
if (buf[p] == '\\' && p + 1 < len) {
p++;
switch (buf[p]) {
case '"': out[j++] = '"'; break;
case '\\': out[j++] = '\\'; break;
case 'n': out[j++] = '\n'; break;
case 'r': break;
case 't': out[j++] = '\t'; break;
case '/': out[j++] = '/'; break;
case 'u': {
if (p + 4 < len) {
unsigned val = 0;
for (int k = 1; k <= 4; k++) {
char h = buf[p + k];
val <<= 4;
if (h >= '0' && h <= '9') val |= h - '0';
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
}
p += 4;
if (val < 128) out[j++] = (char)val;
else if (val == 0x2013 || val == 0x2014) out[j++] = '-';
else if (val == 0x2018 || val == 0x2019) out[j++] = '\'';
else if (val == 0x201C || val == 0x201D) out[j++] = '"';
else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; }
else out[j++] = '?';
}
break;
}
default: out[j++] = buf[p]; break;
}
} else {
out[j++] = buf[p];
}
p++;
}
out[j] = '\0';
return j;
}
out[0] = '\0';
return 0;
}
// ============================================================================
// Display line building (word-wrap adapted for pixel widths)
// ============================================================================
static void wiki_add_line(WikiState* ws, const char* text, int len, uint32_t color) {
if (ws->lineCount >= ws->lineCap) return;
WikiDisplayLine* dl = &ws->lines[ws->lineCount];
int copyLen = len;
if (copyLen > 255) copyLen = 255;
for (int i = 0; i < copyLen; i++) dl->text[i] = text[i];
dl->text[copyLen] = '\0';
dl->color = color;
ws->lineCount++;
}
static void wiki_wrap_text(WikiState* ws, const char* text, int textLen,
int maxChars, uint32_t color) {
if (textLen <= 0 || maxChars <= 0) return;
const char* p = text;
const char* end = text + textLen;
while (p < end && ws->lineCount < ws->lineCap) {
while (p < end && *p == ' ') p++;
if (p >= end) break;
const char* lineStart = p;
const char* lastSpace = nullptr;
int col = 0;
while (p < end && col < maxChars) {
if (*p == ' ') lastSpace = p;
p++;
col++;
}
if (p >= end) {
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
} else if (lastSpace && lastSpace > lineStart) {
wiki_add_line(ws, lineStart, (int)(lastSpace - lineStart), color);
p = lastSpace + 1;
} else {
wiki_add_line(ws, lineStart, (int)(p - lineStart), color);
}
}
}
static void wiki_build_display(WikiState* ws, const char* title,
const char* extract, int extractLen, int contentW) {
ws->lineCount = 0;
ws->scrollY = 0;
int maxChars = (contentW - 24 - WIKI_SCROLLBAR_W) / FONT_WIDTH;
if (maxChars < 20) maxChars = 20;
uint32_t accent_px = colors::ACCENT.to_pixel();
uint32_t green_px = Color::from_rgb(0x2E, 0x7D, 0x32).to_pixel();
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
// Title
if (title && title[0]) {
wiki_wrap_text(ws, title, slen(title), maxChars, accent_px);
}
// Blank separator
if (ws->lineCount > 0)
wiki_add_line(ws, "", 0, text_px);
// Process extract line by line
const char* p = extract;
const char* end = extract + extractLen;
while (p < end && ws->lineCount < ws->lineCap) {
const char* lineStart = p;
while (p < end && *p != '\n') p++;
int lineLen = (int)(p - lineStart);
if (p < end) p++;
if (lineLen == 0) {
wiki_add_line(ws, "", 0, text_px);
continue;
}
// Section header detection (== Title ==)
if (lineLen >= 4 && lineStart[0] == '=' && lineStart[1] == '=') {
int si = 0;
while (si < lineLen && lineStart[si] == '=') si++;
while (si < lineLen && lineStart[si] == ' ') si++;
int ei = lineLen;
while (ei > si && lineStart[ei - 1] == '=') ei--;
while (ei > si && lineStart[ei - 1] == ' ') ei--;
wiki_add_line(ws, "", 0, text_px);
if (ei > si) {
char secBuf[256];
int secLen = ei - si;
if (secLen > 255) secLen = 255;
for (int i = 0; i < secLen; i++) secBuf[i] = lineStart[si + i];
secBuf[secLen] = '\0';
wiki_add_line(ws, secBuf, secLen, green_px);
}
continue;
}
// Regular text
wiki_wrap_text(ws, lineStart, lineLen, maxChars, text_px);
}
}
// ============================================================================
// Process completed response from child
// ============================================================================
static void wiki_process_response(WikiState* ws) {
const char* body = ws->respBuf;
int bodyLen = ws->respPos;
if (bodyLen <= 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: no response from Wikipedia");
ws->mode = WikiState::WIKI_ERROR;
return;
}
// Check for error sentinel from child
if (bodyLen >= 1 && body[0] == '\x01') {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Article not found: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
static char title[512], extractBuf[131072];
wiki_extract_json_string(body, bodyLen, "title", title, sizeof(title));
int extractLen = wiki_extract_json_string(body, bodyLen, "extract",
extractBuf, sizeof(extractBuf) - 1);
if (extractLen == 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "No content found for: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
// Find content width from the window
int contentW = 580;
for (int i = 0; i < ws->ds->window_count; i++) {
Window* w = &ws->ds->windows[i];
if (w->app_data == ws) {
Rect cr = w->content_rect();
contentW = cr.w;
break;
}
}
wiki_build_display(ws, title, extractBuf, extractLen, contentW);
ws->mode = WikiState::DONE;
}
// ============================================================================
// Callbacks
// ============================================================================
static void wiki_on_draw(Window* win, Framebuffer& fb) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
Rect cr = win->content_rect();
int cw = cr.w;
int ch = cr.h;
uint32_t* pixels = win->content;
// Fill background
uint32_t bg_px = colors::WINDOW_BG.to_pixel();
for (int i = 0; i < cw * ch; i++) pixels[i] = bg_px;
// ---- Toolbar background ----
uint32_t toolbar_bg = Color::from_rgb(0xF5, 0xF5, 0xF5).to_pixel();
for (int y = 0; y < WIKI_TOOLBAR_H && y < ch; y++)
for (int x = 0; x < cw; x++)
pixels[y * cw + x] = toolbar_bg;
// Toolbar separator line
uint32_t sep_px = colors::BORDER.to_pixel();
if (WIKI_TOOLBAR_H < ch)
for (int x = 0; x < cw; x++)
pixels[WIKI_TOOLBAR_H * cw + x] = sep_px;
// ---- Draw search box outline ----
int tb_x = 8;
int tb_y = 6;
int tb_w = cw - 90;
int tb_h = 24;
if (tb_w < 100) tb_w = 100;
// TextBox background
uint32_t white_px = colors::WHITE.to_pixel();
uint32_t border_px = colors::BORDER.to_pixel();
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++)
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++)
pixels[y * cw + x] = white_px;
// Border
for (int x = tb_x; x < tb_x + tb_w && x < cw; x++) {
if (tb_y < ch) pixels[tb_y * cw + x] = border_px;
if (tb_y + tb_h - 1 < ch) pixels[(tb_y + tb_h - 1) * cw + x] = border_px;
}
for (int y = tb_y; y < tb_y + tb_h && y < ch; y++) {
if (tb_x < cw) pixels[y * cw + tb_x] = border_px;
if (tb_x + tb_w - 1 < cw) pixels[y * cw + (tb_x + tb_w - 1)] = border_px;
}
// Search text
uint32_t text_px = colors::TEXT_COLOR.to_pixel();
draw_text_to_pixels(pixels, cw, ch, tb_x + 4, tb_y + (tb_h - FONT_HEIGHT) / 2,
ws->searchQuery, text_px);
// ---- Search button ----
int btn_x = tb_x + tb_w + 6;
int btn_y = tb_y;
int btn_w = 66;
int btn_h = tb_h;
uint32_t accent_px = colors::ACCENT.to_pixel();
for (int y = btn_y; y < btn_y + btn_h && y < ch; y++)
for (int x = btn_x; x < btn_x + btn_w && x < cw; x++)
pixels[y * cw + x] = accent_px;
// Button text
uint32_t white_text = colors::WHITE.to_pixel();
const char* btnLabel = "Search";
int labelW = zenith::slen(btnLabel) * FONT_WIDTH;
draw_text_to_pixels(pixels, cw, ch,
btn_x + (btn_w - labelW) / 2,
btn_y + (btn_h - FONT_HEIGHT) / 2,
btnLabel, white_text);
// ---- Content area ----
int content_y = WIKI_TOOLBAR_H + 1;
int content_h = ch - content_y;
int visibleLines = content_h / (FONT_HEIGHT + 4);
if (visibleLines < 1) visibleLines = 1;
if (ws->mode == WikiState::FETCHING) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
"Loading...", Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
} else if (ws->mode == WikiState::WIKI_ERROR) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
ws->statusMsg, colors::CLOSE_BTN.to_pixel());
} else if (ws->mode == WikiState::IDLE) {
draw_text_to_pixels(pixels, cw, ch, 16, content_y + 16,
"Type a topic and press Enter or click Search.",
Color::from_rgb(0x88, 0x88, 0x88).to_pixel());
} else if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
int y = content_y + 8;
int lineH = FONT_HEIGHT + 4;
for (int i = ws->scrollY; i < ws->lineCount && y + FONT_HEIGHT < ch; i++) {
WikiDisplayLine* dl = &ws->lines[i];
if (dl->text[0] != '\0') {
draw_text_to_pixels(pixels, cw, ch, 12, y, dl->text, dl->color);
}
y += lineH;
}
// ---- Scrollbar ----
if (ws->lineCount > visibleLines) {
int sb_x = cw - WIKI_SCROLLBAR_W;
int sb_y = content_y;
int sb_h = content_h;
// Scrollbar track
uint32_t sb_bg = colors::SCROLLBAR_BG.to_pixel();
for (int y2 = sb_y; y2 < sb_y + sb_h && y2 < ch; y2++)
for (int x = sb_x; x < cw; x++)
pixels[y2 * cw + x] = sb_bg;
// Thumb
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 1) maxScroll = 1;
int thumbH = (visibleLines * sb_h) / ws->lineCount;
if (thumbH < 20) thumbH = 20;
int thumbY = sb_y + (ws->scrollY * (sb_h - thumbH)) / maxScroll;
uint32_t sb_fg = colors::SCROLLBAR_FG.to_pixel();
for (int y2 = thumbY; y2 < thumbY + thumbH && y2 < ch; y2++)
for (int x = sb_x + 2; x < cw - 2; x++)
pixels[y2 * cw + x] = sb_fg;
}
}
}
static void wiki_trigger_search(WikiState* ws) {
if (ws->searchQuery[0] == '\0') return;
if (ws->mode == WikiState::FETCHING) return; // already fetching
ws->lineCount = 0;
ws->scrollY = 0;
ws->respPos = 0;
// Build args string: "-d <query>"
static char args[512];
args[0] = '-'; args[1] = 'd'; args[2] = ' ';
int qi = 0;
while (ws->searchQuery[qi] && qi < 500) {
args[3 + qi] = ws->searchQuery[qi];
qi++;
}
args[3 + qi] = '\0';
ws->child_pid = zenith::spawn_redir("0:/os/wiki.elf", args);
if (ws->child_pid <= 0) {
snprintf(ws->statusMsg, sizeof(ws->statusMsg), "Error: could not start wiki process");
ws->mode = WikiState::WIKI_ERROR;
return;
}
ws->mode = WikiState::FETCHING;
}
static void wiki_on_mouse(Window* win, MouseEvent& ev) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
Rect cr = win->content_rect();
int cw = cr.w;
int local_x = ev.x - cr.x;
int local_y = ev.y - cr.y;
// Check search button click
int tb_w = cw - 90;
if (tb_w < 100) tb_w = 100;
int btn_x = 8 + tb_w + 6;
int btn_y = 6;
int btn_w = 66;
int btn_h = 24;
if (ev.left_pressed()) {
if (local_x >= btn_x && local_x < btn_x + btn_w &&
local_y >= btn_y && local_y < btn_y + btn_h) {
wiki_trigger_search(ws);
return;
}
}
// Scroll wheel
if (ev.scroll != 0 && ws->mode == WikiState::DONE && ws->lineCount > 0) {
int ch = cr.h;
int content_h = ch - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
ws->scrollY += ev.scroll * 3;
if (ws->scrollY < 0) ws->scrollY = 0;
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
}
}
static void wiki_on_key(Window* win, const Zenith::KeyEvent& key) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws || !key.pressed) return;
// Enter key triggers search
if (key.ascii == '\n' || key.ascii == '\r') {
wiki_trigger_search(ws);
return;
}
// Page Up / Page Down / arrows
if (ws->mode == WikiState::DONE && ws->lineCount > 0) {
Rect cr = {0, 0, 0, 0};
for (int i = 0; i < ws->ds->window_count; i++) {
Window* w = &ws->ds->windows[i];
if (w->app_data == ws) {
cr = w->content_rect();
break;
}
}
int content_h = cr.h - WIKI_TOOLBAR_H - 1;
int visibleLines = content_h / (FONT_HEIGHT + 4);
if (visibleLines < 1) visibleLines = 1;
int maxScroll = ws->lineCount - visibleLines;
if (maxScroll < 0) maxScroll = 0;
if (key.scancode == 0x49) { // Page Up
ws->scrollY -= visibleLines;
if (ws->scrollY < 0) ws->scrollY = 0;
return;
}
if (key.scancode == 0x51) { // Page Down
ws->scrollY += visibleLines;
if (ws->scrollY > maxScroll) ws->scrollY = maxScroll;
return;
}
if (key.scancode == 0x48) { // Up arrow
if (ws->scrollY > 0) ws->scrollY--;
return;
}
if (key.scancode == 0x50) { // Down arrow
if (ws->scrollY < maxScroll) ws->scrollY++;
return;
}
if (key.scancode == 0x47) { // Home
ws->scrollY = 0;
return;
}
if (key.scancode == 0x4F) { // End
ws->scrollY = maxScroll;
return;
}
}
// Text input for search box
if (key.ascii == '\b' || key.scancode == 0x0E) {
int len = zenith::slen(ws->searchQuery);
if (len > 0) ws->searchQuery[len - 1] = '\0';
} else if (key.ascii >= 32 && key.ascii < 127) {
int len = zenith::slen(ws->searchQuery);
if (len < 254) {
ws->searchQuery[len] = key.ascii;
ws->searchQuery[len + 1] = '\0';
}
}
}
static void wiki_on_poll(Window* win) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
if (ws->mode != WikiState::FETCHING || ws->child_pid <= 0) return;
// Non-blocking read from child process
char buf[4096];
int n = zenith::childio_read(ws->child_pid, buf, sizeof(buf));
if (n > 0) {
// Accumulate data, checking for EOT sentinel
for (int i = 0; i < n && ws->respPos < WIKI_RESP_MAX - 1; i++) {
if (buf[i] == '\x04') {
// EOT: child is done, process the response
ws->respBuf[ws->respPos] = '\0';
ws->child_pid = -1;
wiki_process_response(ws);
return;
}
if (buf[i] == '\x01') {
// Error sentinel from child
ws->child_pid = -1;
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
"Article not found: %s", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
return;
}
ws->respBuf[ws->respPos++] = buf[i];
}
} else if (n < 0) {
// Child process exited — process whatever we accumulated
ws->child_pid = -1;
if (ws->respPos > 0) {
ws->respBuf[ws->respPos] = '\0';
wiki_process_response(ws);
} else {
snprintf(ws->statusMsg, sizeof(ws->statusMsg),
"Error: fetch failed for \"%s\"", ws->searchQuery);
ws->mode = WikiState::WIKI_ERROR;
}
}
}
static void wiki_on_close(Window* win) {
WikiState* ws = (WikiState*)win->app_data;
if (!ws) return;
if (ws->lines) zenith::mfree(ws->lines);
if (ws->respBuf) zenith::mfree(ws->respBuf);
zenith::mfree(ws);
win->app_data = nullptr;
}
// ============================================================================
// Wikipedia launcher
// ============================================================================
void open_wiki(DesktopState* ds) {
int idx = desktop_create_window(ds, "Wikipedia", 100, 80, 600, 480);
if (idx < 0) return;
Window* win = &ds->windows[idx];
WikiState* ws = (WikiState*)zenith::malloc(sizeof(WikiState));
zenith::memset(ws, 0, sizeof(WikiState));
ws->ds = ds;
ws->mode = WikiState::IDLE;
ws->searchQuery[0] = '\0';
ws->scrollY = 0;
ws->child_pid = -1;
// Allocate display lines
ws->lineCap = WIKI_MAX_LINES;
ws->lines = (WikiDisplayLine*)zenith::malloc(ws->lineCap * sizeof(WikiDisplayLine));
ws->lineCount = 0;
// Allocate response buffer
ws->respBuf = (char*)zenith::malloc(WIKI_RESP_MAX);
ws->respPos = 0;
ws->statusMsg[0] = '\0';
win->app_data = ws;
win->on_draw = wiki_on_draw;
win->on_mouse = wiki_on_mouse;
win->on_key = wiki_on_key;
win->on_poll = wiki_on_poll;
win->on_close = wiki_on_close;
}
+1
View File
@@ -248,3 +248,4 @@ void open_calculator(DesktopState* ds);
void open_texteditor(DesktopState* ds);
void open_texteditor_with_file(DesktopState* ds, const char* path);
void open_klog(DesktopState* ds);
void open_wiki(DesktopState* ds);
+26 -19
View File
@@ -31,23 +31,29 @@ void gui::desktop_init(DesktopState* ds) {
zenith::memset(&ds->mouse, 0, sizeof(Zenith::MouseState));
zenith::set_mouse_bounds(ds->screen_w - 1, ds->screen_h - 1);
// Load SVG icons
ds->icon_terminal = svg_load("0:/icons/utilities-terminal-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_filemanager = svg_load("0:/icons/system-file-manager-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
ds->icon_folder = svg_load("0:/icons/folder-symbolic.svg", 16, 16, Color::from_rgb(0xFF, 0xBD, 0x2E));
ds->icon_file = svg_load("0:/icons/text-x-generic-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_computer = svg_load("0:/icons/computer-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_calculator = svg_load("0:/icons/accessories-calculator-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor-symbolic.svg", 20, 20, colors::ICON_COLOR);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_home = svg_load("0:/icons/user-home-symbolic.svg", 16, 16, colors::ICON_COLOR);
ds->icon_exec = svg_load("0:/icons/application-x-executable-symbolic.svg", 16, 16, Color::from_rgb(0x4E, 0x9A, 0x06));
// Load SVG icons — scalable (colorful) for app menu, symbolic for toolbar/panel
Color defColor = colors::ICON_COLOR;
ds->icon_terminal = svg_load("0:/icons/utilities-terminal.svg", 20, 20, defColor);
ds->icon_filemanager = svg_load("0:/icons/system-file-manager.svg", 20, 20, defColor);
ds->icon_sysinfo = svg_load("0:/icons/preferences-desktop-apps.svg", 20, 20, defColor);
ds->icon_appmenu = svg_load("0:/icons/view-app-grid-symbolic.svg", 20, 20, colors::PANEL_TEXT);
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
ds->icon_computer = svg_load("0:/icons/computer.svg", 20, 20, defColor);
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_calculator = svg_load("0:/icons/accessories-calculator.svg", 20, 20, defColor);
ds->icon_texteditor = svg_load("0:/icons/accessories-text-editor.svg", 20, 20, defColor);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
ds->icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, defColor);
ds->icon_home = svg_load("0:/icons/user-home.svg", 16, 16, defColor);
ds->icon_exec = svg_load("0:/icons/utilities-terminal.svg", 16, 16, defColor);
ds->icon_wikipedia = svg_load("0:/icons/web-browser.svg", 20, 20, defColor);
ds->icon_folder_lg = svg_load("0:/icons/folder.svg", 48, 48, defColor);
ds->icon_file_lg = svg_load("0:/icons/text-x-generic.svg", 48, 48, defColor);
ds->icon_exec_lg = svg_load("0:/icons/utilities-terminal.svg", 48, 48, defColor);
ds->net_popup_open = false;
zenith::get_netcfg(&ds->cached_net_cfg);
@@ -334,7 +340,7 @@ void gui::desktop_draw_panel(DesktopState* ds) {
// App Menu (5 items with separator and rounded corners)
// ============================================================================
static constexpr int MENU_ITEM_COUNT = 6;
static constexpr int MENU_ITEM_COUNT = 7;
static constexpr int MENU_W = 220;
static constexpr int MENU_ITEM_H = 40;
@@ -364,6 +370,7 @@ static void desktop_draw_app_menu(DesktopState* ds) {
{ "Calculator", &ds->icon_calculator },
{ "Text Editor", &ds->icon_texteditor },
{ "Kernel Log", &ds->icon_terminal },
{ "Wikipedia", &ds->icon_wikipedia },
};
int mx = ds->mouse.x;
@@ -558,6 +565,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
case 3: open_calculator(ds); break;
case 4: open_texteditor(ds); break;
case 5: open_klog(ds); break;
case 6: open_wiki(ds); break;
}
ds->app_menu_open = false;
}
@@ -598,7 +606,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->app_menu_open = false;
return;
}
// Window indicator buttons
int indicator_x = 40;
for (int i = 0; i < ds->window_count; i++) {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+58 -3
View File
@@ -25,7 +25,7 @@ static const char WIKI_HOST[] = "en.wikipedia.org";
static constexpr int MAX_LINES = 4096;
static constexpr int MAX_SEARCH_RESULTS = 10;
enum Mode { MODE_SUMMARY, MODE_FULL, MODE_SEARCH };
enum Mode { MODE_SUMMARY, MODE_FULL, MODE_SEARCH, MODE_DUMP };
enum LineType { LINE_BLANK, LINE_TITLE, LINE_DESC, LINE_SECTION, LINE_BODY };
struct WikiLine {
@@ -1002,6 +1002,9 @@ extern "C" void _start() {
} else if (arg[0] == '-' && arg[1] == 's' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_SEARCH;
arg = skip_spaces(arg + 2);
} else if (arg[0] == '-' && arg[1] == 'd' && (arg[2] == ' ' || arg[2] == '\0')) {
mode = MODE_DUMP;
arg = skip_spaces(arg + 2);
}
if (*arg == '\0') {
@@ -1017,16 +1020,19 @@ extern "C" void _start() {
query[qlen] = '\0';
// Initialize: resolve DNS and load certs
zenith::print("\033[1;33mConnecting to Wikipedia...\033[0m\n");
if (mode != MODE_DUMP)
zenith::print("\033[1;33mConnecting to Wikipedia...\033[0m\n");
g_serverIp = zenith::resolve(WIKI_HOST);
if (g_serverIp == 0) {
if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); }
zenith::print("\033[1;31mError:\033[0m could not resolve en.wikipedia.org\n");
zenith::exit(1);
}
g_tas = load_trust_anchors();
if (g_tas.count == 0) {
if (mode == MODE_DUMP) { zenith::print("\x01"); zenith::sleep_ms(100); zenith::exit(1); }
zenith::print("\033[1;31mError:\033[0m no CA certificates loaded\n");
zenith::exit(1);
}
@@ -1040,7 +1046,56 @@ extern "C" void _start() {
zenith::exit(1);
}
if (mode == MODE_SEARCH) {
if (mode == MODE_DUMP) {
// ---- Dump mode: output raw JSON body for desktop GUI ----
static char path[2048], encoded[1024];
url_encode_title(query, encoded, sizeof(encoded));
snprintf(path, sizeof(path),
"/w/api.php?action=query&format=json&formatversion=2"
"&prop=extracts&explaintext=1&titles=%s", encoded);
int respLen = wiki_fetch(path, respBuf, RESP_MAX);
if (respLen <= 0) {
zenith::print("\x01"); // error sentinel
zenith::sleep_ms(100);
zenith::exit(1);
}
respBuf[respLen] = '\0';
int headerEnd = find_header_end(respBuf, respLen);
if (headerEnd < 0) {
zenith::print("\x01");
zenith::sleep_ms(100);
zenith::exit(1);
}
int statusCode = parse_status_code(respBuf, headerEnd);
if (statusCode == 404) {
zenith::print("\x01");
zenith::sleep_ms(100);
zenith::exit(1);
}
// Output raw JSON body in chunks to avoid overflowing
// the 4KB kernel ring buffer (parent polls at ~60fps)
const char* body = respBuf + headerEnd;
int bodyLen = respLen - headerEnd;
static char chunk[2049];
int sent = 0;
while (sent < bodyLen) {
int n = bodyLen - sent;
if (n > 2048) n = 2048;
for (int ci = 0; ci < n; ci++) chunk[ci] = body[sent + ci];
chunk[n] = '\0';
zenith::print(chunk);
sent += n;
if (sent < bodyLen) zenith::sleep_ms(20);
}
zenith::putchar('\x04'); // EOT sentinel
// Brief delay so parent can drain the ring buffer before we exit
zenith::sleep_ms(100);
zenith::exit(0);
} else if (mode == MODE_SEARCH) {
// ---- Search mode ----
static char path[2048], encoded[1024];
url_encode_query(query, encoded, sizeof(encoded));
Binary file not shown.
BIN
View File
Binary file not shown.
+14
View File
@@ -36,6 +36,20 @@ ICONS=(
"mimetypes/symbolic/application-x-executable-symbolic.svg"
"devices/symbolic/computer-symbolic.svg"
"devices/symbolic/network-wired-symbolic.svg"
"apps/symbolic/web-browser-symbolic.svg"
# Scalable (colorful) icons for app menu
"apps/scalable/utilities-terminal.svg"
"apps/scalable/system-file-manager.svg"
"apps/scalable/preferences-desktop-apps.svg"
"apps/scalable/accessories-calculator.svg"
"apps/scalable/accessories-text-editor.svg"
"apps/scalable/web-browser.svg"
"places/scalable/folder.svg"
"places/scalable/user-home.svg"
"devices/scalable/computer.svg"
"devices/scalable/network-wired.svg"
"mimetypes/scalable/text-x-generic.svg"
"mimetypes/scalable/application-x-executable.svg"
)
copied=0