feat: TrueType (TTF) font rendering, many new desktop applications and DOOM support, among other improvements
This commit is contained in:
@@ -99,6 +99,10 @@ struct Canvas {
|
||||
// ---- Text ----
|
||||
|
||||
void text(int x, int y, const char* str, Color c) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::UI_SIZE);
|
||||
return;
|
||||
}
|
||||
uint32_t px = c.to_pixel();
|
||||
for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH <= w; i++) {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT];
|
||||
@@ -118,6 +122,10 @@ struct Canvas {
|
||||
}
|
||||
|
||||
void text_2x(int x, int y, const char* str, Color c) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
fonts::system_font->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::LARGE_SIZE);
|
||||
return;
|
||||
}
|
||||
uint32_t px = c.to_pixel();
|
||||
for (int i = 0; str[i] && x + (i + 1) * FONT_WIDTH * 2 <= w; i++) {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)str[i] * FONT_HEIGHT];
|
||||
@@ -141,6 +149,14 @@ struct Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
void text_mono(int x, int y, const char* str, Color c) {
|
||||
if (fonts::mono && fonts::mono->valid) {
|
||||
fonts::mono->draw_to_buffer(pixels, w, h, x, y, str, c, fonts::TERM_SIZE);
|
||||
return;
|
||||
}
|
||||
text(x, y, str, c);
|
||||
}
|
||||
|
||||
// ---- Icons ----
|
||||
|
||||
void icon(int x, int y, const SvgIcon& ic) {
|
||||
@@ -176,7 +192,8 @@ struct Canvas {
|
||||
|
||||
// ---- High-level helpers ----
|
||||
|
||||
void kv_line(int x, int* y, const char* line, Color c, int line_h = FONT_HEIGHT + 6) {
|
||||
void kv_line(int x, int* y, const char* line, Color c, int line_h = 0) {
|
||||
if (line_h == 0) line_h = system_font_height() + 6;
|
||||
text(x, *y, line, c);
|
||||
*y += line_h;
|
||||
}
|
||||
@@ -190,8 +207,9 @@ struct Canvas {
|
||||
Color bg, Color fg, int radius = 4) {
|
||||
fill_rounded_rect(x, y, bw, bh, radius, bg);
|
||||
int tw = text_width(label);
|
||||
int fh = system_font_height();
|
||||
int tx = x + (bw - tw) / 2;
|
||||
int ty = y + (bh - FONT_HEIGHT) / 2;
|
||||
int ty = y + (bh - fh) / 2;
|
||||
text(tx, ty, label, fg);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,6 +18,25 @@ namespace gui {
|
||||
static constexpr int MAX_WINDOWS = 8;
|
||||
static constexpr int PANEL_HEIGHT = 32;
|
||||
|
||||
struct DesktopSettings {
|
||||
// Background
|
||||
bool bg_gradient; // true = gradient, false = solid
|
||||
Color bg_solid; // solid background color
|
||||
Color bg_grad_top; // gradient top color
|
||||
Color bg_grad_bottom; // gradient bottom color
|
||||
|
||||
// Panel
|
||||
Color panel_color; // panel background color
|
||||
|
||||
// Accent
|
||||
Color accent_color; // buttons, highlights, active indicators
|
||||
|
||||
// Display
|
||||
bool show_shadows; // window shadows on/off
|
||||
bool clock_24h; // 24-hour clock format
|
||||
int ui_scale; // 0=Small, 1=Default, 2=Large
|
||||
};
|
||||
|
||||
struct DesktopState {
|
||||
Framebuffer fb;
|
||||
Window windows[MAX_WINDOWS];
|
||||
@@ -54,6 +73,11 @@ struct DesktopState {
|
||||
SvgIcon icon_settings;
|
||||
SvgIcon icon_reboot;
|
||||
|
||||
SvgIcon icon_doom;
|
||||
SvgIcon icon_procmgr;
|
||||
SvgIcon icon_mandelbrot;
|
||||
SvgIcon icon_devexplorer;
|
||||
|
||||
bool ctx_menu_open;
|
||||
int ctx_menu_x, ctx_menu_y;
|
||||
|
||||
@@ -63,6 +87,8 @@ struct DesktopState {
|
||||
Rect net_icon_rect;
|
||||
|
||||
int screen_w, screen_h;
|
||||
|
||||
DesktopSettings settings;
|
||||
};
|
||||
|
||||
// Forward declarations - implemented in main.cpp
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* font.hpp
|
||||
* ZenithOS 8x16 VGA bitmap font rendering
|
||||
* ZenithOS text rendering — TrueType with bitmap fallback
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "gui/gui.hpp"
|
||||
#include "gui/framebuffer.hpp"
|
||||
#include "gui/truetype.hpp"
|
||||
|
||||
namespace gui {
|
||||
|
||||
@@ -16,6 +17,30 @@ static constexpr int FONT_HEIGHT = 16;
|
||||
// Defined in font_data.cpp
|
||||
extern const uint8_t font_data[256 * 16];
|
||||
|
||||
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
||||
inline int system_font_height() {
|
||||
if (fonts::system_font && fonts::system_font->valid)
|
||||
return fonts::system_font->get_line_height(fonts::UI_SIZE);
|
||||
return FONT_HEIGHT;
|
||||
}
|
||||
|
||||
// Dynamic mono font cell dimensions
|
||||
inline int mono_cell_width() {
|
||||
if (fonts::mono && fonts::mono->valid) {
|
||||
// Monospace: all glyphs have the same advance
|
||||
GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE);
|
||||
CachedGlyph* g = fonts::mono->get_glyph(gc, 'M');
|
||||
if (g) return g->advance;
|
||||
}
|
||||
return FONT_WIDTH;
|
||||
}
|
||||
|
||||
inline int mono_cell_height() {
|
||||
if (fonts::mono && fonts::mono->valid)
|
||||
return fonts::mono->get_line_height(fonts::TERM_SIZE);
|
||||
return FONT_HEIGHT;
|
||||
}
|
||||
|
||||
inline void draw_char(Framebuffer& fb, int x, int y, char c, Color fg) {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)c * FONT_HEIGHT];
|
||||
for (int row = 0; row < FONT_HEIGHT; row++) {
|
||||
@@ -43,18 +68,29 @@ inline void draw_char_bg(Framebuffer& fb, int x, int y, char c, Color fg, Color
|
||||
}
|
||||
|
||||
inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; text[i]; i++) {
|
||||
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
|
||||
}
|
||||
}
|
||||
|
||||
inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color fg, Color bg) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; text[i]; i++) {
|
||||
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
|
||||
}
|
||||
}
|
||||
|
||||
inline int text_width(const char* text) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
||||
}
|
||||
int len = 0;
|
||||
while (text[len]) len++;
|
||||
return len * FONT_WIDTH;
|
||||
|
||||
@@ -25,13 +25,13 @@ inline fixed_t fixed_from_parts(int whole, int frac_num, int frac_den) {
|
||||
struct Color {
|
||||
uint8_t r, g, b, a;
|
||||
|
||||
static Color from_rgb(uint8_t r, uint8_t g, uint8_t b) { return {r, g, b, 255}; }
|
||||
static Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return {r, g, b, a}; }
|
||||
static Color from_hex(uint32_t hex) {
|
||||
static constexpr Color from_rgb(uint8_t r, uint8_t g, uint8_t b) { return {r, g, b, 255}; }
|
||||
static constexpr Color from_rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return {r, g, b, a}; }
|
||||
static constexpr Color from_hex(uint32_t hex) {
|
||||
return {(uint8_t)((hex >> 16) & 0xFF), (uint8_t)((hex >> 8) & 0xFF), (uint8_t)(hex & 0xFF), 255};
|
||||
}
|
||||
|
||||
uint32_t to_pixel() const { return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; }
|
||||
constexpr uint32_t to_pixel() const { return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; }
|
||||
};
|
||||
|
||||
// Named colors for the desktop theme
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* stb_math.h
|
||||
* Math functions for stb_truetype in freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#ifndef STB_MATH_H
|
||||
#define STB_MATH_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static inline double stb_floor(double x) {
|
||||
double i = (double)(long long)x;
|
||||
return (x < i) ? i - 1.0 : i;
|
||||
}
|
||||
|
||||
static inline double stb_ceil(double x) {
|
||||
double f = stb_floor(x);
|
||||
return (x > f) ? f + 1.0 : f;
|
||||
}
|
||||
|
||||
static inline double stb_fabs(double x) {
|
||||
return x < 0.0 ? -x : x;
|
||||
}
|
||||
|
||||
static inline double stb_fmod(double x, double y) {
|
||||
if (y == 0.0) return 0.0;
|
||||
return x - (double)((long long)(x / y)) * y;
|
||||
}
|
||||
|
||||
static inline double stb_sqrt(double x) {
|
||||
if (x <= 0.0) return 0.0;
|
||||
double guess = x;
|
||||
for (int i = 0; i < 30; i++)
|
||||
guess = (guess + x / guess) * 0.5;
|
||||
return guess;
|
||||
}
|
||||
|
||||
static inline double stb_pow(double base, double exp) {
|
||||
if (exp == 0.0) return 1.0;
|
||||
if (exp == 1.0) return base;
|
||||
if (base == 0.0) return 0.0;
|
||||
// Integer exponent fast path
|
||||
if (exp == (double)(long long)exp) {
|
||||
long long e = (long long)exp;
|
||||
int neg = 0;
|
||||
if (e < 0) { neg = 1; e = -e; }
|
||||
double r = 1.0;
|
||||
double b = base;
|
||||
while (e > 0) {
|
||||
if (e & 1) r *= b;
|
||||
b *= b;
|
||||
e >>= 1;
|
||||
}
|
||||
return neg ? 1.0 / r : r;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
static inline double stb_cos(double x) {
|
||||
// Reduce to [0, 2*pi]
|
||||
const double PI = 3.14159265358979323846;
|
||||
const double TWO_PI = 6.28318530717958647692;
|
||||
x = stb_fmod(stb_fabs(x), TWO_PI);
|
||||
// Taylor series: cos(x) = 1 - x^2/2! + x^4/4! - x^6/6! + ...
|
||||
double x2 = x * x;
|
||||
double term = 1.0;
|
||||
double result = 1.0;
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
term *= -x2 / (double)((2 * i - 1) * (2 * i));
|
||||
result += term;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static inline double stb_acos(double x) {
|
||||
// Clamp input
|
||||
if (x <= -1.0) return 3.14159265358979323846;
|
||||
if (x >= 1.0) return 0.0;
|
||||
// Polynomial approximation (Abramowitz & Stegun style)
|
||||
double ax = stb_fabs(x);
|
||||
double result = (-0.0187293 * ax + 0.0742610) * ax - 0.2121144;
|
||||
result = (result * ax + 1.5707288) * stb_sqrt(1.0 - ax);
|
||||
if (x < 0.0)
|
||||
return 3.14159265358979323846 - result;
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // STB_MATH_H
|
||||
File diff suppressed because it is too large
Load Diff
@@ -423,6 +423,11 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||
}
|
||||
|
||||
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
|
||||
int cell_w = mono_cell_width();
|
||||
int cell_h = mono_cell_height();
|
||||
bool use_ttf = fonts::mono && fonts::mono->valid;
|
||||
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
|
||||
|
||||
// Fill background
|
||||
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
||||
int total = pw * ph;
|
||||
@@ -431,8 +436,8 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
}
|
||||
|
||||
// Render each visible cell
|
||||
int visible_rows = ph / FONT_HEIGHT;
|
||||
int visible_cols = pw / FONT_WIDTH;
|
||||
int visible_rows = ph / cell_h;
|
||||
int visible_cols = pw / cell_w;
|
||||
if (visible_rows > t->rows) visible_rows = t->rows;
|
||||
if (visible_cols > t->cols) visible_cols = t->cols;
|
||||
|
||||
@@ -441,17 +446,16 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
int idx = r * t->cols + c;
|
||||
TermCell& cell = t->cells[idx];
|
||||
|
||||
int px = c * FONT_WIDTH;
|
||||
int py = r * FONT_HEIGHT;
|
||||
int px = c * cell_w;
|
||||
int py = r * cell_h;
|
||||
|
||||
uint32_t cell_bg = cell.bg.to_pixel();
|
||||
uint32_t cell_fg = cell.fg.to_pixel();
|
||||
|
||||
// Draw cell background
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
for (int fy = 0; fy < cell_h; fy++) {
|
||||
int dy = py + fy;
|
||||
if (dy >= ph) break;
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
for (int fx = 0; fx < cell_w; fx++) {
|
||||
int dx = px + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = cell_bg;
|
||||
@@ -460,16 +464,23 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
|
||||
// Draw character glyph
|
||||
if (cell.ch > 32 || cell.ch < 0) {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
int dy = py + fy;
|
||||
if (dy >= ph) break;
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = px + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = cell_fg;
|
||||
if (use_ttf) {
|
||||
int baseline = py + gc->ascent;
|
||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
|
||||
} else {
|
||||
uint32_t cell_fg = cell.fg.to_pixel();
|
||||
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
int dy = py + fy;
|
||||
if (dy >= ph) break;
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = px + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = cell_fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,13 +490,13 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
|
||||
// Draw cursor
|
||||
if (t->cursor_visible && t->cursor_x < visible_cols && t->cursor_y < visible_rows) {
|
||||
int cx = t->cursor_x * FONT_WIDTH;
|
||||
int cy = t->cursor_y * FONT_HEIGHT;
|
||||
int cx = t->cursor_x * cell_w;
|
||||
int cy = t->cursor_y * cell_h;
|
||||
uint32_t cursor_px = colors::WHITE.to_pixel();
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
for (int fy = 0; fy < cell_h; fy++) {
|
||||
int dy = cy + fy;
|
||||
if (dy >= ph) break;
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
for (int fx = 0; fx < cell_w; fx++) {
|
||||
int dx = cx + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = cursor_px;
|
||||
@@ -496,17 +507,23 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
int idx = t->cursor_y * t->cols + t->cursor_x;
|
||||
char ch = t->cells[idx].ch;
|
||||
if (ch > 32 || ch < 0) {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
||||
uint32_t black_px = colors::BLACK.to_pixel();
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
int dy = cy + fy;
|
||||
if (dy >= ph) break;
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = cx + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = black_px;
|
||||
if (use_ttf) {
|
||||
int baseline = cy + gc->ascent;
|
||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||
cx, baseline, (unsigned char)ch, colors::BLACK, gc);
|
||||
} else {
|
||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
||||
uint32_t black_px = colors::BLACK.to_pixel();
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
int dy = cy + fy;
|
||||
if (dy >= ph) break;
|
||||
uint8_t bits = glyph[fy];
|
||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
||||
if (bits & (0x80 >> fx)) {
|
||||
int dx = cx + fx;
|
||||
if (dx >= pw) break;
|
||||
pixels[dy * pw + dx] = black_px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,6 +532,58 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
}
|
||||
}
|
||||
|
||||
static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) {
|
||||
if (new_cols == t->cols && new_rows == t->rows) return;
|
||||
if (new_cols < 1 || new_rows < 1) return;
|
||||
|
||||
int new_total = new_cols * new_rows;
|
||||
TermCell* new_cells = (TermCell*)zenith::alloc(new_total * sizeof(TermCell));
|
||||
TermCell* new_alt = (TermCell*)zenith::alloc(new_total * sizeof(TermCell));
|
||||
|
||||
// Clear new buffers
|
||||
for (int i = 0; i < new_total; i++) {
|
||||
new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
}
|
||||
|
||||
// Copy existing content (as much as fits)
|
||||
int copy_rows = t->rows < new_rows ? t->rows : new_rows;
|
||||
int copy_cols = t->cols < new_cols ? t->cols : new_cols;
|
||||
|
||||
// If cursor is beyond the new grid, scroll content up to keep cursor visible
|
||||
int row_offset = 0;
|
||||
if (t->cursor_y >= new_rows) {
|
||||
row_offset = t->cursor_y - new_rows + 1;
|
||||
}
|
||||
|
||||
for (int r = 0; r < copy_rows && (r + row_offset) < t->rows; r++) {
|
||||
for (int c = 0; c < copy_cols; c++) {
|
||||
new_cells[r * new_cols + c] = t->cells[(r + row_offset) * t->cols + c];
|
||||
}
|
||||
}
|
||||
|
||||
if (t->cells) zenith::mfree(t->cells);
|
||||
if (t->alt_cells) zenith::mfree(t->alt_cells);
|
||||
|
||||
t->cells = new_cells;
|
||||
t->alt_cells = new_alt;
|
||||
|
||||
int old_cols = t->cols;
|
||||
t->cols = new_cols;
|
||||
t->rows = new_rows;
|
||||
|
||||
// Adjust cursor position
|
||||
t->cursor_y -= row_offset;
|
||||
if (t->cursor_x >= new_cols) t->cursor_x = new_cols - 1;
|
||||
if (t->cursor_y >= new_rows) t->cursor_y = new_rows - 1;
|
||||
if (t->cursor_y < 0) t->cursor_y = 0;
|
||||
|
||||
// Notify child process of new terminal size
|
||||
if (t->child_pid > 0) {
|
||||
zenith::childio_settermsz(t->child_pid, new_cols, new_rows);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void terminal_handle_key(TerminalState* t, const Zenith::KeyEvent& key) {
|
||||
if (t->child_pid > 0) {
|
||||
zenith::childio_writekey(t->child_pid, &key);
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* truetype.hpp
|
||||
* ZenithOS TrueType font rendering via stb_truetype
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <zenith/syscall.h>
|
||||
#include <zenith/heap.h>
|
||||
#include <zenith/string.h>
|
||||
#include "gui/gui.hpp"
|
||||
#include "gui/framebuffer.hpp"
|
||||
|
||||
// Forward-declare stbtt_fontinfo to avoid including stb_truetype.h in every TU.
|
||||
// The actual struct is defined in stb_truetype.h and only used in truetype.hpp
|
||||
// method bodies (which are inline but only instantiated where stb_truetype.h
|
||||
// is also included — i.e. stb_truetype_impl.cpp). We include the full header
|
||||
// here since our inline methods need the complete type.
|
||||
#include "gui/stb_math.h"
|
||||
|
||||
// We need the stb macros defined before including stb_truetype.h (header-only mode)
|
||||
#ifndef STBTT_ifloor
|
||||
#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))
|
||||
#endif
|
||||
|
||||
#include "gui/stb_truetype.h"
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct CachedGlyph {
|
||||
uint8_t* bitmap;
|
||||
int width, height;
|
||||
int xoff, yoff;
|
||||
int advance;
|
||||
bool loaded;
|
||||
};
|
||||
|
||||
struct GlyphCache {
|
||||
CachedGlyph glyphs[128];
|
||||
int pixel_size;
|
||||
float scale;
|
||||
int ascent, descent, line_gap;
|
||||
int line_height;
|
||||
};
|
||||
|
||||
struct TrueTypeFont {
|
||||
stbtt_fontinfo info;
|
||||
uint8_t* data;
|
||||
GlyphCache caches[4];
|
||||
int cache_count;
|
||||
bool valid;
|
||||
|
||||
bool init(const char* vfs_path) {
|
||||
valid = false;
|
||||
data = nullptr;
|
||||
cache_count = 0;
|
||||
|
||||
int fd = zenith::open(vfs_path);
|
||||
if (fd < 0) return false;
|
||||
|
||||
uint64_t size = zenith::getsize(fd);
|
||||
if (size == 0 || size > 1024 * 1024) {
|
||||
zenith::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
data = (uint8_t*)zenith::alloc(size);
|
||||
if (!data) {
|
||||
zenith::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
zenith::read(fd, data, 0, size);
|
||||
zenith::close(fd);
|
||||
|
||||
if (!stbtt_InitFont(&info, data, stbtt_GetFontOffsetForIndex(data, 0))) {
|
||||
zenith::free(data);
|
||||
data = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
GlyphCache* get_cache(int pixel_size) {
|
||||
// Search existing caches
|
||||
for (int i = 0; i < cache_count; i++) {
|
||||
if (caches[i].pixel_size == pixel_size)
|
||||
return &caches[i];
|
||||
}
|
||||
|
||||
// Create new cache
|
||||
if (cache_count >= 4) return &caches[0]; // fallback to first
|
||||
|
||||
GlyphCache* gc = &caches[cache_count++];
|
||||
gc->pixel_size = pixel_size;
|
||||
gc->scale = stbtt_ScaleForPixelHeight(&info, (float)pixel_size);
|
||||
|
||||
int asc, desc, lg;
|
||||
stbtt_GetFontVMetrics(&info, &asc, &desc, &lg);
|
||||
gc->ascent = (int)(asc * gc->scale);
|
||||
gc->descent = (int)(desc * gc->scale);
|
||||
gc->line_gap = (int)(lg * gc->scale);
|
||||
gc->line_height = gc->ascent - gc->descent + gc->line_gap;
|
||||
|
||||
for (int i = 0; i < 128; i++) {
|
||||
gc->glyphs[i].bitmap = nullptr;
|
||||
gc->glyphs[i].loaded = false;
|
||||
}
|
||||
|
||||
return gc;
|
||||
}
|
||||
|
||||
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||
if (codepoint < 0 || codepoint >= 128) return nullptr;
|
||||
CachedGlyph* g = &gc->glyphs[codepoint];
|
||||
if (g->loaded) return g;
|
||||
|
||||
g->loaded = true;
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&info, codepoint, &advance, &lsb);
|
||||
g->advance = (int)(advance * gc->scale);
|
||||
|
||||
int x0, y0, x1, y1;
|
||||
stbtt_GetCodepointBitmapBox(&info, codepoint, gc->scale, gc->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);
|
||||
stbtt_MakeCodepointBitmap(&info, g->bitmap, g->width, g->height,
|
||||
g->width, gc->scale, gc->scale, codepoint);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
int measure_text(const char* text, int pixel_size) {
|
||||
if (!valid) return 0;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int w = 0;
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
if (g) w += g->advance;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
int get_line_height(int pixel_size) {
|
||||
if (!valid) return 16;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
return gc->line_height;
|
||||
}
|
||||
|
||||
void draw(Framebuffer& fb, int x, int y, const char* text,
|
||||
Color color, int pixel_size) {
|
||||
if (!valid) return;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int cx = x;
|
||||
int baseline = y + gc->ascent;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
if (!g) continue;
|
||||
|
||||
if (g->bitmap) {
|
||||
int gx = cx + g->xoff;
|
||||
int gy = baseline + g->yoff;
|
||||
for (int row = 0; row < g->height; row++) {
|
||||
for (int col = 0; col < g->width; col++) {
|
||||
uint8_t alpha = g->bitmap[row * g->width + col];
|
||||
if (alpha > 0) {
|
||||
Color c = {color.r, color.g, color.b, alpha};
|
||||
fb.put_pixel_alpha(gx + col, gy + row, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cx += g->advance;
|
||||
}
|
||||
}
|
||||
|
||||
void draw_bg(Framebuffer& fb, int x, int y, const char* text,
|
||||
Color fg, Color bg, int pixel_size) {
|
||||
if (!valid) return;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
|
||||
// Fill background for the text extent
|
||||
int tw = measure_text(text, pixel_size);
|
||||
fb.fill_rect(x, y, tw, gc->line_height, bg);
|
||||
|
||||
// Then draw foreground
|
||||
draw(fb, x, y, text, fg, pixel_size);
|
||||
}
|
||||
|
||||
void draw_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
||||
int x, int y, const char* text,
|
||||
Color color, int pixel_size) {
|
||||
if (!valid) return;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int cx = x;
|
||||
int baseline = y + gc->ascent;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
if (!g) continue;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw single character to buffer, returning advance width
|
||||
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
||||
int x, int baseline, int codepoint,
|
||||
Color color, GlyphCache* gc) {
|
||||
CachedGlyph* g = get_glyph(gc, codepoint);
|
||||
if (!g) return 0;
|
||||
|
||||
if (g->bitmap) {
|
||||
int gx = x + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return g->advance;
|
||||
}
|
||||
};
|
||||
|
||||
// Global font manager
|
||||
namespace fonts {
|
||||
inline TrueTypeFont* system_font = nullptr;
|
||||
inline TrueTypeFont* system_bold = nullptr;
|
||||
inline TrueTypeFont* mono = nullptr;
|
||||
inline TrueTypeFont* mono_bold = nullptr;
|
||||
|
||||
inline int UI_SIZE = 18;
|
||||
inline int TITLE_SIZE = 18;
|
||||
inline int TERM_SIZE = 18;
|
||||
inline int LARGE_SIZE = 28;
|
||||
|
||||
inline bool init() {
|
||||
auto load = [](const char* path) -> TrueTypeFont* {
|
||||
TrueTypeFont* f = (TrueTypeFont*)zenith::malloc(sizeof(TrueTypeFont));
|
||||
zenith::memset(f, 0, sizeof(TrueTypeFont));
|
||||
if (!f->init(path)) {
|
||||
zenith::mfree(f);
|
||||
return nullptr;
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
system_font = load("0:/fonts/Roboto-Medium.ttf");
|
||||
system_bold = load("0:/fonts/Roboto-Bold.ttf");
|
||||
mono = load("0:/fonts/JetBrainsMono-Regular.ttf");
|
||||
mono_bold = load("0:/fonts/JetBrainsMono-Bold.ttf");
|
||||
|
||||
return system_font != nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
@@ -79,7 +79,7 @@ struct Button {
|
||||
// Center text
|
||||
int tw = text_width(text);
|
||||
int tx = bounds.x + (bounds.w - tw) / 2;
|
||||
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
|
||||
int ty = bounds.y + (bounds.h - system_font_height()) / 2;
|
||||
draw_text(fb, tx, ty, text, fg);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ struct IconButton {
|
||||
|
||||
// Draw text
|
||||
if (text) {
|
||||
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
|
||||
int ty = bounds.y + (bounds.h - system_font_height()) / 2;
|
||||
draw_text(fb, content_x, ty, text, text_color);
|
||||
}
|
||||
}
|
||||
@@ -206,13 +206,18 @@ struct TextBox {
|
||||
|
||||
// Draw text with 4px padding
|
||||
int tx = bounds.x + 4;
|
||||
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
|
||||
int fh = system_font_height();
|
||||
int ty = bounds.y + (bounds.h - fh) / 2;
|
||||
draw_text(fb, tx, ty, text, fg);
|
||||
|
||||
// Draw cursor if focused
|
||||
if (focused) {
|
||||
int cx = tx + cursor * FONT_WIDTH;
|
||||
draw_vline(fb, cx, ty, FONT_HEIGHT, cursor_color);
|
||||
// Measure text up to cursor position for proportional fonts
|
||||
char prefix[256];
|
||||
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
|
||||
prefix[cursor < 255 ? cursor : 255] = '\0';
|
||||
int cx = tx + text_width(prefix);
|
||||
draw_vline(fb, cx, ty, fh, cursor_color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ struct Window {
|
||||
WindowPollCallback on_poll;
|
||||
void* app_data;
|
||||
|
||||
bool external; // true = shared-memory window from external process
|
||||
int ext_win_id; // window server ID (valid when external == true)
|
||||
|
||||
Rect titlebar_rect() const {
|
||||
return {frame.x, frame.y, frame.w, TITLEBAR_HEIGHT};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user