Skip to content
Aditya-B-Dhruva edited this page Oct 14, 2025 · 1 revision

Phase 1: The Foundation - The Graphics Driver & Framebuffer This is the lowest level, the canvas upon which everything will be painted. You cannot draw anything without this.

  1. Acquiring the Framebuffer: Your primary constraint is "no API calling," which means we can't use high-level graphics APIs. The standard, low-level way to do this is to have your bootloader (like GRUB) set the video mode for you. Action: Configure your bootloader to request a 1280x720, 24-bit color VESA/VBE video mode. Mechanism: The bootloader will get the physical address of the Linear Frame Buffer (LFB) from the BIOS/UEFI and pass it to your kernel as part of the boot information (e.g., the Multiboot2 info structure). Result: Your kernel starts with direct, raw access to a contiguous block of memory that represents the screen.
  2. The Framebuffer Driver: This is a small C module that knows how to manipulate this memory. Data Structure: Create a Framebuffer struct to hold the essential information. C typedef struct { void* address; // The physical address of the LFB unsigned int width; // 1280 unsigned int height; // 720 unsigned int pitch; // Bytes per row (may be > width * bpp) unsigned int bpp; // Bits per pixel (24) } Framebuffer;

Pitch is critical. It's the number of bytes to get from the start of one scanline to the next. It might be slightly larger than width * 3 due to hardware memory alignment. The Most Fundamental Operation: put_pixel() Every single drawing operation will ultimately call this function. It writes a 24-bit color value to a specific (x, y) coordinate. C void put_pixel(Framebuffer* fb, int x, int y, uint32_t color) { // Bounds checking if (x < 0 || x >= fb->width || y < 0 || y >= fb->height) { return; }

// 24-bit color (8-8-8 RGB)
unsigned char red   = (color >> 16) & 0xFF;
unsigned char green = (color >> 8) & 0xFF;
unsigned char blue  = color & 0xFF;

// Calculate the address
uint8_t* pixel_address = (uint8_t*)fb->address + (y * fb->pitch) + (x * (fb->bpp / 8));

// Write the color bytes (assuming little-endian BGR order, common in VESA)
pixel_address[0] = blue;
pixel_address[1] = green;
pixel_address[2] = red;

}

Phase 2: The Toolbox - Drawing Primitives Now that you can draw a pixel, you can create a library of functions to draw basic shapes. draw_rect(x, y, width, height, color): This is the easiest. It's just two nested for loops calling put_pixel. This will be your workhorse for windows, buttons, and text boxes. draw_line(x1, y1, x2, y2, color): Don't implement this with floating-point math. The classic, integer-only solution is Bresenham's Line Algorithm. It's fast, efficient, and perfect for this context. draw_bitmap(x, y, bitmap): This is key for your pixelated aesthetic. A bitmap is just an array of color values. Your function will iterate through the array and call put_pixel for each one. This is how you'll draw icons, cursors, and characters. You can support a "transparent" color key to skip drawing certain pixels, allowing for non-rectangular sprites. Bitmap Fonts: For text, you will use bitmap fonts. A font will be a single large bitmap (a sprite sheet) containing all the ASCII characters. draw_char(x, y, char c, font, color): This function calculates the position of the character c in the font's sprite sheet and calls draw_bitmap for that specific region. draw_string(x, y, const char* str, font, color): This simply calls draw_char in a loop, advancing the x position after each character.

Phase 3: The Building Blocks - The Widget Toolkit Widgets are the interactive elements of your GUI (buttons, labels, etc.). They combine the drawing primitives with state and behavior. Base Widget Structure: Use a generic struct and function pointers to create an object-oriented-like system in C. C struct Widget; // Forward declaration

typedef struct Widget { int x, y, width, height; bool dirty; // Flag to know if it needs redrawing

// Function pointers for behavior
void (*draw)(struct Widget* self, Framebuffer* fb);
void (*onClick)(struct Widget* self, int mouse_x, int mouse_y);
// ... onHover, onKeyPress, etc.

void* data; // Pointer to specific widget data (e.g., ButtonData)

} Widget;

Specific Widgets: Button: data points to a struct containing text, colors, and a callback function. Its draw function draws a rectangle and the button's text. Label: data points to a struct with the text string. Its draw function just calls draw_string. Panel/Frame: A simple container widget. Its draw function just draws a rectangle that forms the background of a window or a section of the UI.

Phase 4: The Organizer - The Window Manager & Compositor This system manages a collection of windows and is responsible for drawing them correctly. Window Structure: A window is just another widget, a container that holds a list of child widgets (its buttons, labels, etc.). It will also have properties like a title bar string. Z-Ordering: You need a way to track which window is on top of which. A simple doubly linked list of all active windows is a perfect data structure for this. The head of the list is the top-most window. The Compositor/Redraw Loop: This is the heart of the GUI's main loop. To avoid flicker and draw everything correctly, you follow these steps on every "frame": Draw the desktop background (a simple draw_rect or draw_bitmap). Iterate through your window list from back to front (from tail to head). For each window, call its draw function. This will in turn call the draw function for all of its child widgets. Draw the mouse cursor on top of everything else. This back-to-front drawing process naturally handles overlapping windows without complex clipping algorithms.

Phase 5: The Interaction - Event Handling A GUI needs input. You'll need low-level drivers for your PS/2 or USB mouse and keyboard. Event Queue: Your drivers should not call GUI code directly. They should place input events into a fixed-size circular buffer (the event queue). An event could be MOUSE_MOVE, MOUSE_BUTTON_DOWN, KEY_PRESS, etc. Event Dispatcher: Your main GUI loop will continuously pull events from this queue. On MOUSE_MOVE, it updates the cursor's position and redraws the screen. It also checks which widget is under the cursor to handle hover effects. On MOUSE_BUTTON_DOWN, it performs hit detection: It checks which window is at the top of the Z-order under the cursor. Then it checks which widget within that window is under the cursor. Finally, it calls that widget's onClick handler. On KEY_PRESS, it sends the key event to the currently focused window/widget (e.g., a textbox). This layered design ensures that your code is organized, extensible, and free from external dependencies. You are essentially building a complete display server from the ground up.

Clone this wiki locally