feat: TrueType (TTF) font rendering, many new desktop applications and DOOM support, among other improvements

This commit is contained in:
2026-02-20 22:46:41 +01:00
parent a0db5899ef
commit 596be25eaf
124 changed files with 9021 additions and 355 deletions
+6 -2
View File
@@ -82,9 +82,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki desktop icons bearssl libc
.PHONY: all clean doom fetch wiki desktop icons fonts bearssl libc
all: bearssl libc $(TARGETS) fetch wiki doom desktop icons $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
all: bearssl libc $(TARGETS) fetch wiki doom desktop icons fonts $(GAME_DATA) $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library.
bearssl:
@@ -110,6 +110,10 @@ desktop:
icons:
../scripts/copy_icons.sh
# Copy TTF fonts for the desktop into bin/fonts/.
fonts:
../scripts/copy_fonts.sh
# Build doom via its own Makefile.
doom:
$(MAKE) -C src/doom
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,79 @@
JetBrains Mono Variable Font
============================
This download contains JetBrains Mono as both variable fonts and static fonts.
JetBrains Mono is a variable font with this axis:
wght
This means all the styles are contained in these files:
JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf
JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for JetBrains Mono:
JetBrains_Mono/static/JetBrainsMono-Thin.ttf
JetBrains_Mono/static/JetBrainsMono-ExtraLight.ttf
JetBrains_Mono/static/JetBrainsMono-Light.ttf
JetBrains_Mono/static/JetBrainsMono-Regular.ttf
JetBrains_Mono/static/JetBrainsMono-Medium.ttf
JetBrains_Mono/static/JetBrainsMono-SemiBold.ttf
JetBrains_Mono/static/JetBrainsMono-Bold.ttf
JetBrains_Mono/static/JetBrainsMono-ExtraBold.ttf
JetBrains_Mono/static/JetBrainsMono-ThinItalic.ttf
JetBrains_Mono/static/JetBrainsMono-ExtraLightItalic.ttf
JetBrains_Mono/static/JetBrainsMono-LightItalic.ttf
JetBrains_Mono/static/JetBrainsMono-Italic.ttf
JetBrains_Mono/static/JetBrainsMono-MediumItalic.ttf
JetBrains_Mono/static/JetBrainsMono-SemiBoldItalic.ttf
JetBrains_Mono/static/JetBrainsMono-BoldItalic.ttf
JetBrains_Mono/static/JetBrainsMono-ExtraBoldItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+118
View File
@@ -0,0 +1,118 @@
Roboto Variable Font
====================
This download contains Roboto as both variable fonts and static fonts.
Roboto is a variable font with these axes:
wdth
wght
This means all the styles are contained in these files:
Roboto/Roboto-VariableFont_wdth,wght.ttf
Roboto/Roboto-Italic-VariableFont_wdth,wght.ttf
If your app fully supports variable fonts, you can now pick intermediate styles
that arent available as static fonts. Not all apps support variable fonts, and
in those cases you can use the static font files for Roboto:
Roboto/static/Roboto_Condensed-Thin.ttf
Roboto/static/Roboto_Condensed-ExtraLight.ttf
Roboto/static/Roboto_Condensed-Light.ttf
Roboto/static/Roboto_Condensed-Regular.ttf
Roboto/static/Roboto_Condensed-Medium.ttf
Roboto/static/Roboto_Condensed-SemiBold.ttf
Roboto/static/Roboto_Condensed-Bold.ttf
Roboto/static/Roboto_Condensed-ExtraBold.ttf
Roboto/static/Roboto_Condensed-Black.ttf
Roboto/static/Roboto_SemiCondensed-Thin.ttf
Roboto/static/Roboto_SemiCondensed-ExtraLight.ttf
Roboto/static/Roboto_SemiCondensed-Light.ttf
Roboto/static/Roboto_SemiCondensed-Regular.ttf
Roboto/static/Roboto_SemiCondensed-Medium.ttf
Roboto/static/Roboto_SemiCondensed-SemiBold.ttf
Roboto/static/Roboto_SemiCondensed-Bold.ttf
Roboto/static/Roboto_SemiCondensed-ExtraBold.ttf
Roboto/static/Roboto_SemiCondensed-Black.ttf
Roboto/static/Roboto-Thin.ttf
Roboto/static/Roboto-ExtraLight.ttf
Roboto/static/Roboto-Light.ttf
Roboto/static/Roboto-Regular.ttf
Roboto/static/Roboto-Medium.ttf
Roboto/static/Roboto-SemiBold.ttf
Roboto/static/Roboto-Bold.ttf
Roboto/static/Roboto-ExtraBold.ttf
Roboto/static/Roboto-Black.ttf
Roboto/static/Roboto_Condensed-ThinItalic.ttf
Roboto/static/Roboto_Condensed-ExtraLightItalic.ttf
Roboto/static/Roboto_Condensed-LightItalic.ttf
Roboto/static/Roboto_Condensed-Italic.ttf
Roboto/static/Roboto_Condensed-MediumItalic.ttf
Roboto/static/Roboto_Condensed-SemiBoldItalic.ttf
Roboto/static/Roboto_Condensed-BoldItalic.ttf
Roboto/static/Roboto_Condensed-ExtraBoldItalic.ttf
Roboto/static/Roboto_Condensed-BlackItalic.ttf
Roboto/static/Roboto_SemiCondensed-ThinItalic.ttf
Roboto/static/Roboto_SemiCondensed-ExtraLightItalic.ttf
Roboto/static/Roboto_SemiCondensed-LightItalic.ttf
Roboto/static/Roboto_SemiCondensed-Italic.ttf
Roboto/static/Roboto_SemiCondensed-MediumItalic.ttf
Roboto/static/Roboto_SemiCondensed-SemiBoldItalic.ttf
Roboto/static/Roboto_SemiCondensed-BoldItalic.ttf
Roboto/static/Roboto_SemiCondensed-ExtraBoldItalic.ttf
Roboto/static/Roboto_SemiCondensed-BlackItalic.ttf
Roboto/static/Roboto-ThinItalic.ttf
Roboto/static/Roboto-ExtraLightItalic.ttf
Roboto/static/Roboto-LightItalic.ttf
Roboto/static/Roboto-Italic.ttf
Roboto/static/Roboto-MediumItalic.ttf
Roboto/static/Roboto-SemiBoldItalic.ttf
Roboto/static/Roboto-BoldItalic.ttf
Roboto/static/Roboto-ExtraBoldItalic.ttf
Roboto/static/Roboto-BlackItalic.ttf
Get started
-----------
1. Install the font files you want to use
2. Use your app's font picker to view the font family and all the
available styles
Learn more about variable fonts
-------------------------------
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
https://variablefonts.typenetwork.com
https://medium.com/variable-fonts
In desktop apps
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
Online
https://developers.google.com/fonts/docs/getting_started
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
Installing fonts
MacOS: https://support.apple.com/en-us/HT201749
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
Android Apps
https://developers.google.com/fonts/docs/android
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
License
-------
Please read the full license text (OFL.txt) to understand the permissions,
restrictions and requirements for usage, redistribution, and modification.
You can use them in your products & projects print or digital,
commercial or otherwise.
This isn't legal advice, please consider consulting a lawyer and see the full
license for all details.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+56
View File
@@ -66,6 +66,20 @@ namespace Zenith {
static constexpr uint64_t SYS_CHILDIO_WRITEKEY = 52;
static constexpr uint64_t SYS_CHILDIO_SETTERMSZ = 53;
// Window server syscalls
static constexpr uint64_t SYS_WINCREATE = 54;
static constexpr uint64_t SYS_WINDESTROY = 55;
static constexpr uint64_t SYS_WINPRESENT = 56;
static constexpr uint64_t SYS_WINPOLL = 57;
static constexpr uint64_t SYS_WINENUM = 58;
static constexpr uint64_t SYS_WINMAP = 59;
static constexpr uint64_t SYS_WINSENDEVENT = 60;
// Process management syscalls
static constexpr uint64_t SYS_PROCLIST = 61;
static constexpr uint64_t SYS_KILL = 62;
static constexpr uint64_t SYS_DEVLIST = 63;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
@@ -118,4 +132,46 @@ namespace Zenith {
uint8_t buttons;
};
// Window server shared types
struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close
uint8_t _pad[3];
union {
KeyEvent key;
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
struct { int32_t w, h; } resize;
};
};
struct WinInfo {
int32_t id;
int32_t ownerPid;
char title[64];
int32_t width, height;
uint8_t dirty;
uint8_t _pad2[3];
};
struct WinCreateResult {
int32_t id; // -1 on failure
uint32_t _pad;
uint64_t pixelVa; // VA of pixel buffer in caller's address space
};
struct DevInfo {
uint8_t category; // 0=CPU, 1=Interrupt, 2=Timer, 3=Input, 4=USB, 5=Network, 6=Display, 7=PCI
uint8_t _pad[3];
char name[48];
char detail[48];
};
struct ProcInfo {
int32_t pid;
int32_t parentPid;
uint8_t state; // 0=Free, 1=Ready, 2=Running, 3=Terminated
uint8_t _pad[3];
char name[64];
uint64_t heapUsed; // heapNext - UserHeapBase (bytes)
};
}
+20 -2
View File
@@ -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);
}
};
+26
View File
@@ -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
+37 -1
View File
@@ -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;
+4 -4
View File
@@ -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
+95
View File
@@ -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
+101 -32
View File
@@ -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);
+335
View File
@@ -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
+10 -5
View File
@@ -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);
}
}
+3
View File
@@ -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};
}
+34
View File
@@ -284,4 +284,38 @@ namespace zenith {
return (int)syscall3(Zenith::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
}
// Process listing / kill
inline int proclist(Zenith::ProcInfo* buf, int max) {
return (int)syscall2(Zenith::SYS_PROCLIST, (uint64_t)buf, (uint64_t)max);
}
inline int kill(int pid) {
return (int)syscall1(Zenith::SYS_KILL, (uint64_t)pid);
}
inline int devlist(Zenith::DevInfo* buf, int max) {
return (int)syscall2(Zenith::SYS_DEVLIST, (uint64_t)buf, (uint64_t)max);
}
// Window server
inline int win_create(const char* title, int w, int h, Zenith::WinCreateResult* result) {
return (int)syscall4(Zenith::SYS_WINCREATE, (uint64_t)title, (uint64_t)w, (uint64_t)h, (uint64_t)result);
}
inline int win_destroy(int id) {
return (int)syscall1(Zenith::SYS_WINDESTROY, (uint64_t)id);
}
inline int win_present(int id) {
return (int)syscall1(Zenith::SYS_WINPRESENT, (uint64_t)id);
}
inline int win_poll(int id, Zenith::WinEvent* event) {
return (int)syscall2(Zenith::SYS_WINPOLL, (uint64_t)id, (uint64_t)event);
}
inline int win_enumerate(Zenith::WinInfo* info, int max) {
return (int)syscall2(Zenith::SYS_WINENUM, (uint64_t)info, (uint64_t)max);
}
inline uint64_t win_map(int id) {
return (uint64_t)syscall1(Zenith::SYS_WINMAP, (uint64_t)id);
}
inline int win_sendevent(int id, const Zenith::WinEvent* event) {
return (int)syscall2(Zenith::SYS_WINSENDEVENT, (uint64_t)id, (uint64_t)event);
}
}
+1 -15
View File
@@ -1,26 +1,12 @@
/*
* main.cpp
* clear - Clear terminal screen and framebuffer
* clear - Clear terminal screen
* Copyright (c) 2025-2026 Daniel Hammer
*/
#include <zenith/syscall.h>
extern "C" void _start() {
// Clear the raw framebuffer (needed after graphical programs like DOOM)
Zenith::FbInfo fb;
zenith::fb_info(&fb);
uint8_t* pixels = (uint8_t*)zenith::fb_map();
if (pixels) {
for (uint64_t y = 0; y < fb.height; y++) {
uint32_t* row = (uint32_t*)(pixels + y * fb.pitch);
for (uint64_t x = 0; x < fb.width; x++) {
row[x] = 0x00000000;
}
}
}
// Reset the text console
zenith::print("\033[2J"); // Clear entire screen
zenith::print("\033[H"); // Move cursor to top-left
zenith::exit(0);
+6 -7
View File
@@ -20,7 +20,7 @@ LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
# ---- Compiler flags ----
# ---- C++ compiler flags ----
CXXFLAGS := \
-std=gnu++20 \
@@ -40,10 +40,8 @@ CXXFLAGS := \
-fdata-sections \
-m64 \
-march=x86-64 \
-mno-80387 \
-mno-mmx \
-mno-sse \
-mno-sse2 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-I $(PROG_INC) \
@@ -61,9 +59,9 @@ LDFLAGS := \
-z max-page-size=0x1000 \
-T $(LINK_LD)
# ---- Source files ----
# ---- C++ source files ----
SRCS := main.cpp font_data.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_settings.cpp
SRCS := main.cpp font_data.cpp stb_truetype_impl.cpp app_terminal.cpp app_filemanager.cpp app_sysinfo.cpp app_calculator.cpp app_texteditor.cpp app_klog.cpp app_wiki.cpp app_procmgr.cpp app_mandelbrot.cpp app_devexplorer.cpp app_settings.cpp app_doom.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
@@ -78,6 +76,7 @@ $(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@
# C++ sources
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
+13 -5
View File
@@ -231,10 +231,18 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
c.fill_rect(0, 0, c.w, CALC_DISPLAY_H, Color::from_rgb(0x2D, 0x2D, 0x2D));
// Display text (right-aligned, 2x scale)
int text_len = zenith::slen(cs->display_str);
int text_w = text_len * FONT_WIDTH * 2;
int text_w;
int large_h;
if (fonts::system_font && fonts::system_font->valid) {
text_w = fonts::system_font->measure_text(cs->display_str, fonts::LARGE_SIZE);
large_h = fonts::system_font->get_line_height(fonts::LARGE_SIZE);
} else {
int text_len = zenith::slen(cs->display_str);
text_w = text_len * FONT_WIDTH * 2;
large_h = FONT_HEIGHT * 2;
}
int tx = c.w - text_w - 12;
int ty = (CALC_DISPLAY_H - FONT_HEIGHT * 2) / 2;
int ty = (CALC_DISPLAY_H - large_h) / 2;
if (tx < 4) tx = 4;
c.text_2x(tx, ty, cs->display_str, colors::WHITE);
@@ -271,9 +279,9 @@ static void calculator_on_draw(Window* win, Framebuffer& fb) {
// Button text
const char* label = calc_labels[row][col];
Color label_color = (col == 3) ? colors::WHITE : colors::TEXT_COLOR;
int label_w = zenith::slen(label) * FONT_WIDTH;
int label_w = text_width(label);
int lx = bx + (bw - label_w) / 2;
int ly = by + (CALC_BTN_H - FONT_HEIGHT) / 2;
int ly = by + (CALC_BTN_H - system_font_height()) / 2;
c.text(lx, ly, label, label_color);
}
}
+475
View File
@@ -0,0 +1,475 @@
/*
* app_devexplorer.cpp
* ZenithOS Desktop - Device Explorer (lists hardware detected by the kernel)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Device Explorer state
// ============================================================================
static constexpr int DE_TOOLBAR_H = 36;
static constexpr int DE_CAT_H = 28;
static constexpr int DE_ITEM_H = 24;
static constexpr int DE_MAX_DEVS = 64;
static constexpr int DE_POLL_MS = 2000;
static constexpr int DE_INDENT = 28;
// Category names matching DevInfo.category values
static const char* category_names[] = {
"CPU", // 0
"Interrupt", // 1
"Timer", // 2
"Input", // 3
"USB", // 4
"Network", // 5
"Display", // 6
"PCI", // 7
};
static constexpr int NUM_CATEGORIES = 8;
static Color category_colors[] = {
Color::from_rgb(0x33, 0x66, 0xCC), // CPU - blue
Color::from_rgb(0x88, 0x44, 0xAA), // Interrupt - purple
Color::from_rgb(0x22, 0x88, 0x22), // Timer - green
Color::from_rgb(0xCC, 0x88, 0x00), // Input - amber
Color::from_rgb(0x00, 0x88, 0x88), // USB - teal
Color::from_rgb(0xCC, 0x55, 0x22), // Network - orange
Color::from_rgb(0x44, 0x66, 0xCC), // Display - indigo
Color::from_rgb(0x66, 0x66, 0x66), // PCI - gray
};
struct DevExplorerState {
DesktopState* desktop;
Zenith::DevInfo devs[DE_MAX_DEVS];
int dev_count;
bool collapsed[NUM_CATEGORIES]; // per-category collapse state
int selected_row; // index into visible display rows (-1 = none)
int scroll_y; // scroll offset in display rows
uint64_t last_poll_ms;
};
// ============================================================================
// Display row model
// ============================================================================
enum RowType { ROW_CATEGORY, ROW_DEVICE };
struct DisplayRow {
RowType type;
int category; // category index (0..7)
int dev_index; // index into devs[] (only valid for ROW_DEVICE)
};
static constexpr int MAX_DISPLAY_ROWS = DE_MAX_DEVS + NUM_CATEGORIES;
static int build_display_rows(DevExplorerState* de, DisplayRow* rows) {
int count = 0;
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
// Count devices in this category
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
if (cat_count == 0) continue;
// Emit category header
rows[count].type = ROW_CATEGORY;
rows[count].category = cat;
rows[count].dev_index = -1;
count++;
// Emit device rows if expanded
if (!de->collapsed[cat]) {
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) {
rows[count].type = ROW_DEVICE;
rows[count].category = cat;
rows[count].dev_index = d;
count++;
}
}
}
}
return count;
}
// ============================================================================
// Triangle drawing helpers
// ============================================================================
static void draw_triangle_right(Canvas& c, int x, int y, int size, Color col) {
// Right-pointing filled triangle (▶)
int half = size / 2;
for (int row = 0; row < size; row++) {
int dist = (row <= half) ? row : (size - 1 - row);
for (int col_px = 0; col_px <= dist; col_px++) {
c.put_pixel(x + col_px, y + row, col);
}
}
}
static void draw_triangle_down(Canvas& c, int x, int y, int size, Color col) {
// Down-pointing filled triangle (▼)
int half = size / 2;
for (int row = 0; row < half + 1; row++) {
int w = size - row * 2;
for (int col_px = 0; col_px < w; col_px++) {
c.put_pixel(x + row + col_px, y + row, col);
}
}
}
// ============================================================================
// Callbacks
// ============================================================================
static void devexplorer_on_poll(Window* win) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
uint64_t now = zenith::get_milliseconds();
if (now - de->last_poll_ms < DE_POLL_MS) return;
de->last_poll_ms = now;
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
}
static void devexplorer_on_draw(Window* win, Framebuffer& fb) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, DE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, DE_TOOLBAR_H - 1, c.w, colors::BORDER);
// "Refresh" button
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
c.button(btn_x, btn_y, btn_w, btn_h, "Refresh",
Color::from_rgb(0x33, 0x66, 0xCC), colors::WHITE, 4);
// Device count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d devices", de->dev_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (DE_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Build display rows ---
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// --- Compute visible area ---
int list_y = DE_TOOLBAR_H;
int list_h = c.h - list_y;
if (list_h < 1) return;
// Clamp scroll
// Calculate total content height
int total_h = 0;
for (int i = 0; i < row_count; i++) {
total_h += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
int max_scroll_px = total_h - list_h;
if (max_scroll_px < 0) max_scroll_px = 0;
// Convert scroll_y (in rows) to pixel offset for simplicity,
// but keep the row-based model: scroll_y = row offset
// We'll compute cumulative pixel offsets
int scroll_px = 0;
for (int i = 0; i < de->scroll_y && i < row_count; i++) {
scroll_px += (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
if (scroll_px > max_scroll_px) {
scroll_px = max_scroll_px;
// Recalculate scroll_y to match
de->scroll_y = 0;
int acc = 0;
for (int i = 0; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (acc + rh > max_scroll_px) break;
acc += rh;
de->scroll_y = i + 1;
}
}
if (de->scroll_y < 0) de->scroll_y = 0;
// Clamp selected_row
if (de->selected_row >= row_count) de->selected_row = row_count - 1;
// --- Draw rows ---
int draw_y = list_y - scroll_px;
// Advance to first visible area by recomputing from scroll offset
draw_y = list_y;
int first_visible = de->scroll_y;
// Compute starting y by accumulating heights of skipped rows
// Actually, let's just iterate all rows and skip those above the viewport
draw_y = list_y;
for (int i = 0; i < first_visible && i < row_count; i++) {
draw_y -= (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
}
// Draw from first_visible onward
int cur_y = list_y;
for (int i = first_visible; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_y >= c.h) break; // Past bottom of window
if (rows[i].type == ROW_CATEGORY) {
// Category header
int cat = rows[i].category;
c.fill_rect(0, cur_y, c.w, DE_CAT_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
// Highlight if selected
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_CAT_H, colors::MENU_HOVER);
}
// Expand/collapse triangle
int tri_x = 10;
int tri_y = cur_y + (DE_CAT_H - 8) / 2;
Color tri_color = Color::from_rgb(0x55, 0x55, 0x55);
if (de->collapsed[cat]) {
draw_triangle_right(c, tri_x, tri_y, 8, tri_color);
} else {
draw_triangle_down(c, tri_x, tri_y, 8, tri_color);
}
// Colored dot
int dot_x = 24;
int dot_y = cur_y + (DE_CAT_H - 8) / 2;
Color cat_col = (cat >= 0 && cat < NUM_CATEGORIES)
? category_colors[cat] : colors::TEXT_COLOR;
c.fill_rounded_rect(dot_x, dot_y, 8, 8, 4, cat_col);
// Category name
const char* cat_name = (cat >= 0 && cat < NUM_CATEGORIES)
? category_names[cat] : "?";
int text_y = cur_y + (DE_CAT_H - fh) / 2;
c.text(36, text_y, cat_name, Color::from_rgb(0x33, 0x33, 0x33));
// Device count in parentheses on right
int cat_count = 0;
for (int d = 0; d < de->dev_count; d++) {
if (de->devs[d].category == cat) cat_count++;
}
char cnt_buf[16];
snprintf(cnt_buf, sizeof(cnt_buf), "(%d)", cat_count);
int name_w = text_width(cat_name);
c.text(36 + name_w + 8, text_y, cnt_buf,
Color::from_rgb(0x88, 0x88, 0x88));
// Bottom border
c.hline(0, cur_y + DE_CAT_H - 1, c.w, Color::from_rgb(0xE0, 0xE0, 0xE0));
} else {
// Device item row
int di = rows[i].dev_index;
// Selected row highlight
if (i == de->selected_row) {
c.fill_rect(0, cur_y, c.w, DE_ITEM_H, colors::MENU_HOVER);
}
int text_y = cur_y + (DE_ITEM_H - fh) / 2;
// Device name (indented)
c.text(DE_INDENT + 10, text_y, de->devs[di].name, colors::TEXT_COLOR);
// Detail string (right column, gray)
int detail_x = c.w / 2 + 20;
c.text(detail_x, text_y, de->devs[di].detail,
Color::from_rgb(0x66, 0x66, 0x66));
}
cur_y += row_h;
}
// --- Scrollbar ---
if (total_h > list_h) {
int sb_x = c.w - 6;
int thumb_h = (list_h * list_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int thumb_y = list_y + (scroll_px * (list_h - thumb_h)) / max_scroll_px;
c.fill_rect(sb_x, list_y, 4, list_h, Color::from_rgb(0xE0, 0xE0, 0xE0));
c.fill_rect(sb_x, thumb_y, 4, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA));
}
}
static void devexplorer_on_mouse(Window* win, MouseEvent& ev) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Scroll
if (ev.scroll != 0) {
de->scroll_y -= ev.scroll;
if (de->scroll_y < 0) de->scroll_y = 0;
return;
}
if (ev.left_pressed()) {
// Check "Refresh" button click
int btn_w = 80;
int btn_h = 26;
int btn_x = 8;
int btn_y = (DE_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
de->last_poll_ms = 0; // force refresh
return;
}
// Check row click in list area
int list_y = DE_TOOLBAR_H;
if (ly >= list_y) {
// Build display rows to map click to row
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
// Walk rows from scroll offset, accumulating y
int cur_y = list_y;
for (int i = de->scroll_y; i < row_count; i++) {
int row_h = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (ly >= cur_y && ly < cur_y + row_h) {
// Hit this row
if (rows[i].type == ROW_CATEGORY) {
// Toggle collapse
int cat = rows[i].category;
de->collapsed[cat] = !de->collapsed[cat];
de->selected_row = -1;
} else {
de->selected_row = i;
}
return;
}
cur_y += row_h;
if (cur_y >= win->content_h) break;
}
// Clicked below all rows
de->selected_row = -1;
}
}
}
static void devexplorer_on_key(Window* win, const Zenith::KeyEvent& key) {
DevExplorerState* de = (DevExplorerState*)win->app_data;
if (!de || !key.pressed) return;
DisplayRow rows[MAX_DISPLAY_ROWS];
int row_count = build_display_rows(de, rows);
if (row_count == 0) return;
if (key.scancode == 0x48) { // Up arrow
if (de->selected_row <= 0) {
de->selected_row = 0;
} else {
de->selected_row--;
}
// Scroll to keep selection visible
if (de->selected_row < de->scroll_y) {
de->scroll_y = de->selected_row;
}
} else if (key.scancode == 0x50) { // Down arrow
if (de->selected_row < row_count - 1) {
de->selected_row++;
}
// Scroll to keep selection visible — estimate visible rows
int list_h = win->content_h - DE_TOOLBAR_H;
int cur_h = 0;
int last_visible = de->scroll_y;
for (int i = de->scroll_y; i < row_count; i++) {
int rh = (rows[i].type == ROW_CATEGORY) ? DE_CAT_H : DE_ITEM_H;
if (cur_h + rh > list_h) break;
cur_h += rh;
last_visible = i;
}
if (de->selected_row > last_visible) {
de->scroll_y += (de->selected_row - last_visible);
}
} else if (key.scancode == 0x4B) { // Left arrow — collapse
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
if (!de->collapsed[cat]) {
de->collapsed[cat] = true;
// If we were on a device row, move selection to the category header
if (rows[de->selected_row].type == ROW_DEVICE) {
// Find the category header row for this category
for (int i = de->selected_row - 1; i >= 0; i--) {
if (rows[i].type == ROW_CATEGORY && rows[i].category == cat) {
de->selected_row = i;
break;
}
}
// After collapsing, rebuild and clamp
int new_count = build_display_rows(de, rows);
if (de->selected_row >= new_count)
de->selected_row = new_count - 1;
}
}
}
} else if (key.scancode == 0x4D) { // Right arrow — expand
if (de->selected_row >= 0 && de->selected_row < row_count) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = false;
}
} else if (key.scancode == 0x1C) { // Enter — toggle on category row
if (de->selected_row >= 0 && de->selected_row < row_count) {
if (rows[de->selected_row].type == ROW_CATEGORY) {
int cat = rows[de->selected_row].category;
de->collapsed[cat] = !de->collapsed[cat];
}
}
}
}
static void devexplorer_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Device Explorer launcher
// ============================================================================
void open_devexplorer(DesktopState* ds) {
int idx = desktop_create_window(ds, "Devices", 140, 70, 640, 460);
if (idx < 0) return;
Window* win = &ds->windows[idx];
DevExplorerState* de = (DevExplorerState*)zenith::malloc(sizeof(DevExplorerState));
zenith::memset(de, 0, sizeof(DevExplorerState));
de->desktop = ds;
de->selected_row = -1;
de->scroll_y = 0;
de->last_poll_ms = 0;
// All categories start expanded
for (int i = 0; i < NUM_CATEGORIES; i++)
de->collapsed[i] = false;
// Initial poll
de->dev_count = zenith::devlist(de->devs, DE_MAX_DEVS);
win->app_data = de;
win->on_draw = devexplorer_on_draw;
win->on_mouse = devexplorer_on_mouse;
win->on_key = devexplorer_on_key;
win->on_close = devexplorer_on_close;
win->on_poll = devexplorer_on_poll;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* app_doom.cpp
* ZenithOS Desktop - DOOM launcher (spawns standalone doom.elf)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
void open_doom(DesktopState* ds) {
(void)ds;
zenith::spawn("0:/games/doom.elf");
}
+6 -5
View File
@@ -373,7 +373,7 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
int tx = cell_x + (FM_GRID_CELL_W - tw) / 2;
if (tx < cell_x) tx = cell_x;
int ty = icon_y + FM_GRID_ICON + 2;
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
if (ty >= list_y && ty + system_font_height() <= c.h)
c.text(tx, ty, label, colors::TEXT_COLOR);
}
} else {
@@ -447,19 +447,20 @@ static void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Name
int tx = 30;
int ty = iy + (FM_ITEM_H - FONT_HEIGHT) / 2;
if (ty >= list_y && ty + FONT_HEIGHT <= c.h)
int fm_sfh = system_font_height();
int ty = iy + (FM_ITEM_H - fm_sfh) / 2;
if (ty >= list_y && ty + fm_sfh <= c.h)
c.text(tx, ty, fm->entry_names[i], colors::TEXT_COLOR);
// Size
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + FONT_HEIGHT <= c.h) {
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
char size_str[16];
format_size(size_str, fm->entry_sizes[i]);
c.text(size_col_x, ty, size_str, dim);
}
// Type
if (type_col_x > 160 && ty >= list_y && ty + FONT_HEIGHT <= c.h) {
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
const char* type_str = "File";
if (fm->entry_types[i] == 1) type_str = "Dir";
else if (fm->entry_types[i] == 2) type_str = "Exec";
+2 -2
View File
@@ -119,8 +119,8 @@ void open_klog(DesktopState* ds) {
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
int cols = cr.w / FONT_WIDTH;
int rows = cr.h / FONT_HEIGHT;
int cols = cr.w / mono_cell_width();
int rows = cr.h / mono_cell_height();
KlogState* klog = (KlogState*)zenith::malloc(sizeof(KlogState));
zenith::memset(klog, 0, sizeof(KlogState));
+296
View File
@@ -0,0 +1,296 @@
/*
* app_mandelbrot.cpp
* ZenithOS Desktop - Mandelbrot set visualizer
* Supports zoom (scroll wheel), pan (drag), and reset (R key)
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Fixed-point Mandelbrot (avoids FPU/SSE dependency issues)
// Uses 28.36 fixed-point: 36 fractional bits gives ~1e-10 precision
// ============================================================================
using fp_t = int64_t;
static constexpr int FP_SHIFT = 36;
static constexpr fp_t FP_ONE = (fp_t)1 << FP_SHIFT;
static inline fp_t fp_from_int(int v) { return (fp_t)v << FP_SHIFT; }
static inline fp_t fp_mul(fp_t a, fp_t b) {
// Split to avoid overflow: a * b >> SHIFT
// Use 128-bit intermediate via compiler builtin
__int128 r = (__int128)a * b;
return (fp_t)(r >> FP_SHIFT);
}
// fp_div for small numerators (avoids __divti3 by using 64-bit division)
// Only valid when a is small enough that a << FP_SHIFT fits in int64_t
static inline fp_t fp_div_small(int a, int b) {
return ((fp_t)a << FP_SHIFT) / (fp_t)b;
}
// ============================================================================
// State
// ============================================================================
static constexpr int MB_MAX_ITER = 256;
static constexpr int MB_TOOLBAR_H = 32;
struct MandelbrotState {
DesktopState* desktop;
fp_t center_x, center_y; // center of view in fractal coords
fp_t scale; // units per pixel (fixed-point)
int max_iter;
bool needs_render;
// Drag state
bool dragging;
int drag_start_x, drag_start_y;
fp_t drag_start_cx, drag_start_cy;
};
// ============================================================================
// Color palette
// ============================================================================
static uint32_t mandelbrot_color(int iter, int max_iter) {
if (iter >= max_iter) return 0xFF000000; // black for inside set
// Smooth cycling palette using bit manipulation
int t = (iter * 7) & 0xFF;
int phase = (iter * 7) >> 8;
uint8_t r, g, b;
switch (phase % 6) {
case 0: r = 255; g = (uint8_t)t; b = 0; break;
case 1: r = (uint8_t)(255 - t); g = 255; b = 0; break;
case 2: r = 0; g = 255; b = (uint8_t)t; break;
case 3: r = 0; g = (uint8_t)(255 - t); b = 255; break;
case 4: r = (uint8_t)t; g = 0; b = 255; break;
default: r = 255; g = 0; b = (uint8_t)(255 - t); break;
}
return 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
// ============================================================================
// Render
// ============================================================================
static void mandelbrot_render(MandelbrotState* mb, uint32_t* pixels, int w, int h) {
int render_h = h - MB_TOOLBAR_H;
if (render_h <= 0) return;
fp_t half_w = fp_mul(fp_from_int(w / 2), mb->scale);
fp_t half_h = fp_mul(fp_from_int(render_h / 2), mb->scale);
fp_t x_min = mb->center_x - half_w;
fp_t y_min = mb->center_y - half_h;
fp_t bailout = fp_from_int(4);
for (int py = 0; py < render_h; py++) {
fp_t ci = y_min + fp_mul(fp_from_int(py), mb->scale);
uint32_t* row = pixels + (py + MB_TOOLBAR_H) * w;
for (int px = 0; px < w; px++) {
fp_t cr = x_min + fp_mul(fp_from_int(px), mb->scale);
fp_t zr = 0, zi = 0;
int iter = 0;
while (iter < mb->max_iter) {
fp_t zr2 = fp_mul(zr, zr);
fp_t zi2 = fp_mul(zi, zi);
if (zr2 + zi2 > bailout) break;
fp_t new_zr = zr2 - zi2 + cr;
zi = fp_mul(fp_from_int(2), fp_mul(zr, zi)) + ci;
zr = new_zr;
iter++;
}
row[px] = mandelbrot_color(iter, mb->max_iter);
}
}
mb->needs_render = false;
}
// ============================================================================
// Callbacks
// ============================================================================
static void mb_on_draw(Window* win, Framebuffer& fb) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Canvas c(win);
if (mb->needs_render) {
mandelbrot_render(mb, win->content, c.w, c.h);
}
// Draw toolbar over the render
c.fill_rect(0, 0, c.w, MB_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, MB_TOOLBAR_H - 1, c.w, colors::BORDER);
// Reset button
c.button(8, 4, 60, 24, "Reset", colors::ACCENT, colors::WHITE, 4);
// Iter +/- buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int fh = system_font_height();
c.text(80, (MB_TOOLBAR_H - fh) / 2, iter_str, colors::TEXT_COLOR);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
c.button(btn_x, 4, 24, 24, "-", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
c.button(btn_x + 28, 4, 24, 24, "+", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
// Hint
const char* hint = "Scroll=zoom Drag=pan";
int hw = text_width(hint);
c.text(c.w - hw - 8, (MB_TOOLBAR_H - fh) / 2, hint, Color::from_rgb(0x99, 0x99, 0x99));
}
static void mb_on_mouse(Window* win, MouseEvent& ev) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
// Handle toolbar clicks
if (ev.left_pressed() && ly < MB_TOOLBAR_H) {
// Reset button
Rect reset_r = {8, 4, 60, 24};
if (reset_r.contains(lx, ly)) {
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
return;
}
// Iter buttons
char iter_str[24];
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
int iter_text_w = text_width(iter_str);
int btn_x = 80 + iter_text_w + 8;
Rect minus_r = {btn_x, 4, 24, 24};
Rect plus_r = {btn_x + 28, 4, 24, 24};
if (minus_r.contains(lx, ly)) {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
return;
}
if (plus_r.contains(lx, ly)) {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
return;
}
return;
}
// Drag panning
if (ev.left_pressed() && ly >= MB_TOOLBAR_H) {
mb->dragging = true;
mb->drag_start_x = lx;
mb->drag_start_y = ly;
mb->drag_start_cx = mb->center_x;
mb->drag_start_cy = mb->center_y;
return;
}
if (mb->dragging && ev.left_held()) {
int dx = lx - mb->drag_start_x;
int dy = ly - mb->drag_start_y;
mb->center_x = mb->drag_start_cx - fp_mul(fp_from_int(dx), mb->scale);
mb->center_y = mb->drag_start_cy - fp_mul(fp_from_int(dy), mb->scale);
mb->needs_render = true;
return;
}
if (mb->dragging && !ev.left_held()) {
mb->dragging = false;
}
// Scroll zoom (centered on mouse position)
if (ev.scroll != 0 && ly >= MB_TOOLBAR_H) {
int render_h = cr.h - MB_TOOLBAR_H;
// Mouse position in fractal coords before zoom
fp_t mx_frac = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t my_frac = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
if (ev.scroll < 0) {
// Zoom in
mb->scale = fp_mul(mb->scale, fp_from_int(3) / 4); // * 0.75
} else {
// Zoom out
mb->scale = fp_mul(mb->scale, fp_from_int(4) / 3); // * 1.33
}
// Adjust center so mouse stays at same fractal point
fp_t new_mx = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
fp_t new_my = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
mb->center_x += mx_frac - new_mx;
mb->center_y += my_frac - new_my;
mb->needs_render = true;
}
}
static void mb_on_key(Window* win, const Zenith::KeyEvent& key) {
MandelbrotState* mb = (MandelbrotState*)win->app_data;
if (!mb || !key.pressed) return;
if (key.ascii == 'r' || key.ascii == 'R') {
Rect cr = win->content_rect();
mb->center_x = -fp_from_int(1) / 2;
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
} else if (key.ascii == '+' || key.ascii == '=') {
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
} else if (key.ascii == '-') {
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
}
}
static void mb_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Mandelbrot launcher
// ============================================================================
void open_mandelbrot(DesktopState* ds) {
int idx = desktop_create_window(ds, "Mandelbrot", 120, 60, 500, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
Rect cr = win->content_rect();
MandelbrotState* mb = (MandelbrotState*)zenith::malloc(sizeof(MandelbrotState));
zenith::memset(mb, 0, sizeof(MandelbrotState));
mb->desktop = ds;
mb->center_x = -fp_from_int(1) / 2; // -0.5
mb->center_y = 0;
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
mb->max_iter = MB_MAX_ITER;
mb->needs_render = true;
mb->dragging = false;
win->app_data = mb;
win->on_draw = mb_on_draw;
win->on_mouse = mb_on_mouse;
win->on_key = mb_on_key;
win->on_close = mb_on_close;
}
+244
View File
@@ -0,0 +1,244 @@
/*
* app_procmgr.cpp
* ZenithOS Desktop - Process Manager
* Copyright (c) 2026 Daniel Hammer
*/
#include "apps_common.hpp"
// ============================================================================
// Process Manager state
// ============================================================================
static constexpr int PM_TOOLBAR_H = 36;
static constexpr int PM_HEADER_H = 24;
static constexpr int PM_ITEM_H = 28;
static constexpr int PM_MAX_PROCS = 16;
static constexpr int PM_POLL_MS = 1000;
struct ProcMgrState {
DesktopState* desktop;
Zenith::ProcInfo procs[PM_MAX_PROCS];
int proc_count;
int selected; // selected row index (-1 = none)
uint64_t last_poll_ms;
};
// ============================================================================
// Callbacks
// ============================================================================
static void procmgr_on_poll(Window* win) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
uint64_t now = zenith::get_milliseconds();
if (now - pm->last_poll_ms < PM_POLL_MS) return;
pm->last_poll_ms = now;
// Remember previously selected PID
int prev_pid = -1;
if (pm->selected >= 0 && pm->selected < pm->proc_count) {
prev_pid = pm->procs[pm->selected].pid;
}
pm->proc_count = zenith::proclist(pm->procs, PM_MAX_PROCS);
// Restore selection by matching PID
pm->selected = -1;
if (prev_pid >= 0) {
for (int i = 0; i < pm->proc_count; i++) {
if (pm->procs[i].pid == prev_pid) {
pm->selected = i;
break;
}
}
}
}
static void procmgr_on_draw(Window* win, Framebuffer& fb) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Canvas c(win);
c.fill(colors::WINDOW_BG);
int fh = system_font_height();
// --- Toolbar ---
c.fill_rect(0, 0, c.w, PM_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
c.hline(0, PM_TOOLBAR_H - 1, c.w, colors::BORDER);
// "End Process" button
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Color btn_bg = (pm->selected >= 0 && pm->procs[pm->selected].pid != 0)
? Color::from_rgb(0xCC, 0x33, 0x33)
: Color::from_rgb(0xAA, 0xAA, 0xAA);
c.button(btn_x, btn_y, btn_w, btn_h, "End Process", btn_bg, colors::WHITE, 4);
// Process count on right
char count_str[24];
snprintf(count_str, sizeof(count_str), "%d processes", pm->proc_count);
int cw = text_width(count_str);
c.text(c.w - cw - 12, (PM_TOOLBAR_H - fh) / 2, count_str, colors::TEXT_COLOR);
// --- Header row ---
int header_y = PM_TOOLBAR_H;
c.fill_rect(0, header_y, c.w, PM_HEADER_H, Color::from_rgb(0xF0, 0xF0, 0xF0));
int col_pid = 12;
int col_name = 64;
int col_mem = c.w - 120;
int col_state = c.w - 52;
int ty = header_y + (PM_HEADER_H - fh) / 2;
c.text(col_pid, ty, "PID", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_name, ty, "Name", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_mem, ty, "Mem", Color::from_rgb(0x66, 0x66, 0x66));
c.text(col_state, ty, "St", Color::from_rgb(0x66, 0x66, 0x66));
c.hline(0, header_y + PM_HEADER_H - 1, c.w, colors::BORDER);
// --- Process rows ---
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
for (int i = 0; i < pm->proc_count; i++) {
int row_y = list_y + i * PM_ITEM_H;
if (row_y + PM_ITEM_H > c.h) break;
// Selected row highlight
if (i == pm->selected) {
c.fill_rect(0, row_y, c.w, PM_ITEM_H, colors::MENU_HOVER);
}
int ry = row_y + (PM_ITEM_H - fh) / 2;
// PID (right-aligned in PID column)
char pid_str[8];
snprintf(pid_str, sizeof(pid_str), "%d", (int)pm->procs[i].pid);
int pid_w = text_width(pid_str);
c.text(col_pid + 32 - pid_w, ry, pid_str, colors::TEXT_COLOR);
// Name (truncated to fit)
c.text(col_name, ry, pm->procs[i].name, colors::TEXT_COLOR);
// Memory
char mem_str[16];
format_size(mem_str, (int)pm->procs[i].heapUsed);
c.text(col_mem, ry, mem_str, colors::TEXT_COLOR);
// State
const char* st_str;
Color st_color;
switch (pm->procs[i].state) {
case 2: // Running
st_str = "Run";
st_color = Color::from_rgb(0x22, 0x88, 0x22);
break;
case 1: // Ready
st_str = "Rdy";
st_color = Color::from_rgb(0x33, 0x66, 0xCC);
break;
case 3: // Terminated
st_str = "Term";
st_color = Color::from_rgb(0xCC, 0x33, 0x33);
break;
default:
st_str = "?";
st_color = colors::TEXT_COLOR;
break;
}
c.text(col_state, ry, st_str, st_color);
}
}
static void procmgr_on_mouse(Window* win, MouseEvent& ev) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
if (ev.left_pressed()) {
// Check "End Process" button click
int btn_w = 100;
int btn_h = 26;
int btn_x = 8;
int btn_y = (PM_TOOLBAR_H - btn_h) / 2;
Rect btn_rect = {btn_x, btn_y, btn_w, btn_h};
if (btn_rect.contains(lx, ly)) {
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
zenith::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
return;
}
// Check row click
int list_y = PM_TOOLBAR_H + PM_HEADER_H;
if (ly >= list_y) {
int row = (ly - list_y) / PM_ITEM_H;
if (row >= 0 && row < pm->proc_count) {
pm->selected = row;
} else {
pm->selected = -1;
}
}
}
}
static void procmgr_on_key(Window* win, const Zenith::KeyEvent& key) {
ProcMgrState* pm = (ProcMgrState*)win->app_data;
if (!pm || !key.pressed) return;
if (key.scancode == 0x48) { // Up arrow
if (pm->selected > 0) pm->selected--;
else if (pm->proc_count > 0) pm->selected = 0;
} else if (key.scancode == 0x50) { // Down arrow
if (pm->selected < pm->proc_count - 1) pm->selected++;
} else if (key.scancode == 0x53) { // Delete key
if (pm->selected >= 0 && pm->selected < pm->proc_count
&& pm->procs[pm->selected].pid != 0) {
zenith::kill(pm->procs[pm->selected].pid);
pm->last_poll_ms = 0; // force refresh
}
}
}
static void procmgr_on_close(Window* win) {
if (win->app_data) {
zenith::mfree(win->app_data);
win->app_data = nullptr;
}
}
// ============================================================================
// Process Manager launcher
// ============================================================================
void open_procmgr(DesktopState* ds) {
int idx = desktop_create_window(ds, "Processes", 180, 80, 520, 400);
if (idx < 0) return;
Window* win = &ds->windows[idx];
ProcMgrState* pm = (ProcMgrState*)zenith::malloc(sizeof(ProcMgrState));
zenith::memset(pm, 0, sizeof(ProcMgrState));
pm->desktop = ds;
pm->selected = -1;
pm->last_poll_ms = 0;
// Initial poll
pm->proc_count = zenith::proclist(pm->procs, PM_MAX_PROCS);
win->app_data = pm;
win->on_draw = procmgr_on_draw;
win->on_mouse = procmgr_on_mouse;
win->on_key = procmgr_on_key;
win->on_close = procmgr_on_close;
win->on_poll = procmgr_on_poll;
}

Some files were not shown because too many files have changed in this diff Show More