PS/2 Mouse Driver
The PS/2 mouse interface dates back to IBM's PS/2 computers (1987), but remains supported on virtually all PCs through USB-to-PS/2 emulation. It's the simplest way to add mouse support to your OS—no USB stack required!
Mouse Protocol
The PS/2 mouse communicates through the same 8042 keyboard controller we used for keyboard input, but uses a separate data channel (the "auxiliary" port). Think of it like having two phone lines through one switchboard:
╔═════════════════════════════════════════════════════════════════════════════╗
║ 8042 KEYBOARD CONTROLLER ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌────────────────────────┐ ║
║ ┌──────────┐ │ 8042 Controller │ ┌──────────┐ ║
║ │ Keyboard │────────│ Port 0x60: Data │────────│ CPU │ ║
║ │ (IRQ 1) │ │ Port 0x64: Command │ │ │ ║
║ └──────────┘ │ │ └──────────┘ ║
║ │ ┌─────────────────┐ │ ║
║ ┌──────────┐ │ │ Status Register │ │ ║
║ │ Mouse │────────│ │ Bit 0: Output │ │ ║
║ │ (IRQ 12) │ │ │ Bit 1: Input │ │ ║
║ └──────────┘ │ │ Bit 5: Aux data │ │ ║
║ │ └─────────────────┘ │ ║
║ └────────────────────────┘ ║
║ ║
║ Commands to 0x64: Data via 0x60: ║
║ 0xA8 = Enable auxiliary (mouse) 0xF4 = Enable data reporting ║
║ 0xA9 = Disable auxiliary 0xF5 = Disable data reporting ║
║ 0xD4 = Write to auxiliary device 0xF6 = Set defaults ║
║ 0x20 = Read controller config 0xFF = Reset mouse ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════╝
Before sending data to the mouse or reading from it, we must check the controller's status register:
/* Wait for 8042 controller to be ready */
static void mouse_wait(int type) {
int timeout = 100000;
while (timeout--) {
if (type == 0) {
// Wait for output buffer full (data ready to read)
if (inb(0x64) & 0x01) return;
} else {
// Wait for input buffer empty (ready to receive command)
if (!(inb(0x64) & 0x02)) return;
}
}
}
/* Write command to mouse */
static void mouse_write(uint8_t data) {
mouse_wait(1);
outb(0x64, 0xD4); // Tell controller: next byte goes to mouse
mouse_wait(1);
outb(0x60, data); // Send the actual command
}
/* Read response from mouse */
static uint8_t mouse_read(void) {
mouse_wait(0);
return inb(0x60);
}
The initialization sequence enables the mouse and tells the controller to generate IRQ 12 when mouse data arrives:
/* PS/2 Mouse State */
typedef struct {
int x, y; // Current position
uint8_t buttons; // Button state
int8_t packet[3]; // Current packet
uint8_t cycle; // Packet byte index (0-2)
} mouse_t;
mouse_t mouse = {0};
/* Initialize PS/2 mouse */
void mouse_init(void) {
// Enable auxiliary device (mouse)
mouse_wait(1);
outb(0x64, 0xA8);
// Enable interrupts
mouse_wait(1);
outb(0x64, 0x20);
mouse_wait(0);
uint8_t status = inb(0x60) | 2;
mouse_wait(1);
outb(0x64, 0x60);
mouse_wait(1);
outb(0x60, status);
// Use default settings
mouse_write(0xF6);
mouse_read();
// Enable data reporting
mouse_write(0xF4);
mouse_read();
}
Packet Format
PS/2 mice send data in 3-byte packets. Each packet contains button state, X movement, and Y movement. The tricky part is that movement values are signed 9-bit numbers!
╔═════════════════════════════════════════════════════════════════════════════╗
║ PS/2 MOUSE PACKET FORMAT (3 bytes) ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ║
║ BYTE 0 (Status/Buttons): ║
║ ┌───┬───┬───┬───┬───┬───┬───┬───┐ ║
║ │YOF│XOF│YSN│XSN│ 1 │MID│RGT│LFT│ ║
║ └───┴───┴───┴───┴───┴───┴───┴───┘ ║
║ 7 6 5 4 3 2 1 0 ║
║ ║
║ BYTE 1 (X Movement): Lower 8 bits of X delta ║
║ ┌───┬───┬───┬───┬───┬───┬───┬───┐ ║
║ │ X7│ X6│ X5│ X4│ X3│ X2│ X1│ X0│ Range: -256 to +255 ║
║ └───┴───┴───┴───┴───┴───┴───┴───┘ (9th bit = XSN in byte 0) ║
║ ║
║ BYTE 2 (Y Movement): Lower 8 bits of Y delta ║
║ ┌───┬───┬───┬───┬───┬───┬───┬───┐ ║
║ │ Y7│ Y6│ Y5│ Y4│ Y3│ Y2│ Y1│ Y0│ Range: -256 to +255 ║
║ └───┴───┴───┴───┴───┴───┴───┴───┘ (9th bit = YSN in byte 0) ║
║ ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ STATUS BITS: ║
║ LFT (bit 0) = Left button pressed ║
║ RGT (bit 1) = Right button pressed ║
║ MID (bit 2) = Middle button pressed ║
║ Bit 3 = Always 1 (sync marker) ║
║ XSN (bit 4) = X sign bit (1 = negative) ║
║ YSN (bit 5) = Y sign bit (1 = negative) ║
║ XOF (bit 6) = X overflow (movement too fast) ║
║ YOF (bit 7) = Y overflow (movement too fast) ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════╝
Sync Marker: Bit 3 of byte 0 is always 1. If you see a byte with bit 3 clear, you've lost synchronization—discard bytes until you find one with bit 3 set.
The IRQ 12 handler assembles packets one byte at a time. When all three bytes arrive, we process the complete packet:
/* Mouse IRQ12 Handler */
void mouse_handler(void) {
uint8_t data = inb(0x60);
switch (mouse.cycle) {
case 0:
// Byte 0: buttons + signs + overflow
if (data & 0x08) { // Must have bit 3 set
mouse.packet[0] = data;
mouse.cycle++;
}
break;
case 1:
// Byte 1: X movement
mouse.packet[1] = data;
mouse.cycle++;
break;
case 2:
// Byte 2: Y movement - complete packet
mouse.packet[2] = data;
mouse.cycle = 0;
// Update mouse state
mouse.buttons = mouse.packet[0] & 0x07;
int dx = mouse.packet[1];
int dy = mouse.packet[2];
// Handle sign extension
if (mouse.packet[0] & 0x10) dx |= 0xFFFFFF00;
if (mouse.packet[0] & 0x20) dy |= 0xFFFFFF00;
// Update position (invert Y for screen coords)
mouse.x = clamp(mouse.x + dx, 0, screen_width - 1);
mouse.y = clamp(mouse.y - dy, 0, screen_height - 1);
// Notify window manager
wm_mouse_event(mouse.x, mouse.y, mouse.buttons);
break;
}
pic_send_eoi(12);
}
Y-Axis Inversion: The mouse reports +Y as "up" but screen coordinates have Y increasing downward. We subtract dy instead of adding it!
Cursor Rendering
Rendering a mouse cursor requires careful handling. The cursor must appear on top of everything, move smoothly, and not leave visual artifacts. There are two approaches:
╔═════════════════════════════════════════════════════════════════════════════╗
║ CURSOR RENDERING APPROACHES ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ║
║ SOFTWARE CURSOR (simple, portable): ║
║ ║
║ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ║
║ │ Screen with │ │ Save rect at │ │ Draw cursor │ ║
║ │ windows │─────▶│ old cursor pos │─────▶│ at new pos │ ║
║ │ │ │ to buffer │ │ │ ║
║ └─────────────────┘ └─────────────────┘ └─────────────────┘ ║
║ │ │ ║
║ └───────────────────────┘ ║
║ On next move: restore old, save new, draw new ║
║ ║
║ HARDWARE CURSOR (VGA/GPU feature): ║
║ ║
║ ┌─────────────────┐ ┌─────────────────┐ ║
║ │ GPU overlays │ │ Just update │ No background save/restore ║
║ │ cursor sprite │─────▶│ position reg │ Hardware handles blending ║
║ │ on scanout │ │ (one write!) │ Very fast and tear-free ║
║ └─────────────────┘ └─────────────────┘ ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════╝
Here's a software cursor implementation with background preservation:
/* Mouse cursor data (16x16 pixels) */
typedef struct {
int hotspot_x, hotspot_y; // Click point offset
uint32_t pixels[16][16]; // ARGB data (alpha for transparency)
uint32_t background[16][16]; // Saved screen content
int saved_x, saved_y; // Position of saved background
} cursor_t;
/* Default arrow cursor bitmap */
static const uint8_t cursor_arrow[16][16] = {
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,2,2,1,0,0,0,0,0,0,0,0,0,0,0,0},
{1,2,2,2,1,0,0,0,0,0,0,0,0,0,0,0},
{1,2,2,2,2,1,0,0,0,0,0,0,0,0,0,0},
{1,2,2,2,2,2,1,0,0,0,0,0,0,0,0,0},
{1,2,2,2,2,2,2,1,0,0,0,0,0,0,0,0},
{1,2,2,2,2,1,1,1,1,0,0,0,0,0,0,0},
{1,2,2,2,2,1,0,0,0,0,0,0,0,0,0,0},
{1,2,1,1,2,2,1,0,0,0,0,0,0,0,0,0},
{1,1,0,0,1,2,1,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,2,1,0,0,0,0,0,0,0,0},
{0,0,0,0,0,1,2,1,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
}; // 0=transparent, 1=black, 2=white
cursor_t cursor;
/* Save screen area under cursor */
void cursor_save_background(int x, int y) {
cursor.saved_x = x;
cursor.saved_y = y;
for (int dy = 0; dy < 16; dy++) {
for (int dx = 0; dx < 16; dx++) {
int px = x + dx, py = y + dy;
if (px >= 0 && px < screen_width && py >= 0 && py < screen_height) {
cursor.background[dy][dx] = get_pixel(px, py);
}
}
}
}
/* Restore screen area from saved background */
void cursor_restore_background(void) {
int x = cursor.saved_x, y = cursor.saved_y;
for (int dy = 0; dy < 16; dy++) {
for (int dx = 0; dx < 16; dx++) {
int px = x + dx, py = y + dy;
if (px >= 0 && px < screen_width && py >= 0 && py < screen_height) {
put_pixel(px, py, cursor.background[dy][dx]);
}
}
}
}
/* Draw cursor at position */
void cursor_draw(int x, int y) {
cursor_save_background(x, y);
for (int dy = 0; dy < 16; dy++) {
for (int dx = 0; dx < 16; dx++) {
uint8_t pixel = cursor_arrow[dy][dx];
if (pixel == 0) continue; // Transparent
int px = x + dx, py = y + dy;
if (px >= 0 && px < screen_width && py >= 0 && py < screen_height) {
uint32_t color = (pixel == 1) ? 0x000000 : 0xFFFFFF;
put_pixel(px, py, color);
}
}
}
}
/* Update cursor position (called from mouse handler) */
void cursor_move(int new_x, int new_y) {
cursor_restore_background();
cursor_draw(new_x, new_y);
}
Double Buffering Integration: With double buffering from Phase 13, cursor handling becomes simpler—just redraw the entire scene including cursor each frame, then flip buffers. No save/restore needed!
APIC Timer
The Local APIC Timer is a per-CPU timer built into every x86 processor (from Pentium onwards). Unlike the PIT or HPET which are system-wide, each CPU has its own APIC timer—perfect for SMP scheduling!
Local APIC
The Local APIC (Advanced Programmable Interrupt Controller) is part of the CPU itself. It handles interrupts locally and includes a built-in timer:
╔═════════════════════════════════════════════════════════════════════════════╗
║ LOCAL APIC ARCHITECTURE ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ║
║ CPU 0 CPU 1 ║
║ ┌────────────────────────┐ ┌────────────────────────┐ ║
║ │ CPU Core │ │ CPU Core │ ║
║ ├────────────────────────┤ ├────────────────────────┤ ║
║ │ Local APIC │ │ Local APIC │ ║
║ │ ┌───────────────────┐ │ │ ┌───────────────────┐ │ ║
║ │ │ APIC Timer │ │ │ │ APIC Timer │ │ ║
║ │ │ • Initial Count │ │ │ │ • Initial Count │ │ ║
║ │ │ • Current Count │ │ │ │ • Current Count │ │ ║
║ │ │ • Divider Config │ │ │ │ • Divider Config │ │ ║
║ │ │ • LVT Entry │ │ │ │ • LVT Entry │ │ ║
║ │ └───────────────────┘ │ │ └───────────────────┘ │ ║
║ │ │ Interrupt Handler │ │ │ │ Interrupt Handler │ │ ║
║ │ │ IPI Send/Receive │ │ │ │ IPI Send/Receive │ │ ║
║ └────────────┬───────────┘ └────────────┬───────────┘ ║
║ │ │ ║
║ └──────────────┬───────────────────────┘ ║
║ │ ║
║ ┌────────┴────────┐ ║
║ │ I/O APIC │ Routes external IRQs ║
║ │ (Motherboard) │ to CPU Local APICs ║
║ └─────────────────┘ ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════╝
The Local APIC is memory-mapped (default base: 0xFEE00000). Key timer-related registers:
/* Local APIC Registers (relative to base) */
#define LAPIC_ID 0x020 // APIC ID
#define LAPIC_TPR 0x080 // Task Priority
#define LAPIC_EOI 0x0B0 // End of Interrupt
#define LAPIC_SVR 0x0F0 // Spurious Vector
#define LAPIC_TIMER_LVT 0x320 // Timer Local Vector Table
#define LAPIC_TIMER_ICR 0x380 // Timer Initial Count
#define LAPIC_TIMER_CCR 0x390 // Timer Current Count
#define LAPIC_TIMER_DCR 0x3E0 // Timer Divide Configuration
volatile uint32_t* lapic_base = NULL;
/* Read LAPIC register */
static inline uint32_t lapic_read(uint32_t reg) {
return lapic_base[reg / 4];
}
/* Write LAPIC register */
static inline void lapic_write(uint32_t reg, uint32_t val) {
lapic_base[reg / 4] = val;
}
The Timer LVT (Local Vector Table) register controls timer behavior:
╔═════════════════════════════════════════════════════════════════════════════╗
║ APIC TIMER LVT REGISTER (0x320) ║
╠═════════════════════════════════════════════════════════════════════════════╣
║ ║
║ ┌────┬────┬────────┬───────┬─────────────────────────────────────────┐ ║
║ │Rsvd│Mode │ Mask │ Status │ Vector (bits 0-7) │ ║
║ └────┴────┴────────┴───────┴─────────────────────────────────────────┘ ║
║ 31-19 17-18 16 12 7-0 ║
║ ║
║ Timer Mode (bits 17-18): ║
║ 00 = One-shot (fire once, then stop) ║
║ 01 = Periodic (auto-reload Initial Count) ║
║ 10 = TSC-Deadline (fire when TSC reaches value) ║
║ ║
║ Mask (bit 16): ║
║ 0 = Interrupt enabled ║
║ 1 = Interrupt disabled (masked) ║
║ ║
║ Vector (bits 0-7): IDT entry to invoke (32-255) ║
║ ║
╚═════════════════════════════════════════════════════════════════════════════╝
Calibration
Unlike the PIT which runs at a known frequency (1.193182 MHz), the APIC timer's frequency depends on the CPU bus speed. We must calibrate it using a known time source:
Calibration Strategy: Start the APIC timer, wait a known time using PIT (or HPET), then calculate how many APIC ticks elapsed. This tells us the timer's actual frequency.
static uint32_t lapic_ticks_per_ms; // Calibrated value
/* Calibrate APIC timer using PIT */
void lapic_timer_calibrate(void) {
// Set divider to 16
lapic_write(LAPIC_TIMER_DCR, 0x03);
// Set initial count to max
lapic_write(LAPIC_TIMER_ICR, 0xFFFFFFFF);
// Wait 10ms using PIT
pit_wait_ms(10);
// Stop timer
lapic_write(LAPIC_TIMER_LVT, 0x10000);
// Calculate ticks per ms
uint32_t elapsed = 0xFFFFFFFF - lapic_read(LAPIC_TIMER_CCR);
lapic_ticks_per_ms = elapsed / 10;
// Set up periodic timer (1ms interval)
lapic_write(LAPIC_TIMER_LVT, 32 | 0x20000); // Vector 32, periodic
lapic_write(LAPIC_TIMER_DCR, 0x03); // Divide by 16
lapic_write(LAPIC_TIMER_ICR, lapic_ticks_per_ms); // 1ms interval
}
/* Helper: Wait using PIT Channel 2 */
void pit_wait_ms(uint32_t ms) {
// PIT frequency = 1193182 Hz
// For ms milliseconds, need (1193182 / 1000) * ms ticks
uint32_t ticks = (1193182 / 1000) * ms;
// Configure PIT channel 2 for one-shot
outb(0x61, (inb(0x61) & 0xFD) | 1); // Enable gate
outb(0x43, 0xB2); // Channel 2, mode 0
outb(0x42, ticks & 0xFF);
outb(0x42, ticks >> 8);
// Wait for output to go high
while (!(inb(0x61) & 0x20));
}
For SMP systems, each CPU needs its own timer. The APIC timer is perfect because each CPU's Local APIC has its own timer:
/* Initialize APIC timer on current CPU */
void lapic_timer_init_this_cpu(void) {
// First CPU calibrates; others use the same value
static bool calibrated = false;
if (!calibrated) {
lapic_timer_calibrate();
calibrated = true;
}
// Set up periodic 1ms timer on this CPU
lapic_write(LAPIC_TIMER_LVT, TIMER_VECTOR | 0x20000); // Periodic
lapic_write(LAPIC_TIMER_DCR, 0x03); // Divide by 16
lapic_write(LAPIC_TIMER_ICR, lapic_ticks_per_ms); // 1ms interval
}
/* Timer interrupt handler (runs on each CPU) */
void timer_handler(void) {
// Update per-CPU tick counter
cpu_local_t* cpu = get_cpu_local();
cpu->ticks++;
// Check if current process time slice expired
if (current_process && --current_process->time_left == 0) {
current_process->time_left = TIME_SLICE;
schedule(); // Switch to next process
}
lapic_write(LAPIC_EOI, 0); // End of interrupt
}
SMP Note: When using APIC timer for scheduling, remember to calibrate before bringing up other CPUs. TSC-deadline mode (modern CPUs) can avoid calibration entirely!
What You Can Build
Phase 14 Project: A fully interactive graphical OS! Your system now has mouse support for clicking windows, dragging, and GUI interaction. High-precision timers enable smooth animations and accurate scheduling.
Here's a complete demo that brings together mouse input and precision timing to create an interactive drawing application:
/* interactive_demo.c - Mouse + Timing Demo */
#include "mouse.h"
#include "hpet.h"
#include "graphics.h"
#define CANVAS_X 50
#define CANVAS_Y 50
#define CANVAS_W 700
#define CANVAS_H 500
typedef struct {
uint32_t fg_color;
uint32_t bg_color;
int brush_size;
bool drawing;
uint64_t last_frame_time;
uint32_t frame_count;
uint32_t fps;
} app_state_t;
app_state_t app = {
.fg_color = 0x000000,
.bg_color = 0xFFFFFF,
.brush_size = 3,
.drawing = false
};
/* Color palette */
uint32_t palette[] = {
0x000000, 0xFF0000, 0x00FF00, 0x0000FF,
0xFFFF00, 0xFF00FF, 0x00FFFF, 0xFFFFFF
};
void draw_toolbar(void) {
// Draw palette
for (int i = 0; i < 8; i++) {
int x = 10 + i * 35;
fill_rect(x, 10, 30, 30, palette[i]);
draw_rect(x, 10, 30, 30, 0x000000);
}
// Draw brush size indicator
draw_printf(320, 15, 0x000000, "Brush: %d", app.brush_size);
// Draw FPS counter
draw_printf(450, 15, 0x008000, "FPS: %d", app.fps);
// Draw canvas border
draw_rect(CANVAS_X - 1, CANVAS_Y - 1, CANVAS_W + 2, CANVAS_H + 2, 0x000000);
}
void handle_mouse_click(int x, int y) {
// Check palette clicks
for (int i = 0; i < 8; i++) {
int px = 10 + i * 35;
if (x >= px && x < px + 30 && y >= 10 && y < 40) {
app.fg_color = palette[i];
return;
}
}
}
void draw_brush(int x, int y) {
// Only draw inside canvas
if (x < CANVAS_X || x >= CANVAS_X + CANVAS_W ||
y < CANVAS_Y || y >= CANVAS_Y + CANVAS_H) {
return;
}
// Draw filled circle at cursor
fill_circle(x, y, app.brush_size, app.fg_color);
}
void update_fps(void) {
uint64_t now = hpet_get_ns();
app.frame_count++;
// Update FPS every second
if (now - app.last_frame_time >= 1000000000ULL) {
app.fps = app.frame_count;
app.frame_count = 0;
app.last_frame_time = now;
}
}
void interactive_demo(void) {
// Initialize subsystems
graphics_init();
mouse_init();
hpet_init_from_acpi();
// Clear canvas
fill_rect(CANVAS_X, CANVAS_Y, CANVAS_W, CANVAS_H, app.bg_color);
draw_toolbar();
app.last_frame_time = hpet_get_ns();
while (1) {
// Process mouse state
int mx = mouse.x, my = mouse.y;
uint8_t buttons = mouse.buttons;
// Handle left button for drawing
if (buttons & 0x01) {
if (!app.drawing) {
handle_mouse_click(mx, my);
app.drawing = true;
}
draw_brush(mx, my);
} else {
app.drawing = false;
}
// Handle right button for eraser
if (buttons & 0x02) {
uint32_t old_color = app.fg_color;
app.fg_color = app.bg_color;
draw_brush(mx, my);
app.fg_color = old_color;
}
// Handle middle button for brush size
static bool mid_pressed = false;
if ((buttons & 0x04) && !mid_pressed) {
app.brush_size = (app.brush_size % 10) + 1;
mid_pressed = true;
draw_toolbar();
} else if (!(buttons & 0x04)) {
mid_pressed = false;
}
// Draw cursor
draw_rect(mx - 1, my - 1, 3, 3, 0xFF0000);
// Update display
gfx_flip();
update_fps();
// Cap frame rate to 60 FPS (~16.7ms per frame)
hpet_sleep_us(16667);
}
}
Exercises
Exercise 1: Scroll Wheel Support
Input
Medium
Extend the PS/2 mouse driver to support scroll wheel (IntelliMouse protocol):
- Send the "magic sequence" (set sample rate: 200, 100, 80) to enable scroll wheel
- Mouse packets become 4 bytes (byte 3 = scroll delta)
- Handle the Z-axis data in your mouse handler
- Test by zooming in/out on the canvas
// Enable IntelliMouse scroll wheel
void mouse_enable_scroll(void) {
mouse_set_sample_rate(200);
mouse_set_sample_rate(100);
mouse_set_sample_rate(80);
// Now packets are 4 bytes with scroll in byte 3
}
Exercise 2: Animation System
Timing
Medium
Create a simple animation system using HPET for timing:
- Define an
animation_t struct with start_time, duration, start_value, end_value
- Implement
animation_update() to calculate current value based on elapsed time
- Support easing functions (linear, ease-in, ease-out)
- Animate a window moving across the screen at 60 FPS
// Animation easing functions
float ease_linear(float t) { return t; }
float ease_in_quad(float t) { return t * t; }
float ease_out_quad(float t) { return t * (2 - t); }
Exercise 3: Custom Cursors
Graphics
Easy
Implement context-sensitive cursors:
- Create multiple cursor bitmaps (arrow, hand, resize, text)
- Track which cursor should be used based on mouse position
- Change cursor when over clickable items (hand), resize borders (resize arrows), etc.
- Implement smooth cursor animation (e.g., loading spinner)
Exercise 4: Double-Click Detection
Input + Timing
Medium
Implement double-click detection using HPET:
- Record timestamp of each click using
hpet_get_ns()
- If two clicks are within 500ms and 5 pixels of each other, it's a double-click
- Generate separate events for single-click, double-click, and triple-click
- Test by double-clicking to select words in a text editor
// Double-click detection state
typedef struct {
uint64_t last_click_time;
int last_click_x, last_click_y;
int click_count;
} click_state_t;