diff --git a/programs/GNUmakefile b/programs/GNUmakefile index e19b72b..4a57b9f 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -57,7 +57,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer desktop +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview 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 imageviewer desktop icons fonts bearssl libc tls libjpeg +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview desktop icons fonts bearssl libc tls libjpeg -all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) +all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP) # Build BearSSL static library. bearssl: @@ -122,6 +122,10 @@ libjpeg: libc imageviewer: libc libjpeg $(MAKE) -C src/imageviewer +# Build fontpreview standalone GUI client (no library deps). +fontpreview: + $(MAKE) -C src/fontpreview + # Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper). desktop: libc libjpeg $(MAKE) -C src/desktop @@ -181,3 +185,4 @@ clean: $(MAKE) -C src/wikipedia clean $(MAKE) -C src/weather clean $(MAKE) -C src/imageviewer clean + $(MAKE) -C src/fontpreview clean diff --git a/programs/src/desktop/app_filemanager.cpp b/programs/src/desktop/app_filemanager.cpp index cb48129..bc72e0a 100644 --- a/programs/src/desktop/app_filemanager.cpp +++ b/programs/src/desktop/app_filemanager.cpp @@ -61,6 +61,10 @@ static bool is_image_file(const char* name) { return str_ends_with(name, ".jpg") || str_ends_with(name, ".jpeg"); } +static bool is_font_file(const char* name) { + return str_ends_with(name, ".ttf"); +} + static int detect_file_type(const char* name, bool is_dir) { if (is_dir) return 1; if (str_ends_with(name, ".elf")) return 2; @@ -551,6 +555,8 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { str_append(fullpath, fm->entry_names[clicked_idx], 512); if (is_image_file(fm->entry_names[clicked_idx])) { zenith::spawn("0:/os/imageviewer.elf", fullpath); + } else if (is_font_file(fm->entry_names[clicked_idx])) { + zenith::spawn("0:/os/fontpreview.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } @@ -590,6 +596,8 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { str_append(fullpath, fm->entry_names[clicked_idx], 512); if (is_image_file(fm->entry_names[clicked_idx])) { zenith::spawn("0:/os/imageviewer.elf", fullpath); + } else if (is_font_file(fm->entry_names[clicked_idx])) { + zenith::spawn("0:/os/fontpreview.elf", fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } diff --git a/programs/src/fontpreview/Makefile b/programs/src/fontpreview/Makefile new file mode 100644 index 0000000..1860ba5 --- /dev/null +++ b/programs/src/fontpreview/Makefile @@ -0,0 +1,84 @@ +# Makefile for fontpreview (standalone TTF font previewer) 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 ---- + +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 ../../../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) + +# ---- Source files ---- + +SRCS := main.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/os/fontpreview.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/os + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/fontpreview/main.cpp b/programs/src/fontpreview/main.cpp new file mode 100644 index 0000000..e8650b4 --- /dev/null +++ b/programs/src/fontpreview/main.cpp @@ -0,0 +1,449 @@ +/* + * main.cpp + * ZenithOS Font Preview - standalone Window Server process + * Displays TTF font samples at multiple sizes with vertical scrolling + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +using namespace gui; + +// ============================================================================ +// Constants — light theme matching ZenithOS desktop conventions +// ============================================================================ + +static constexpr int INIT_W = 800; +static constexpr int INIT_H = 600; +static constexpr int SCROLL_STEP = 40; +static constexpr int PADDING = 16; +static constexpr int SECTION_GAP = 12; +static constexpr int UI_FONT_SZ = 13; + +static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33); +static constexpr Color LABEL_COLOR = Color::from_rgb(0x88, 0x88, 0x88); +static constexpr Color SEPARATOR = Color::from_rgb(0xE0, 0xE0, 0xE0); +static constexpr Color ERR_COLOR = Color::from_rgb(0xCC, 0x33, 0x33); +static constexpr Color SCROLLBAR_BG = Color::from_rgb(0xE0, 0xE0, 0xE0); +static constexpr Color SCROLLBAR_FG = Color::from_rgb(0xAA, 0xAA, 0xAA); + +static const int PREVIEW_SIZES[] = {12, 16, 20, 24, 32, 48, 72}; +static constexpr int NUM_SIZES = 7; + +static const char* PANGRAM = "The quick brown fox jumps over the lazy dog"; +static const char* UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +static const char* LOWER_NUM = "abcdefghijklmnopqrstuvwxyz 0123456789"; + +// ============================================================================ +// Pre-rendered glyph cache — rasterised once at startup for all 7 sizes +// ============================================================================ + +struct PreviewGlyph { + uint8_t* bitmap; + int width, height; + int xoff, yoff; + int advance; +}; + +struct SizeCache { + PreviewGlyph glyphs[128]; // printable ASCII is enough + int pixel_size; + int ascent; + int line_height; +}; + +static SizeCache g_caches[NUM_SIZES]; + +// ============================================================================ +// App state +// ============================================================================ + +static int g_win_w = INIT_W; +static int g_win_h = INIT_H; +static int g_scroll_y = 0; +static int g_content_h = 0; + +static bool g_load_ok = false; + +static TrueTypeFont* g_ui_font = nullptr; + +static stbtt_fontinfo g_preview_info; +static uint8_t* g_preview_data = 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; +} + +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; +} + +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'; +} + +// ============================================================================ +// Pre-render all glyphs at all sizes (called once at startup) +// ============================================================================ + +static void prerrender_glyphs() { + for (int s = 0; s < NUM_SIZES; s++) { + SizeCache* sc = &g_caches[s]; + sc->pixel_size = PREVIEW_SIZES[s]; + float scale = stbtt_ScaleForPixelHeight(&g_preview_info, (float)sc->pixel_size); + + int asc, desc, lg; + stbtt_GetFontVMetrics(&g_preview_info, &asc, &desc, &lg); + sc->ascent = (int)(asc * scale); + sc->line_height = (int)((asc - desc + lg) * scale); + + for (int cp = 0; cp < 128; cp++) { + PreviewGlyph* g = &sc->glyphs[cp]; + g->bitmap = nullptr; + g->width = g->height = 0; + g->xoff = g->yoff = 0; + g->advance = 0; + + if (cp < 0x20) continue; // skip control chars + + int advance, lsb; + stbtt_GetCodepointHMetrics(&g_preview_info, cp, &advance, &lsb); + g->advance = (int)(advance * scale); + + int x0, y0, x1, y1; + stbtt_GetCodepointBitmapBox(&g_preview_info, cp, scale, scale, + &x0, &y0, &x1, &y1); + g->width = x1 - x0; + g->height = y1 - y0; + g->xoff = x0; + g->yoff = y0; + + if (g->width > 0 && g->height > 0) { + g->bitmap = (uint8_t*)zenith::malloc(g->width * g->height); + if (g->bitmap) { + stbtt_MakeCodepointBitmap(&g_preview_info, g->bitmap, + g->width, g->height, g->width, scale, scale, cp); + } + } + } + } +} + +// ============================================================================ +// Draw text from pre-rendered cache (fast blit, no rasterisation) +// ============================================================================ + +static void draw_cached_text(uint32_t* pixels, int buf_w, int buf_h, + int x, int y, const char* text, + Color color, int size_idx) { + SizeCache* sc = &g_caches[size_idx]; + int baseline = y + sc->ascent; + int cx = x; + + for (int i = 0; text[i]; i++) { + int cp = (unsigned char)text[i]; + if (cp >= 128) continue; + PreviewGlyph* g = &sc->glyphs[cp]; + + if (g->bitmap) { + int gx = cx + g->xoff; + int gy = baseline + g->yoff; + for (int row = 0; row < g->height; row++) { + int dy = gy + row; + if (dy < 0 || dy >= buf_h) continue; + for (int col = 0; col < g->width; col++) { + int dx = gx + col; + if (dx < 0 || dx >= buf_w) continue; + uint8_t alpha = g->bitmap[row * g->width + col]; + if (alpha == 0) continue; + + if (alpha == 255) { + pixels[dy * buf_w + dx] = + 0xFF000000 | ((uint32_t)color.r << 16) | + ((uint32_t)color.g << 8) | color.b; + } else { + uint32_t dst = pixels[dy * buf_w + dx]; + uint8_t dr = (dst >> 16) & 0xFF; + uint8_t dg = (dst >> 8) & 0xFF; + uint8_t db = dst & 0xFF; + uint32_t a = alpha, inv_a = 255 - alpha; + uint32_t rr = (a * color.r + inv_a * dr + 128) / 255; + uint32_t gg = (a * color.g + inv_a * dg + 128) / 255; + uint32_t bb = (a * color.b + inv_a * db + 128) / 255; + pixels[dy * buf_w + dx] = + 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } + } + cx += g->advance; + } +} + +// ============================================================================ +// Content height calculation +// ============================================================================ + +static int calc_content_height() { + if (!g_load_ok) return 200; + + int y = PADDING; + + for (int s = 0; s < NUM_SIZES; s++) { + if (g_ui_font) y += g_ui_font->get_line_height(UI_FONT_SZ) + 4; + + int lh = g_caches[s].line_height; + y += lh * 3; + + y += SECTION_GAP + 1 + SECTION_GAP; + } + + y += PADDING; + return y; +} + +// ============================================================================ +// Rendering +// ============================================================================ + +static void render(uint32_t* pixels) { + px_fill(pixels, g_win_w, g_win_h, 0, 0, g_win_w, g_win_h, BG_COLOR); + + if (!g_load_ok) { + if (g_ui_font) { + g_ui_font->draw_to_buffer(pixels, g_win_w, g_win_h, + PADDING, g_win_h / 2 - 8, "Error: could not load font", ERR_COLOR, 15); + } + return; + } + + int y = PADDING - g_scroll_y; + + for (int s = 0; s < NUM_SIZES; s++) { + int lh = g_caches[s].line_height; + int section_h = (g_ui_font ? g_ui_font->get_line_height(UI_FONT_SZ) + 4 : 0) + + lh * 3 + SECTION_GAP + 1 + SECTION_GAP; + + // Skip sections entirely above the viewport + if (y + section_h < 0) { + y += section_h; + continue; + } + // Stop once we're entirely below the viewport + if (y >= g_win_h) break; + + // Size label (e.g. "48px") + if (g_ui_font) { + char label[16] = {}; + int_to_str(label, PREVIEW_SIZES[s]); + str_append(label, "px", 16); + g_ui_font->draw_to_buffer(pixels, g_win_w, g_win_h, + PADDING, y, label, LABEL_COLOR, UI_FONT_SZ); + y += g_ui_font->get_line_height(UI_FONT_SZ) + 4; + } + + draw_cached_text(pixels, g_win_w, g_win_h, + PADDING, y, PANGRAM, TEXT_COLOR, s); + y += lh; + + draw_cached_text(pixels, g_win_w, g_win_h, + PADDING, y, UPPER, TEXT_COLOR, s); + y += lh; + + draw_cached_text(pixels, g_win_w, g_win_h, + PADDING, y, LOWER_NUM, TEXT_COLOR, s); + y += lh; + + y += SECTION_GAP; + if (y >= 0 && y < g_win_h) { + px_fill(pixels, g_win_w, g_win_h, + PADDING, y, g_win_w - 2 * PADDING, 1, SEPARATOR); + } + y += 1 + SECTION_GAP; + } + + // Scrollbar + int view_h = g_win_h; + if (g_content_h > view_h) { + int sb_x = g_win_w - 6; + int max_scroll = g_content_h - view_h; + int thumb_h = (view_h * view_h) / g_content_h; + if (thumb_h < 20) thumb_h = 20; + int thumb_y = (g_scroll_y * (view_h - thumb_h)) / max_scroll; + px_fill(pixels, g_win_w, g_win_h, sb_x, 0, 4, view_h, SCROLLBAR_BG); + px_fill(pixels, g_win_w, g_win_h, sb_x, thumb_y, 4, thumb_h, SCROLLBAR_FG); + } +} + +// ============================================================================ +// Scroll clamping +// ============================================================================ + +static void clamp_scroll() { + int max_scroll = g_content_h - g_win_h; + if (max_scroll < 0) max_scroll = 0; + if (g_scroll_y < 0) g_scroll_y = 0; + if (g_scroll_y > max_scroll) g_scroll_y = max_scroll; +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + char filepath[512] = {}; + int arglen = zenith::getargs(filepath, sizeof(filepath)); + if (arglen <= 0) zenith::strcpy(filepath, ""); + + // Load UI font for labels + 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_ui_font = load_font("0:/fonts/Roboto-Medium.ttf"); + + // Window title from filename + char title[64] = "Font Preview"; + if (filepath[0]) { + const char* name = basename(filepath); + if (name[0]) zenith::strncpy(title, name, 63); + } + + // Load preview font via stbtt + if (filepath[0]) { + int fd = zenith::open(filepath); + if (fd >= 0) { + uint64_t size = zenith::getsize(fd); + if (size > 0 && size <= 1024 * 1024) { + g_preview_data = (uint8_t*)zenith::malloc(size); + if (g_preview_data) { + zenith::read(fd, g_preview_data, 0, size); + if (stbtt_InitFont(&g_preview_info, g_preview_data, + stbtt_GetFontOffsetForIndex(g_preview_data, 0))) { + g_load_ok = true; + } + } + } + zenith::close(fd); + } + } + + // Pre-render all glyphs at all sizes (once, before event loop) + if (g_load_ok) prerrender_glyphs(); + + g_content_h = calc_content_height(); + + // 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; + + render(pixels); + zenith::win_present(win_id); + + 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; + } + + if (ev.type == 3) break; + + 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); + clamp_scroll(); + render(pixels); + zenith::win_present(win_id); + continue; + } + + if (ev.type == 0 && ev.key.pressed) { + bool redraw = false; + + if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01) + break; + + if (ev.key.scancode == 0x48) { g_scroll_y -= SCROLL_STEP; redraw = true; } + if (ev.key.scancode == 0x50) { g_scroll_y += SCROLL_STEP; redraw = true; } + if (ev.key.scancode == 0x47) { g_scroll_y = 0; redraw = true; } + if (ev.key.scancode == 0x4F) { g_scroll_y = g_content_h; redraw = true; } + if (ev.key.scancode == 0x49) { g_scroll_y -= g_win_h; redraw = true; } + if (ev.key.scancode == 0x51) { g_scroll_y += g_win_h; redraw = true; } + + if (redraw) { + clamp_scroll(); + render(pixels); + zenith::win_present(win_id); + } + continue; + } + + if (ev.type == 1) { + if (ev.mouse.scroll != 0) { + g_scroll_y -= ev.mouse.scroll * SCROLL_STEP; + clamp_scroll(); + render(pixels); + zenith::win_present(win_id); + } + continue; + } + } + + zenith::win_destroy(win_id); + zenith::exit(0); +} diff --git a/programs/src/fontpreview/stb_truetype_impl.cpp b/programs/src/fontpreview/stb_truetype_impl.cpp new file mode 100644 index 0000000..bc52e9e --- /dev/null +++ b/programs/src/fontpreview/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in ZenithOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// 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 diff --git a/ramdisk.tar b/ramdisk.tar index fab3d99..cc622a3 100644 Binary files a/ramdisk.tar and b/ramdisk.tar differ