Skip to content

HOWTO Add a GUI application

kazah-png edited this page Jul 28, 2026 · 3 revisions

HOWTO Add a GUI application

How to add a windowed application to the NyxOS desktop. Built-in apps are kernel windows: one *_win.c / *_win.h pair each, a per-instance context struct, and a set of callbacks the compositor invokes.

The worked example is a stopwatch with a display, two buttons and a running clock.

See also: GUI-Subsystem, Desktop-Applications, Kernel-Data-Structures, Building

The application contract

The compositor owns the window frame, the title bar, dragging, resizing and z-order. An application supplies a draw function and whichever callbacks it needs.

FILE — kernel/gui/core/compositor.h (the callback set)

window_draw_fn draw;
void (*on_key)(struct window* win, int key);
void (*on_click)(struct window* win, int mx, int my, int btn);
void (*on_pressed)(struct window* win, int mx, int my, int btn);
void (*on_mousemove)(struct window* win, int mx, int my, int btns);
int  (*on_tick)(struct window* win);
void* reserved;
Callback When Notes
draw Every repaint Receives the client rect: (cx, cy, cw, ch)
on_click Button release inside the client area Coordinates are screen-absolute
on_pressed Button press Use for drag-style interaction
on_mousemove Pointer moves over the window btns is the current button mask
on_key Key while the window is focused
on_tick Periodic, ~30 fps Return 1 if something changed and needs a repaint, 0 if idle
reserved Your per-instance context pointer

Important

reserved is how per-window state is stored. Never use a static variable for application state — a second window of the same app would share it.

Warning

on_tick returning 1 every call forces a full recomposite at 30 fps and will visibly slow the desktop. Return 1 only when the window's contents actually changed.

Step 1 — Header: context and prototypes

FILE — kernel/gui/apps/stopwatch_win.h

#ifndef STOPWATCH_WIN_HDR
#define STOPWATCH_WIN_HDR

#include "kernel.h"
#include "compositor.h"

#define SW_WIN_W 220
#define SW_WIN_H 120

typedef struct {
    uint64_t start_tick;     /* tick_count when started, 0 = stopped */
    uint64_t accumulated;    /* ticks banked from previous runs */
    int      running;
    char     display[16];
} stopwatch_ctx_t;

stopwatch_ctx_t* stopwatch_create_ctx(void);
void stopwatch_win_draw(window_t* win, int cx, int cy, uint32_t cw, uint32_t ch);
void stopwatch_win_click(window_t* win, int mx, int my, int btn);
int  stopwatch_win_tick(window_t* win);

#endif

Geometry constants belong in the header so the launcher can size the window from them, exactly as calc_win.h does with CALC_WIN_W/CALC_WIN_H.

Step 2 — Implementation

FILE — kernel/gui/apps/stopwatch_win.c

#include "theme.h"
#include "kernel.h"
#include "compositor.h"
#include "stopwatch_win.h"
#include "font.h"

#define BTN_W 80
#define BTN_H 28
#define PAD   12

stopwatch_ctx_t* stopwatch_create_ctx(void) {
    stopwatch_ctx_t* c = (stopwatch_ctx_t*)kmalloc(sizeof(stopwatch_ctx_t));
    if (!c) return 0;
    memset(c, 0, sizeof(*c));
    strncpy(c->display, "0.0", sizeof(c->display) - 1);
    return c;
}

static uint64_t elapsed_ticks(stopwatch_ctx_t* c) {
    return c->running ? c->accumulated + (tick_count - c->start_tick)
                      : c->accumulated;
}

void stopwatch_win_draw(window_t* win, int cx, int cy, uint32_t cw, uint32_t ch) {
    stopwatch_ctx_t* c = (stopwatch_ctx_t*)win->reserved;
    if (!c) return;

    fb_fill_rect(cx, cy, cw, ch, THEME_WINDOW_BG);

    uint64_t ms = elapsed_ticks(c);            /* 1000 Hz => ticks are ms */
    snprintf(c->display, sizeof(c->display), "%llu.%llu", ms / 1000, (ms % 1000) / 100);
    font_draw_string(cx + PAD, cy + PAD, c->display, THEME_TEXT, THEME_WINDOW_BG);

    int by = cy + ch - BTN_H - PAD;
    fb_fill_rect(cx + PAD, by, BTN_W, BTN_H, THEME_ACCENT);
    font_draw_string(cx + PAD + 16, by + 8,
                     c->running ? "Stop" : "Start", THEME_ON_ACCENT, THEME_ACCENT);

    fb_fill_rect(cx + PAD * 2 + BTN_W, by, BTN_W, BTN_H, THEME_ACCENT_DIM);
    font_draw_string(cx + PAD * 2 + BTN_W + 16, by + 8,
                     "Reset", THEME_ON_ACCENT, THEME_ACCENT_DIM);
}

void stopwatch_win_click(window_t* win, int mx, int my, int btn) {
    stopwatch_ctx_t* c = (stopwatch_ctx_t*)win->reserved;
    if (!c || btn != 1) return;

    int cx = win->x, cy = win->y;              /* client origin; see note below */
    int by = cy + (int)win->h - BTN_H - PAD;
    if (my < by || my >= by + BTN_H) return;

    if (mx >= cx + PAD && mx < cx + PAD + BTN_W) {
        if (c->running) { c->accumulated = elapsed_ticks(c); c->running = 0; }
        else            { c->start_tick = tick_count;        c->running = 1; }
    } else if (mx >= cx + PAD * 2 + BTN_W && mx < cx + PAD * 2 + BTN_W * 2) {
        c->accumulated = 0;
        c->start_tick  = tick_count;
    }
}

