PHASE II · SURFACE / MODULE 04 ·

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.

Module 04 — walkthrough
module introcoming soon

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.

Lesson 4.1

4.1 A window struct and the stack

the data behind every window
4.1 — the window model
~16 mincoming soon
window.ccopy
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]);
}
Lesson 4.2

4.2 Global window chrome

one titlebar renderer, every window
4.2 — the shared chrome layer
~24 mincoming soon

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.

chrome.c — the shared framecopy
// 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
}
Active vs inactive is what sells "real"

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.

Lesson 4.3

4.3 Dragging and resizing

make the windows move
4.3 — drag, resize, snap
~20 mincoming soon

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.

drag.ccopy
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
    }
}
Lesson 4.4

4.4 Hit-testing and focus

which window is the mouse over?
4.4 — hit-testing top to bottom
~14 mincoming soon

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.

hittest.ccopy
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
}
Lesson 4.5

4.5 The desktop and wallpaper

the bottom layer everything sits on
4.5 — the desktop layer & wallpaper
~14 mincoming soon

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.

desktop.c — draw ordercopy
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);
}
Lesson 4.6

4.6 Desktop icons and a .ico reader

real icon files, decoded to a 48x48 surface
4.6 — parsing .ico into a drawable surface
~26 mincoming soon

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.

ico.c — the .ico header + directorycopy
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:

ico.c — pick 48x48 and decodecopy
// 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];
        }
    }
}
Two things bite everyone with .ico

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.

Lesson 4.7

4.7 The taskbar

pinned launchers, running apps, a home for the clock
4.7 — building the taskbar
~20 mincoming soon

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.

taskbar.ccopy
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
}
Lesson 4.8

4.8 A live clock

reading the hardware clock and formatting it
4.8 — the RTC and drawing the time
~16 mincoming soon

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).

clock.c — read the RTCcopy
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);
}
Read the RTC when it's not updating

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.

Module 04 checkpoint
  • 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.

next module

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 →
or see all plans →