feat: jpeg image rendering, desktop backgrounds, fix memory regression

This commit is contained in:
2026-02-27 19:01:07 +01:00
parent f28b850db2
commit e95089192f
19 changed files with 9019 additions and 29 deletions
+1
View File
@@ -33,6 +33,7 @@ run-hdd: run-hdd-$(ARCH)
run-x86_64: $(IMAGE_NAME).iso
sudo ./scripts/net-setup.sh
qemu-system-$(ARCH) \
-enable-kvm \
-M q35 \
-bios /usr/share/ovmf/OVMF.fd \
-cdrom $(IMAGE_NAME).iso \
+1 -1
View File
@@ -10,7 +10,7 @@
namespace Fs::Ramdisk {
static constexpr int MaxFiles = 128;
static constexpr int MaxFiles = 256;
static constexpr int MaxNameLen = 100;
struct FileEntry {
+15 -5
View File
@@ -57,7 +57,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki wikipedia weather desktop
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer desktop
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, games are handled separately.
@@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki wikipedia weather desktop icons fonts bearssl libc tls
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer desktop icons fonts bearssl libc tls libjpeg
all: bearssl libc tls $(TARGETS) fetch wiki wikipedia weather doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library.
bearssl:
@@ -114,8 +114,16 @@ wikipedia: bearssl libc tls
weather: bearssl libc tls
$(MAKE) -C src/weather
# Build desktop via its own Makefile.
desktop:
# Build libjpeg static library (JPEG decoding via stb_image).
libjpeg: libc
$(MAKE) -C lib/libjpeg
# Build imageviewer standalone GUI client (depends on libc and libjpeg).
imageviewer: libc libjpeg
$(MAKE) -C src/imageviewer
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
desktop: libc libjpeg
$(MAKE) -C src/desktop
# Copy SVG icons for the desktop into bin/icons/.
@@ -165,9 +173,11 @@ clean:
rm -rf $(BINDIR) obj
$(MAKE) -C lib/bearssl clean
$(MAKE) -C lib/libc clean
$(MAKE) -C lib/libjpeg clean
$(MAKE) -C lib/tls clean
$(MAKE) -C src/fetch clean
$(MAKE) -C src/doom clean
$(MAKE) -C src/desktop clean
$(MAKE) -C src/wikipedia clean
$(MAKE) -C src/weather clean
$(MAKE) -C src/imageviewer clean
+7
View File
@@ -21,10 +21,17 @@ static constexpr int PANEL_HEIGHT = 32;
struct DesktopSettings {
// Background
bool bg_gradient; // true = gradient, false = solid
bool bg_image; // true = JPEG wallpaper
Color bg_solid; // solid background color
Color bg_grad_top; // gradient top color
Color bg_grad_bottom; // gradient bottom color
// Wallpaper (valid when bg_image == true)
char bg_image_path[128]; // VFS path to current wallpaper
uint32_t* bg_wallpaper; // scaled ARGB pixel buffer (screen-sized)
int bg_wallpaper_w; // scaled width
int bg_wallpaper_h; // scaled height
// Panel
Color panel_color; // panel background color
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -39,13 +39,14 @@ namespace heap_detail {
g_head.next = node;
}
static inline void grow(uint64_t bytes) {
static inline bool grow(uint64_t bytes) {
uint64_t pages = (bytes + 0xFFF) / 0x1000;
if (pages < 4) pages = 4; // grow at least 16 KiB at a time
void* mem = zenith::alloc(pages * 0x1000);
if (mem != nullptr)
insert_free(mem, pages * 0x1000);
if (mem == nullptr) return false;
insert_free(mem, pages * 0x1000);
return true;
}
} // namespace heap_detail
@@ -93,7 +94,8 @@ namespace heap_detail {
}
// No fit — grow and retry
grow(needed);
if (!grow(needed))
return nullptr;
return malloc(size);
}
+65
View File
@@ -0,0 +1,65 @@
# Makefile for libjpeg (JPEG decoding via stb_image) on ZenithOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CC := $(TOOLCHAIN_PREFIX)gcc
AR := $(TOOLCHAIN_PREFIX)ar
else
CC := gcc
AR := ar
endif
# ---- Paths ----
LIBC_INC := ../../include/libc
PROG_INC := ../../include
OBJDIR := obj
# ---- Compiler flags ----
CFLAGS := \
-std=gnu11 \
-g -O2 -pipe \
-Wall \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(LIBC_INC) \
-isystem $(shell $(CC) -print-file-name=include)
# ---- Target ----
TARGET := libjpeg.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJDIR)/stb_image_impl.o
$(AR) rcs $@ $^
$(OBJDIR)/stb_image_impl.o: stb_image_impl.c Makefile
@mkdir -p $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
Binary file not shown.
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
/*
* stb_image_impl.c
* Single compilation unit for stb_image JPEG decoder on ZenithOS
* Copyright (c) 2026 Daniel Hammer
*/
#include <stdlib.h>
#include <string.h>
/* Only enable JPEG; disable everything else. */
#define STBI_ONLY_JPEG
#define STBI_NO_PNG
#define STBI_NO_BMP
#define STBI_NO_PSD
#define STBI_NO_TGA
#define STBI_NO_GIF
#define STBI_NO_HDR
#define STBI_NO_PIC
#define STBI_NO_PNM
#define STBI_NO_LINEAR
#define STBI_NO_STDIO
#define STBI_NO_THREAD_LOCALS
/* Route allocations through libc (which uses ZenithOS heap). */
#define STBI_MALLOC(sz) malloc(sz)
#define STBI_FREE(p) free(p)
#define STBI_REALLOC(p, newsz) realloc(p, newsz)
#define STB_IMAGE_IMPLEMENTATION
#include <gui/stb_image.h>
+8 -2
View File
@@ -16,6 +16,8 @@ endif
# ---- Paths ----
PROG_INC := ../../include
JPEG_LIB := ../../lib/libjpeg
LIBC_LIB := ../../lib/libc
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
@@ -72,9 +74,13 @@ TARGET := $(BINDIR)/os/desktop.elf
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
# ---- Libraries ----
LIBS := $(JPEG_LIB)/libjpeg.a $(LIBC_LIB)/liblibc.a
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
# C++ sources
$(OBJDIR)/%.o: %.cpp Makefile
+11 -3
View File
@@ -57,6 +57,10 @@ static bool str_ends_with(const char* s, const char* suffix) {
return true;
}
static bool is_image_file(const char* name) {
return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg");
}
static int detect_file_type(const char* name, bool is_dir) {
if (is_dir) return 1;
if (str_ends_with(name, ".elf")) return 2;
@@ -545,7 +549,9 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
str_append(fullpath, "/", 512);
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (fm->desktop) {
if (is_image_file(fm->entry_names[clicked_idx])) {
zenith::spawn("0:/os/imageviewer.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
@@ -574,7 +580,7 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
if (fm->is_dir[clicked_idx]) {
filemanager_navigate(fm, fm->entry_names[clicked_idx]);
} else {
// Open file in text editor
// Open file in appropriate viewer
char fullpath[512];
zenith::strcpy(fullpath, fm->current_path);
int plen = zenith::slen(fullpath);
@@ -582,7 +588,9 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
str_append(fullpath, "/", 512);
}
str_append(fullpath, fm->entry_names[clicked_idx], 512);
if (fm->desktop) {
if (is_image_file(fm->entry_names[clicked_idx])) {
zenith::spawn("0:/os/imageviewer.elf", fullpath);
} else if (fm->desktop) {
open_texteditor_with_file(fm->desktop, fullpath);
}
}
+106 -10
View File
@@ -5,6 +5,7 @@
*/
#include "apps_common.hpp"
#include "wallpaper.hpp"
// ============================================================================
// Settings state
@@ -15,6 +16,8 @@ struct SettingsState {
int active_tab; // 0=Appearance, 1=Display, 2=About
Zenith::SysInfo sys_info;
uint64_t uptime_ms;
WallpaperFileList wp_files;
bool wp_scanned;
};
// ============================================================================
@@ -130,9 +133,12 @@ static void draw_toggle_btn(Canvas& c, int x, int y, int bw, int bh,
c.text(x + (bw - tw) / 2, y + (bh - fh) / 2, label, fg);
}
static constexpr int WP_ITEM_H = 24;
static void settings_draw_appearance(Canvas& c, SettingsState* st) {
DesktopSettings& s = st->desktop->settings;
Color accent = s.accent_color;
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
int x = 16;
int y = 12;
int sfh = system_font_height();
@@ -142,27 +148,80 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
c.text(x, y, "Background", colors::TEXT_COLOR);
y += line_h;
// Radio buttons: Gradient / Solid
draw_radio(c, x, y, s.bg_gradient, accent);
// Radio buttons: Gradient / Solid Color / Image
bool mode_grad = s.bg_gradient && !s.bg_image;
bool mode_solid = !s.bg_gradient && !s.bg_image;
bool mode_image = s.bg_image;
draw_radio(c, x, y, mode_grad, accent);
c.text(x + 20, y + 2, "Gradient", colors::TEXT_COLOR);
draw_radio(c, x + 120, y, !s.bg_gradient, accent);
c.text(x + 140, y + 2, "Solid Color", colors::TEXT_COLOR);
draw_radio(c, x + 120, y, mode_solid, accent);
c.text(x + 140, y + 2, "Solid", colors::TEXT_COLOR);
draw_radio(c, x + 220, y, mode_image, accent);
c.text(x + 240, y + 2, "Image", colors::TEXT_COLOR);
y += line_h + 4;
if (s.bg_gradient) {
if (mode_image) {
// Scan for images lazily
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
st->wp_scanned = true;
}
c.text(x, y, "Images in 0:/home/", dim);
y += sfh + 6;
if (st->wp_files.count == 0) {
c.text(x + 8, y, "No .jpg files found", dim);
y += WP_ITEM_H;
} else {
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
// Build full path for comparison
char fullpath[256];
zenith::strcpy(fullpath, "0:/home/");
str_append(fullpath, st->wp_files.names[i], 256);
bool selected = s.bg_image &&
zenith::streq(s.bg_image_path, fullpath);
if (selected) {
c.fill_rounded_rect(x, y, c.w - 2 * x, WP_ITEM_H, 3, accent);
}
// Truncate long filenames
char label[40];
int nlen = zenith::slen(st->wp_files.names[i]);
if (nlen > 35) {
zenith::strncpy(label, st->wp_files.names[i], 32);
label[32] = '.';
label[33] = '.';
label[34] = '.';
label[35] = '\0';
} else {
zenith::strncpy(label, st->wp_files.names[i], 39);
}
Color tc = selected ? colors::WHITE : colors::TEXT_COLOR;
c.text(x + 8, y + (WP_ITEM_H - sfh) / 2, label, tc);
y += WP_ITEM_H;
}
}
y += 6;
} else if (mode_grad) {
// Top color swatches
c.text(x, y + 4, "Top", Color::from_rgb(0x88, 0x88, 0x88));
c.text(x, y + 4, "Top", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_top, accent);
y += SWATCH_SIZE + 14;
// Bottom color swatches
c.text(x, y + 4, "Bottom", Color::from_rgb(0x88, 0x88, 0x88));
c.text(x, y + 4, "Bottom", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_grad_bottom, accent);
y += SWATCH_SIZE + 14;
} else {
// Solid color swatches
c.text(x, y + 4, "Color", Color::from_rgb(0x88, 0x88, 0x88));
c.text(x, y + 4, "Color", dim);
draw_swatch_row(c, x + 70, y, bg_palette, s.bg_solid, accent);
y += SWATCH_SIZE + 14;
}
@@ -363,23 +422,58 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
int line_h = sfh + 10;
int y = 12;
bool mode_grad = s.bg_gradient && !s.bg_image;
bool mode_image = s.bg_image;
// "Background" label
y += line_h;
// Radio: Gradient
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
s.bg_gradient = true;
s.bg_image = false;
return;
}
// Radio: Solid
if (mx >= x + 120 && mx < x + 260 && cy >= y && cy < y + 16) {
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
s.bg_gradient = false;
s.bg_image = false;
return;
}
// Radio: Image
if (mx >= x + 220 && mx < x + 320 && cy >= y && cy < y + 16) {
s.bg_image = true;
s.bg_gradient = false;
if (!st->wp_scanned) {
wallpaper_scan_dir("0:/home", &st->wp_files);
st->wp_scanned = true;
}
return;
}
y += line_h + 4;
int idx;
if (s.bg_gradient) {
if (mode_image) {
// "Images in 0:/home/" label
y += sfh + 6;
// File list clicks
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
if (cy >= y && cy < y + WP_ITEM_H &&
mx >= x && mx < win->content_w - x) {
// Build full path and load wallpaper
char fullpath[256];
zenith::strcpy(fullpath, "0:/home/");
str_append(fullpath, st->wp_files.names[i], 256);
wallpaper_load(&s, fullpath,
st->desktop->screen_w, st->desktop->screen_h);
return;
}
y += WP_ITEM_H;
}
if (st->wp_files.count == 0) y += WP_ITEM_H;
y += 6;
} else if (mode_grad) {
// Top swatches
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
s.bg_grad_top = bg_palette[idx];
@@ -504,6 +598,8 @@ void open_settings(DesktopState* ds) {
st->active_tab = 0;
zenith::get_info(&st->sys_info);
st->uptime_ms = zenith::get_milliseconds();
st->wp_scanned = false;
st->wp_files.count = 0;
win->app_data = st;
win->on_draw = settings_on_draw;
+21 -4
View File
@@ -5,6 +5,7 @@
*/
#include "apps_common.hpp"
#include "wallpaper.hpp"
// ============================================================================
// Desktop Implementation
@@ -71,14 +72,23 @@ void gui::desktop_init(DesktopState* ds) {
// Settings defaults
ds->settings.bg_gradient = true;
ds->settings.bg_image = false;
ds->settings.bg_grad_top = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.bg_grad_bottom = Color::from_rgb(0xA0, 0xA8, 0xB8);
ds->settings.bg_solid = Color::from_rgb(0xD0, 0xD8, 0xE8);
ds->settings.panel_color = colors::PANEL_BG;
ds->settings.bg_image_path[0] = '\0';
ds->settings.bg_wallpaper = nullptr;
ds->settings.bg_wallpaper_w = 0;
ds->settings.bg_wallpaper_h = 0;
ds->settings.panel_color = Color::from_rgb(0x10, 0x10, 0x10);
ds->settings.accent_color = colors::ACCENT;
ds->settings.show_shadows = true;
ds->settings.clock_24h = true;
ds->settings.ui_scale = 1;
// Try to load default wallpaper
wallpaper_load(&ds->settings, "0:/home/troy-olson-MhYIwOiyZpM-unsplash.jpg",
ds->screen_w, ds->screen_h);
zenith::win_setscale(1);
ds->ctx_menu_open = false;
@@ -299,8 +309,10 @@ void gui::desktop_draw_panel(DesktopState* ds) {
fb.fill_rect(0, y, sw, 1, Color::from_rgb(r, g, b));
}
// Bottom highlight line
fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10));
// Bottom highlight line (skip when wallpaper is set — the white bleeds through)
if (!ds->settings.bg_image) {
fb.fill_rect(0, PANEL_HEIGHT - 1, sw, 1, Color::from_rgba(0xFF, 0xFF, 0xFF, 0x10));
}
// App menu button (left side)
int btn_x = 4;
@@ -880,7 +892,12 @@ void gui::desktop_compose(DesktopState* ds) {
for (int y = 0; y < sh; y++) {
uint32_t* row = (uint32_t*)((uint8_t*)buf + y * pitch);
if (y < grad_start) {
if (ds->settings.bg_image && ds->settings.bg_wallpaper) {
// Wallpaper covers the entire screen; panel draws on top
uint32_t* wp = ds->settings.bg_wallpaper + y * ds->settings.bg_wallpaper_w;
int cw = sw < ds->settings.bg_wallpaper_w ? sw : ds->settings.bg_wallpaper_w;
for (int x = 0; x < cw; x++) row[x] = wp[x];
} else if (y < grad_start) {
// Panel area - will be overwritten by panel drawing
uint32_t px = ds->settings.panel_color.to_pixel();
for (int x = 0; x < sw; x++) row[x] = px;
+215
View File
@@ -0,0 +1,215 @@
/*
* wallpaper.hpp
* ZenithOS Desktop - JPEG wallpaper loading, scaling, and directory scanning
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/desktop.hpp>
// Forward-declare stb_image functions (implementation in libjpeg.a).
// We avoid including stb_image.h directly because its declaration section
// pulls in <stdlib.h> which is unavailable in the desktop's freestanding build.
extern "C" {
unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len,
int* x, int* y, int* channels_in_file,
int desired_channels);
void stbi_image_free(void* retval_from_stbi_load);
const char* stbi_failure_reason(void);
}
namespace gui {
// ============================================================================
// Wallpaper loading
// ============================================================================
// Load a JPEG file and scale it to cover the given screen dimensions.
// Stores the scaled ARGB pixel buffer in settings. Returns true on success.
inline bool wallpaper_load(DesktopSettings* s, const char* path,
int screen_w, int screen_h) {
// Free existing wallpaper
if (s->bg_wallpaper) {
zenith::mfree(s->bg_wallpaper);
s->bg_wallpaper = nullptr;
s->bg_wallpaper_w = 0;
s->bg_wallpaper_h = 0;
}
// Read file
int fd = zenith::open(path);
if (fd < 0) return false;
uint64_t size = zenith::getsize(fd);
if (size == 0 || size > 16 * 1024 * 1024) {
zenith::close(fd);
return false;
}
uint8_t* filedata = (uint8_t*)zenith::malloc(size);
if (!filedata) { zenith::close(fd); return false; }
int bytes_read = zenith::read(fd, filedata, 0, size);
zenith::close(fd);
if (bytes_read <= 0) { zenith::mfree(filedata); return false; }
// Decode JPEG
int img_w, img_h, channels;
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read,
&img_w, &img_h, &channels, 3);
zenith::mfree(filedata);
if (!rgb) return false;
// Scale to cover screen (crop to fill, maintain aspect ratio)
int dst_w = screen_w;
int dst_h = screen_h;
uint32_t* scaled = (uint32_t*)zenith::malloc((uint64_t)dst_w * dst_h * 4);
if (!scaled) { stbi_image_free(rgb); return false; }
// Compute source crop region for "cover" scaling
int src_crop_w, src_crop_h, src_x0, src_y0;
if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) {
// Image is wider — crop sides
src_crop_h = img_h;
src_crop_w = (int)((int64_t)img_h * dst_w / dst_h);
src_x0 = (img_w - src_crop_w) / 2;
src_y0 = 0;
} else {
// Image is taller — crop top/bottom
src_crop_w = img_w;
src_crop_h = (int)((int64_t)img_w * dst_h / dst_w);
src_x0 = 0;
src_y0 = (img_h - src_crop_h) / 2;
}
// Nearest-neighbor scale from cropped region to destination
for (int y = 0; y < dst_h; y++) {
int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h);
if (sy < 0) sy = 0;
if (sy >= img_h) sy = img_h - 1;
for (int x = 0; x < dst_w; x++) {
int sx = src_x0 + (int)((int64_t)x * src_crop_w / dst_w);
if (sx < 0) sx = 0;
if (sx >= img_w) sx = img_w - 1;
int si = (sy * img_w + sx) * 3;
scaled[y * dst_w + x] = 0xFF000000u
| ((uint32_t)rgb[si] << 16)
| ((uint32_t)rgb[si + 1] << 8)
| (uint32_t)rgb[si + 2];
}
}
stbi_image_free(rgb);
s->bg_wallpaper = scaled;
s->bg_wallpaper_w = dst_w;
s->bg_wallpaper_h = dst_h;
zenith::strncpy(s->bg_image_path, path, 127);
s->bg_image = true;
s->bg_gradient = false;
return true;
}
inline void wallpaper_free(DesktopSettings* s) {
if (s->bg_wallpaper) {
zenith::mfree(s->bg_wallpaper);
s->bg_wallpaper = nullptr;
s->bg_wallpaper_w = 0;
s->bg_wallpaper_h = 0;
}
s->bg_image_path[0] = '\0';
s->bg_image = false;
}
// ============================================================================
// Directory scanning for JPEG files
// ============================================================================
static constexpr int WALLPAPER_MAX_FILES = 16;
struct WallpaperFileList {
char names[WALLPAPER_MAX_FILES][64];
int count;
};
inline void wallpaper_scan_dir(const char* dir_path, WallpaperFileList* list) {
list->count = 0;
const char* raw_names[64];
int total = zenith::readdir(dir_path, raw_names, 64);
if (total <= 0) return;
// Compute prefix to strip (readdir returns full paths from VFS root)
const char* after_drive = dir_path;
for (int k = 0; after_drive[k]; k++) {
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
after_drive += k + 2;
break;
}
}
char prefix[256] = {0};
int prefix_len = 0;
if (after_drive[0] != '\0') {
zenith::strcpy(prefix, after_drive);
prefix_len = zenith::slen(prefix);
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
prefix[prefix_len++] = '/';
prefix[prefix_len] = '\0';
}
}
auto to_lower = [](char c) -> char {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
};
for (int i = 0; i < total && list->count < WALLPAPER_MAX_FILES; i++) {
const char* name = raw_names[i];
// Strip prefix
if (prefix_len > 0) {
bool match = true;
for (int k = 0; k < prefix_len; k++) {
if (name[k] != prefix[k]) { match = false; break; }
}
if (match) name += prefix_len;
}
int nlen = zenith::slen(name);
// Skip directories
if (nlen > 0 && name[nlen - 1] == '/') continue;
// Check for .jpg
bool is_jpeg = false;
if (nlen >= 4 &&
to_lower(name[nlen - 4]) == '.' &&
to_lower(name[nlen - 3]) == 'j' &&
to_lower(name[nlen - 2]) == 'p' &&
to_lower(name[nlen - 1]) == 'g') {
is_jpeg = true;
}
// Check for .jpeg
if (!is_jpeg && nlen >= 5 &&
to_lower(name[nlen - 5]) == '.' &&
to_lower(name[nlen - 4]) == 'j' &&
to_lower(name[nlen - 3]) == 'p' &&
to_lower(name[nlen - 2]) == 'e' &&
to_lower(name[nlen - 1]) == 'g') {
is_jpeg = true;
}
if (is_jpeg) {
zenith::strncpy(list->names[list->count], name, 63);
list->count++;
}
}
}
} // namespace gui
+92
View File
@@ -0,0 +1,92 @@
# Makefile for imageviewer (standalone JPEG viewer) on ZenithOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
# ---- Toolchain ----
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
# ---- Paths ----
JPEG_LIB := ../../lib/libjpeg
LIBC_LIB := ../../lib/libc
LIBC_INC := ../../include/libc
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
-isystem $(LIBC_INC) \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
# ---- Linker flags ----
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Libraries ----
LIBS := $(JPEG_LIB)/libjpeg.a $(LIBC_LIB)/liblibc.a
# ---- Source files ----
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/os/imageviewer.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+418
View File
@@ -0,0 +1,418 @@
/*
* main.cpp
* ZenithOS Image Viewer - standalone Window Server process
* Displays JPEG images with pan via mouse drag, scroll wheel, and arrow keys
* Copyright (c) 2026 Daniel Hammer
*/
#include <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdlib.h>
#include <gui/stb_image.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int INIT_W = 800;
static constexpr int INIT_H = 600;
static constexpr int STATUS_H = 24;
static constexpr int PAN_STEP = 40;
static constexpr int FONT_SIZE = 13;
static constexpr Color BG_COLOR = Color::from_rgb(0x30, 0x30, 0x30);
static constexpr Color STATUS_BG = Color::from_rgb(0x24, 0x24, 0x24);
static constexpr Color STATUS_TEXT = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color ERR_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
// ============================================================================
// App state
// ============================================================================
static int g_win_w = INIT_W;
static int g_win_h = INIT_H;
// Image data
static uint32_t* g_image = nullptr; // ARGB pixel buffer
static int g_img_w = 0;
static int g_img_h = 0;
// Pan offset (image position relative to viewport)
static int g_pan_x = 0;
static int g_pan_y = 0;
// Mouse drag state
static bool g_dragging = false;
static int g_drag_start_x = 0;
static int g_drag_start_y = 0;
static int g_drag_pan_x = 0;
static int g_drag_pan_y = 0;
// File info
static char g_filename[128] = {};
static char g_status[256] = {};
static bool g_load_ok = false;
// Font
static TrueTypeFont* g_font = nullptr;
// ============================================================================
// Helpers
// ============================================================================
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
// Extract basename from a VFS path (e.g. "0:/photos/cat.jpg" -> "cat.jpg")
static const char* basename(const char* path) {
const char* last = path;
for (const char* p = path; *p; p++) {
if (*p == '/') last = p + 1;
}
return last;
}
// Format an integer into a decimal string
static void int_to_str(char* buf, int val) {
if (val == 0) { buf[0] = '0'; buf[1] = '\0'; return; }
bool neg = val < 0;
if (neg) val = -val;
char tmp[16];
int i = 0;
while (val > 0 && i < 15) {
tmp[i++] = '0' + (val % 10);
val /= 10;
}
int j = 0;
if (neg) buf[j++] = '-';
while (i > 0) buf[j++] = tmp[--i];
buf[j] = '\0';
}
static void str_append(char* dst, const char* src, int maxlen) {
int len = zenith::slen(dst);
int i = 0;
while (src[i] && len + i < maxlen - 1) {
dst[len + i] = src[i];
i++;
}
dst[len + i] = '\0';
}
// ============================================================================
// Image loading
// ============================================================================
static bool load_image(const char* path) {
int fd = zenith::open(path);
if (fd < 0) {
zenith::strcpy(g_status, "Error: could not open file");
return false;
}
uint64_t size = zenith::getsize(fd);
if (size == 0 || size > 16 * 1024 * 1024) {
zenith::close(fd);
zenith::strcpy(g_status, "Error: file too large or empty");
return false;
}
uint8_t* filedata = (uint8_t*)zenith::malloc(size);
if (!filedata) {
zenith::close(fd);
zenith::strcpy(g_status, "Error: out of memory");
return false;
}
int bytes_read = zenith::read(fd, filedata, 0, size);
zenith::close(fd);
if (bytes_read <= 0) {
zenith::mfree(filedata);
zenith::strcpy(g_status, "Error: could not read file");
return false;
}
int w, h, channels;
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read, &w, &h, &channels, 3);
zenith::mfree(filedata);
if (!rgb) {
zenith::strcpy(g_status, "Error: ");
const char* reason = stbi_failure_reason();
if (reason) str_append(g_status, reason, 256);
else str_append(g_status, "unknown decode error", 256);
return false;
}
// Convert RGB to ARGB
g_image = (uint32_t*)zenith::malloc((uint64_t)w * h * 4);
if (!g_image) {
stbi_image_free(rgb);
zenith::strcpy(g_status, "Error: out of memory for image");
return false;
}
for (int i = 0; i < w * h; i++) {
uint8_t r = rgb[i * 3 + 0];
uint8_t g = rgb[i * 3 + 1];
uint8_t b = rgb[i * 3 + 2];
g_image[i] = 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
stbi_image_free(rgb);
g_img_w = w;
g_img_h = h;
// Build status string: "filename.jpg (1920 x 1080)"
const char* name = basename(path);
zenith::strncpy(g_filename, name, 127);
zenith::strcpy(g_status, g_filename);
str_append(g_status, " (", 256);
char num[16];
int_to_str(num, w);
str_append(g_status, num, 256);
str_append(g_status, " x ", 256);
int_to_str(num, h);
str_append(g_status, num, 256);
str_append(g_status, ")", 256);
return true;
}
// ============================================================================
// Pan clamping
// ============================================================================
static void clamp_pan() {
int view_h = g_win_h - STATUS_H;
// If image fits in viewport, center it
if (g_img_w <= g_win_w) {
g_pan_x = (g_win_w - g_img_w) / 2;
} else {
// Clamp so image edges don't leave the viewport
if (g_pan_x > 0) g_pan_x = 0;
if (g_pan_x < g_win_w - g_img_w) g_pan_x = g_win_w - g_img_w;
}
if (g_img_h <= view_h) {
g_pan_y = (view_h - g_img_h) / 2;
} else {
if (g_pan_y > 0) g_pan_y = 0;
if (g_pan_y < view_h - g_img_h) g_pan_y = view_h - g_img_h;
}
}
static void center_image() {
int view_h = g_win_h - STATUS_H;
g_pan_x = (g_win_w - g_img_w) / 2;
g_pan_y = (view_h - g_img_h) / 2;
clamp_pan();
}
// ============================================================================
// Rendering
// ============================================================================
static void render(uint32_t* pixels) {
int view_h = g_win_h - STATUS_H;
// Fill background
px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, view_h, BG_COLOR);
// Draw image
if (g_image && g_load_ok) {
for (int row = 0; row < g_img_h; row++) {
int dy = g_pan_y + row;
if (dy < 0 || dy >= view_h) continue;
for (int col = 0; col < g_img_w; col++) {
int dx = g_pan_x + col;
if (dx < 0 || dx >= g_win_w) continue;
pixels[dy * g_win_w + dx] = g_image[row * g_img_w + col];
}
}
} else if (!g_load_ok && g_font) {
// Show error message centered
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
20, view_h / 2 - 8, g_status, ERR_COLOR, 15);
}
// Status bar
px_fill(pixels, g_win_w, g_win_h, 0, view_h, g_win_w, STATUS_H, STATUS_BG);
if (g_font) {
g_font->draw_to_buffer(pixels, g_win_w, g_win_h,
8, view_h + (STATUS_H - FONT_SIZE) / 2, g_status, STATUS_TEXT, FONT_SIZE);
}
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Get file path from arguments
char filepath[512] = {};
int arglen = zenith::getargs(filepath, sizeof(filepath));
if (arglen <= 0) {
zenith::strcpy(filepath, "");
}
// Load font
auto load_font = [](const char* path) -> TrueTypeFont* {
TrueTypeFont* f = (TrueTypeFont*)zenith::malloc(sizeof(TrueTypeFont));
if (!f) return nullptr;
zenith::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init(path)) { zenith::mfree(f); return nullptr; }
return f;
};
g_font = load_font("0:/fonts/Roboto-Medium.ttf");
// Determine window title from filename
char title[64] = "Image Viewer";
if (filepath[0]) {
const char* name = basename(filepath);
if (name[0]) zenith::strncpy(title, name, 63);
}
// Create window
Zenith::WinCreateResult wres;
if (zenith::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0)
zenith::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
// Load image
if (filepath[0]) {
g_load_ok = load_image(filepath);
} else {
zenith::strcpy(g_status, "No file specified");
g_load_ok = false;
}
// Center image initially
if (g_load_ok) center_image();
// Initial render
render(pixels);
zenith::win_present(win_id);
// Event loop
while (true) {
Zenith::WinEvent ev;
int r = zenith::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) {
zenith::sleep_ms(16);
continue;
}
// Close event
if (ev.type == 3) break;
// Resize event
if (ev.type == 2) {
g_win_w = ev.resize.w;
g_win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)zenith::win_resize(win_id, g_win_w, g_win_h);
if (g_load_ok) clamp_pan();
render(pixels);
zenith::win_present(win_id);
continue;
}
// Key event
if (ev.type == 0 && ev.key.pressed) {
bool redraw = false;
// Q or Escape to close
if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01) {
break;
}
// Arrow keys to pan
if (ev.key.scancode == 0x48) { g_pan_y += PAN_STEP; redraw = true; } // Up
if (ev.key.scancode == 0x50) { g_pan_y -= PAN_STEP; redraw = true; } // Down
if (ev.key.scancode == 0x4B) { g_pan_x += PAN_STEP; redraw = true; } // Left
if (ev.key.scancode == 0x4D) { g_pan_x -= PAN_STEP; redraw = true; } // Right
// Home key to re-center
if (ev.key.scancode == 0x47) { center_image(); redraw = true; }
if (redraw && g_load_ok) {
clamp_pan();
render(pixels);
zenith::win_present(win_id);
}
continue;
}
// Mouse event
if (ev.type == 1) {
bool redraw = false;
bool left_now = ev.mouse.buttons & 1;
bool left_prev = ev.mouse.prev_buttons & 1;
// Start drag
if (left_now && !left_prev) {
g_dragging = true;
g_drag_start_x = ev.mouse.x;
g_drag_start_y = ev.mouse.y;
g_drag_pan_x = g_pan_x;
g_drag_pan_y = g_pan_y;
}
// Continue drag
if (left_now && g_dragging) {
g_pan_x = g_drag_pan_x + (ev.mouse.x - g_drag_start_x);
g_pan_y = g_drag_pan_y + (ev.mouse.y - g_drag_start_y);
redraw = true;
}
// End drag
if (!left_now && g_dragging) {
g_dragging = false;
}
// Scroll wheel for vertical pan
if (ev.mouse.scroll != 0) {
g_pan_y += ev.mouse.scroll * PAN_STEP;
redraw = true;
}
if (redraw && g_load_ok) {
clamp_pan();
render(pixels);
zenith::win_present(win_id);
}
continue;
}
}
// Cleanup
if (g_image) zenith::mfree(g_image);
zenith::win_destroy(win_id);
zenith::exit(0);
}
@@ -0,0 +1,35 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in ZenithOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <zenith/heap.h>
#include <zenith/string.h>
#include <gui/stb_math.h>
// Override all stb_truetype dependencies before including the implementation
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), zenith::malloc(x))
#define STBTT_free(x,u) ((void)(u), zenith::mfree(x))
#define STBTT_memcpy(d,s,n) zenith::memcpy(d,s,n)
#define STBTT_memset(d,v,n) zenith::memset(d,v,n)
#define STBTT_strlen(x) zenith::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>
BIN
View File
Binary file not shown.