int stopwatch_win_tick(window_t* win) {
    stopwatch_ctx_t* c = (stopwatch_ctx_t*)win->reserved;
    return (c && c->running) ? 1 : 0;          /* repaint only while running */
}

Rules that matter

Rule Why
Colours come from theme.h roles A literal fb_rgb(…) cannot be re-themed. See GUI-Subsystem
Lay out on the 1024×768 design grid The compositor scales to the live framebuffer; deriving from fb_get_width() applies the scale twice
Clip drawing to the client rect Drawing outside it corrupts neighbouring windows
No floating point The kernel is built -mno-sse; use integer or fixed-point maths
Allocate context with kmalloc And memset it — kmalloc does not zero

Step 3 — Add a launcher

FILE — kernel/gui/core/compositor.c

void launch_stopwatch(void) {
    window_t* w = window_create(340, 200, SW_WIN_W, SW_WIN_H,
                                "Stopwatch", stopwatch_win_draw);
    if (!w) return;
    w->reserved = stopwatch_create_ctx();
    if (w->reserved) {
        w->on_click = stopwatch_win_click;
        w->on_tick  = stopwatch_win_tick;
    }
}

window_create returns NULL when MAX_WINDOWS (32) is reached — always check.

This is the same shape as the existing launch_minesweeper():

FILE — kernel/gui/core/compositor.c (existing code)

void launch_minesweeper(void) {
    window_t* mwin = window_create(320, 180, MS_WIN_W, MS_WIN_H,
                                   "Minesweeper", minesweeper_win_draw);
    if (!mwin) return;
    mwin->reserved = minesweeper_create_ctx();
    if (mwin->reserved) {
        mwin->on_click = minesweeper_win_click;
        mwin->on_key   = minesweeper_win_key;
    }
}

Step 4 — Wire it into the desktop

Start menu / action dispatch

Add a case to the action switch in compositor.c:

case 16: // Stopwatch
    launch_stopwatch();
    break;

Desktop icon

Two parallel arrays hold the icon set:

FILE — kernel/gui/core/compositor.c

#define NUM_DESKTOP_ICONS 11              /* was 10 */
static const char* desktop_icon_names[] = {
    "Files", "Terminal", "Editor", "Viewer", "Settings",
    "Paint", "Sounds", "Calc", "Games", "Selene", "Timer"
};
static int desktop_icon_actions[] = {0, 3, 1, 2, 4, 7, 8, 11, 14, 15, 16};

Important

NUM_DESKTOP_ICONS, desktop_icon_names[] and desktop_icon_actions[] must all agree. A mismatch reads past the end of an array and draws garbage — or faults.

Icons reflow into a grid automatically, so no coordinates need updating.

Shell command (optional)

{"stopwatch", cmd_stopwatch, "Open the Stopwatch app", false},

See HOWTO-Add-a-shell-command.

Step 5 — Add to the build

FILE — kernel/Makefile

OBJS_KERNEL = … stopwatch_win.o …

Header dependencies are tracked automatically by -MMD.

Step 6 — Build and verify

CODE — Rebuild and boot the desktop

host $ make -C kernel
host $ ./run.ps1

Check: the icon appears and is labelled; clicking opens the window; buttons respond; the clock advances only while running; the window drags, resizes, minimises and closes; a second instance keeps its own state; the desktop stays responsive.

Freeing the context

window_destroy frees the window but does not know how to free reserved. If your context owns further allocations — a decoded image, a file buffer — free them where the app is torn down, following how Selene-Browser releases decoded images when a tab closes.

Checklist

  • Header defines the context struct and window geometry
  • create_ctx() uses kmalloc + memset, and returns NULL on failure
  • All per-window state lives in reserved, never in static
  • Colours use theme.h roles
  • Layout uses the 1024×768 design grid
  • Drawing stays inside the client rect
  • No floating point
  • on_tick returns 1 only when something changed
  • window_create result checked for NULL
  • Icon arrays and NUM_DESKTOP_ICONS all updated together
  • .o added to OBJS_KERNEL
  • Builds with zero warnings
  • Desktop-Applications page updated in this wiki

Troubleshooting

Symptom Cause Fix
Window opens blank draw not passed to window_create Pass it as the last argument
Clicks do nothing on_click assigned before reserved was checked, or the ctx allocation failed Assign callbacks inside the if (w->reserved) block
Content drawn in the wrong place Layout derived from fb_get_width() Use the 1024×768 design grid
Drawing appears over other windows Drawing outside the client rect Clip to (cx, cy, cw, ch)
Desktop becomes sluggish on_tick always returns 1 Return 0 when idle
Two windows share state State in a static variable Move it into the context struct
Garbage icons, or a fault at startup Icon arrays out of sync with NUM_DESKTOP_ICONS Update all three together
Link error undefined reference to stopwatch_win_draw .o missing from OBJS_KERNEL Add it
Build error mentioning SSE Floating point in kernel code Use integer maths

See also

External resources

Clone this wiki locally