diff --git a/kernel/src/Drivers/USB/UsbDevice.cpp b/kernel/src/Drivers/USB/UsbDevice.cpp index b05e9a6..73f8373 100644 --- a/kernel/src/Drivers/USB/UsbDevice.cpp +++ b/kernel/src/Drivers/USB/UsbDevice.cpp @@ -13,9 +13,28 @@ #include #include #include +#include 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, diff --git a/kernel/src/Drivers/USB/Xhci.cpp b/kernel/src/Drivers/USB/Xhci.cpp index 03bcb7e..c87891f 100644 --- a/kernel/src/Drivers/USB/Xhci.cpp +++ b/kernel/src/Drivers/USB/Xhci.cpp @@ -16,9 +16,28 @@ #include #include #include +#include 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); } diff --git a/programs/include/gui/desktop.hpp b/programs/include/gui/desktop.hpp index 1f28d6e..d46d44b 100644 --- a/programs/include/gui/desktop.hpp +++ b/programs/include/gui/desktop.hpp @@ -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; diff --git a/programs/include/gui/svg.hpp b/programs/include/gui/svg.hpp index 0c10b0b..522a8be 100644 --- a/programs/include/gui/svg.hpp +++ b/programs/include/gui/svg.hpp @@ -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 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), " or ) + const char* gend = svg_strstr(gp, (int)(dend - gp), ""); + if (!gend) gend = svg_strstr(gp, (int)(dend - gp), ""); + 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 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 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 ... blocks entirely + if (remaining > 5 && svg_strncmp(p, "')) { + const char* defs_end = svg_strstr(p, remaining, ""); + if (defs_end) { + p = defs_end + 7; // skip past + } else { + p += 5; + } + continue; + } + // Check for 5 && svg_strncmp(p, "' + 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}; } // --------------------------------------------------------------------------- diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index 7cb08df..855acf5 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -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 ---- diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/app_filemanager.cpp index e5559b7..28512a8 100644 --- a/programs/src/desktop/app_filemanager.cpp +++ b/programs/src/desktop/app_filemanager.cpp @@ -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); diff --git a/programs/src/desktop/app_wiki.cpp b/programs/src/desktop/app_wiki.cpp new file mode 100644 index 0000000..35f968a --- /dev/null +++ b/programs/src/desktop/app_wiki.cpp @@ -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 " + 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; +} diff --git a/programs/src/desktop/apps_common.hpp b/programs/src/desktop/apps_common.hpp index 04ce1d9..8f9e940 100644 --- a/programs/src/desktop/apps_common.hpp +++ b/programs/src/desktop/apps_common.hpp @@ -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); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 43c74c0..026fa32 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -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++) { diff --git a/programs/src/desktop/obj/app_calculator.o b/programs/src/desktop/obj/app_calculator.o index f9c3a6d..4ba5de3 100644 Binary files a/programs/src/desktop/obj/app_calculator.o and b/programs/src/desktop/obj/app_calculator.o differ diff --git a/programs/src/desktop/obj/app_filemanager.o b/programs/src/desktop/obj/app_filemanager.o index c527da9..4956af8 100644 Binary files a/programs/src/desktop/obj/app_filemanager.o and b/programs/src/desktop/obj/app_filemanager.o differ diff --git a/programs/src/desktop/obj/app_klog.o b/programs/src/desktop/obj/app_klog.o index a2bdd2a..35f9677 100644 Binary files a/programs/src/desktop/obj/app_klog.o and b/programs/src/desktop/obj/app_klog.o differ diff --git a/programs/src/desktop/obj/app_sysinfo.o b/programs/src/desktop/obj/app_sysinfo.o index ecb9f13..bd34efb 100644 Binary files a/programs/src/desktop/obj/app_sysinfo.o and b/programs/src/desktop/obj/app_sysinfo.o differ diff --git a/programs/src/desktop/obj/app_terminal.o b/programs/src/desktop/obj/app_terminal.o index 35b82af..022381c 100644 Binary files a/programs/src/desktop/obj/app_terminal.o and b/programs/src/desktop/obj/app_terminal.o differ diff --git a/programs/src/desktop/obj/app_texteditor.o b/programs/src/desktop/obj/app_texteditor.o index 705108a..04815e8 100644 Binary files a/programs/src/desktop/obj/app_texteditor.o and b/programs/src/desktop/obj/app_texteditor.o differ diff --git a/programs/src/desktop/obj/app_wiki.o b/programs/src/desktop/obj/app_wiki.o new file mode 100644 index 0000000..09a7de6 Binary files /dev/null and b/programs/src/desktop/obj/app_wiki.o differ diff --git a/programs/src/desktop/obj/main.o b/programs/src/desktop/obj/main.o index bb23631..035784d 100644 Binary files a/programs/src/desktop/obj/main.o and b/programs/src/desktop/obj/main.o differ diff --git a/programs/src/wiki/main.cpp b/programs/src/wiki/main.cpp index 34738dd..3698f0c 100644 --- a/programs/src/wiki/main.cpp +++ b/programs/src/wiki/main.cpp @@ -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)); diff --git a/programs/src/wiki/obj/main.o b/programs/src/wiki/obj/main.o index 42a3903..9b43383 100644 Binary files a/programs/src/wiki/obj/main.o and b/programs/src/wiki/obj/main.o differ diff --git a/ramdisk.tar b/ramdisk.tar index 32e4751..f41b4a3 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 887ae67..06f286f 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -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