feat: add Font Preview app

This commit is contained in:
2026-02-27 19:17:09 +01:00
parent e95089192f
commit bd16b9afd1
6 changed files with 584 additions and 3 deletions
+8
View File
@@ -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);
}
+84
View File
@@ -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)
+449
View File
@@ -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 <zenith/syscall.h>
#include <zenith/string.h>
#include <zenith/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
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);
}
@@ -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>