Windows & a Desktop
Primitives become a system. Build a window manager that stacks, drags, and resizes windows; a global chrome so every titlebar matches; and a dock. This is where a pile of pixels becomes a desktop.
A window is just a rectangle that remembers its position, size, title, and z-order. A window manager is the code that stacks them, decides which one the mouse is over, and lets you drag and resize. Build it in that order.
4.1 A window struct and the stack
typedef struct {
int x, y, w, h;
const char *title;
int z; // stacking order; higher = on top
int focused;
int minimized;
void (*draw_content)(struct Window*); // each app fills this
} Window;
static Window windows[MAX_WINDOWS];
// draw back-to-front so higher-z windows land on top
void wm_draw_all(void) {
sort_by_z(windows);
for (int i = 0; i < window_count; i++)
if (!windows[i].minimized) draw_window(&windows[i]);
}4.2 Global window chrome
The mistake to avoid: letting each app draw its own titlebar. They drift apart instantly. Instead, one chrome function draws the frame — titlebar, border, shadow, close/min/max buttons, active vs inactive treatment — for every window. Apps only fill the content area. Change the look in one place, every window updates.
// spec-driven constants: change here, whole OS follows
#define TITLE_H 40
#define RADIUS 10
#define CTRL_SIZE 28
void chrome_draw_frame(Window *win) {
int focused = win->focused;
UINT32 body = focused ? 0x141B27 : 0x0F1622;
UINT32 title = focused ? 0xE8EDF4 : 0x64738A;
draw_shadow(win->x, win->y, win->w, win->h);
draw_rounded_rect(win->x, win->y, win->w, win->h, RADIUS, body);
draw_rounded_rect(win->x, win->y, win->w, TITLE_H, RADIUS, 0x101724);
draw_string(win->x+16, win->y+12, win->title, title);
draw_window_controls(win, focused); // close/min/max, right-aligned
}A focused window gets bright title text, a solid border, and a deeper shadow; an unfocused one goes dim, flatter, and its controls stop reacting to hover. That single contrast is most of what makes a desktop read as a real OS rather than a mockup. Bake it into the chrome from the start.
4.3 Dragging and resizing
Dragging is bookkeeping: on mouse-down in the titlebar, record the offset between the cursor and the window's corner; on every mouse-move while held, move the window to keep that offset constant. Resizing is the same idea on the edges, adjusting width/height instead of position.
static int drag_dx, drag_dy;
static Window *dragging;
void on_mouse_down(int mx, int my) {
Window *w = window_at(mx, my);
if (w && in_titlebar(w, mx, my)) {
dragging = w;
drag_dx = mx - w->x; // remember grab offset
drag_dy = my - w->y;
}
}
void on_mouse_move(int mx, int my) {
if (dragging) {
mark_dirty(dragging); // erase old position
dragging->x = mx - drag_dx;
dragging->y = my - drag_dy;
mark_dirty(dragging); // draw new position
}
}4.4 Hit-testing and focus
When the mouse clicks, you need the topmost window under it. Walk the stack from highest z to lowest; the first window whose rectangle contains the point wins. Clicking it raises it to the top and gives it focus.
Window *window_at(int mx, int my) {
// highest z first — topmost window wins
for (int i = window_count - 1; i >= 0; i--) {
Window *w = z_order[i];
if (!w->minimized && point_in_rect(mx, my, w->x, w->y, w->w, w->h))
return w;
}
return 0; // clicked the desktop
}4.5 The desktop and wallpaper
The desktop is the very bottom of the draw order: a full-screen wallpaper, drawn before any window. Keep it cheap — a gradient or a couple of radial blooms, filled with the fast opaque path from Module 03, never per-pixel alpha (that's the watchdog-tripping mistake). Everything else — icons, windows, the taskbar — draws on top of it.
void desktop_draw(void) {
draw_wallpaper(); // 1. bottom: full-screen fill
draw_desktop_icons(); // 2. icons on the wallpaper
wm_draw_all(); // 3. windows, back-to-front
taskbar_draw(); // 4. taskbar always on top
cursor_draw(); // 5. the mouse, last of all
}
// wallpaper uses the FAST opaque fill, never per-pixel alpha
void draw_wallpaper(void) {
fill_rect(0, 0, ScreenW, ScreenH, FORGE_WALLPAPER_BASE);
draw_radial_bloom(ScreenW*68/100, ScreenH/4, FORGE_WALLPAPER_BLOOM);
}4.6 Desktop icons and a .ico reader
A desktop icon is a small image plus a label. To use real .ico files, you write a tiny decoder. The .ico format is simple: a header, a directory of image entries, then the image data. Each entry is usually a BMP (occasionally PNG). You pick the entry closest to your target — 48x48 — and decode its pixels into a surface your compositor can blit.
struct ico_header { // 6 bytes at the start of the file
UINT16 reserved; // always 0
UINT16 type; // 1 = icon
UINT16 count; // how many images in the file
} __attribute__((packed));
struct ico_entry { // 16 bytes per image
UINT8 width; // 0 means 256
UINT8 height;
UINT8 colors;
UINT8 reserved;
UINT16 planes;
UINT16 bpp;
UINT32 size; // bytes of image data
UINT32 offset; // where in the file that data starts
} __attribute__((packed));Read the header, walk the directory, and pick the entry whose size is closest to 48x48. Then decode that entry — the common case is a 32-bit BMP, which is bottom-up rows of BGRA pixels:
// choose the entry nearest our target icon size
int ico_pick_48(struct ico_entry *dir, int count) {
int best = 0, best_d = 9999;
for (int i = 0; i < count; i++) {
int wsz = dir[i].width ? dir[i].width : 256;
int d = abs(wsz - 48);
if (d < best_d) { best_d = d; best = i; }
}
return best;
}
// decode a 32-bpp BMP entry into a 48x48 BGRA surface
void ico_decode(UINT8 *data, int w, int h, UINT32 *out48) {
UINT8 *px = data + bmp_pixel_offset(data);
for (int y = 0; y < 48; y++) {
for (int x = 0; x < 48; x++) {
// nearest-neighbour scale from wxh down/up to 48x48
int sx = x * w / 48, sy = y * h / 48;
// BMP rows are bottom-up, so flip y
UINT8 *p = px + ((h-1-sy) * w + sx) * 4;
out48[y*48+x] = (p[3]<<24)|(p[2]<<16)|(p[1]<<8)|p[0];
}
}
}First: BMP rows are stored bottom-up. If your icon renders upside down, this is why — flip the y index as shown. Second: a width byte of 0 means 256, not zero — a 256x256 icon stores its size as 0 because the field is only 8 bits. Miss that and your largest icons look like empty 0x0 images.
With the surface decoded once at load time, drawing an icon is just a blit with alpha — and the icon size, shape, and label visibility are exactly the knobs the Desktop Designer exposes. Everything you set there maps to a value here.
4.7 The taskbar
The taskbar is a strip along one screen edge holding app buttons and the clock. It's a thin layer over things you already built: a background fill, a row of icon buttons (each a hit-test region that launches or focuses an app), and a clock pinned to the far end. Position (bottom/top/left), thickness, and style (full-width vs floating) are all just layout math — the same knobs the designer exposes.
void taskbar_draw(void) {
int h = FORGE_TASKBAR_H;
int y = (FORGE_TASKBAR_POS == FORGE_TB_TOP) ? 0 : ScreenH - h;
// full-width vs floating is just insets on the rect
int inset = (FORGE_TASKBAR_STYLE == FORGE_TB_FLOAT) ? 10 : 0;
draw_rounded_rect(inset, y - inset, ScreenW - inset*2, h,
inset ? 12 : 0, 0x00121A26);
// pinned + running app buttons, left to right
int bx = inset + 12;
for (int i = 0; i < app_count; i++) {
blit_icon(apps[i].icon48, bx, y + (h-32)/2, 32);
taskbar_hit(i, bx, y, 32, h); // click = launch/focus
bx += 40;
}
if (FORGE_TASKBAR_CLOCK)
clock_draw(ScreenW - inset - 96, y, h); // far end
}4.8 A live clock
The time comes from the RTC (real-time clock) chip, read through I/O ports 0x70/0x71 in a handful of BCD registers. Read the fields, convert from BCD, and format them — 12-hour or 24-hour, exactly the toggle the designer exposes. Redraw once a second (your Module 02 timer already gives you the tick).
static UINT8 rtc_read(UINT8 reg) {
outb(0x70, reg); // select register
return inb(0x71); // read its value
}
static UINT8 bcd(UINT8 v){ return (v & 0x0F) + ((v >> 4) * 10); }
void clock_draw(int x, int y, int h) {
UINT8 hour = bcd(rtc_read(0x04));
UINT8 min = bcd(rtc_read(0x02));
char buf[16];
if (FORGE_CLOCK_FORMAT == 12) {
const char *ap = hour < 12 ? "AM" : "PM";
int h12 = hour % 12; if (!h12) h12 = 12;
sprintf(buf, "%d:%02d %s", h12, min, ap);
} else {
sprintf(buf, "%02d:%02d", hour, min);
}
draw_string(x, y + (h-16)/2, buf, 0x00E8EDF4);
}The RTC updates once a second, and if you read mid-update you can get a garbled time (e.g. 10:59 rolling to 11:00 reading as 10:00). Check the update-in-progress flag (status register A, bit 7) and wait for it to clear before reading, or read twice and confirm the values match. It's a rare glitch but a baffling one when the clock jumps.
- A window struct with z-order and a content callback
- One global chrome renderer — every window matches
- Draggable titlebars and resizable edges
- Top-down hit-testing and click-to-focus
- A desktop with a cheap, fast wallpaper
- A .ico decoder producing 48x48 icon surfaces
- A taskbar with app buttons and a live RTC clock
Every knob in the Desktop Designer — icon size and shape, taskbar position and style, the clock format, window radius and titlebar height — now maps to real code you've written. Design a look in the tool, export the config, and drop the values straight into these files.
Module 05 — USB & a Filesystem
Claim the xHCI controller from the firmware, talk to a USB drive over Bulk-Only Transport, and read & write real FAT32 files — then a working file manager on top.
Get lifetime access — $149 →