mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-26 09:02:46 +00:00
Merge branch 'feat/gui-desktop' into main
Bring the SUB-OS Graphical Desktop Environment (SUB-WM compositor, SUB-WT widget toolkit, icon set, modal dialogs, terminal emulator and the desktop apps) onto main alongside main's userland /sbin/init and framebuffer console. Conflicts resolved: - kernel/main.c: boot into gui_start_desktop() by default (unless nogui/text/ emergency/single), then fall back through fbcon + /sbin/init + shell when the GUI session closes or a text console is requested. Splash raised by default unless a text console is asked for. Kept main's fbcon include and the weave_preempt_selftest path. - Makefile: kept main's userland init compilation rules and kernel/elf.c, kernel/exec.c in CORE_SRCS; retained the feature branch's gui/ sources. - README.md: kept main's Weave Scheduler section and adopted the feature branch's Boot Logo / Console Font / Framebuffer Console / GUI Desktop docs. Verified: full x86_64 build links cleanly (kernel.elf + SUB-OS.img). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
This commit is contained in:
commit
25c0519c25
28 files changed
+5344
-33
No files matched your search
@@ -279,11 +279,14 @@ CONFIG_SRCS-y += drivers/char/tty.c drivers/char/keyboard.c drivers/char/serial.
|
||||
drivers/input/mouse.c drivers/power/acpi.c drivers/power/cpufreq.c drivers/rtc/rtc.c drivers/sound/speaker.c \
|
||||
drivers/net/e1000e.c drivers/virtio/virtio_gpu.c drivers/virtio/virtio_input.c
|
||||
|
||||
# Userland
|
||||
# Userland & GUI Desktop Subsystem
|
||||
ifeq ($(CONFIG_USERLAND_LAZYBOX), y)
|
||||
CONFIG_SRCS-y += userland/lazybox/lazybox.c userland/lazybox/shell.c userland/lazybox/sh.c userland/lazybox/nano.c userland/lazybox/snake.c userland/lazybox/tree.c userland/lazybox/coreutils.c
|
||||
endif
|
||||
|
||||
CONFIG_SRCS-y += gui/gui.c gui/gui_gfx.c gui/gui_cursor.c gui/gui_wm.c gui/gui_desktop.c gui/gui_apps.c \
|
||||
gui/gui_widgets.c gui/gui_icons.c gui/gui_dialog.c gui/gui_apps_ext.c gui/gui_terminal.c
|
||||
|
||||
# Deduplicate
|
||||
CONFIG_SRCS-y := $(sort $(CONFIG_SRCS-y))
|
||||
|
||||
@@ -456,11 +459,34 @@ run-gui: $(TARGET)
|
||||
ifeq ($(ARCH), x86_64)
|
||||
qemu-system-x86_64 -drive format=raw,file=$(IMAGE) \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80 \
|
||||
-device e1000,netdev=net0 -serial stdio
|
||||
-device e1000,netdev=net0 $(QEMU_VGA) -display gtk,zoom-to-fit=on -serial stdio
|
||||
else ifneq ($(filter $(ARCH), aarch64 arm64),)
|
||||
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 128M -kernel $(BUILD_DIR)/kernel.elf -serial stdio
|
||||
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 128M -kernel $(BUILD_DIR)/kernel.elf -device virtio-gpu-pci -serial stdio
|
||||
else
|
||||
qemu-system-arm -M virt -cpu cortex-a15 -m 128M -kernel $(BUILD_DIR)/kernel.elf -serial stdio
|
||||
qemu-system-arm -M virt -cpu cortex-a15 -m 128M -kernel $(BUILD_DIR)/kernel.elf -device virtio-gpu-pci -serial stdio
|
||||
endif
|
||||
|
||||
# Fullscreen without distortion: zoom-to-fit scales the guest while preserving
|
||||
# its aspect ratio, letterboxing rather than stretching on a wider panel.
|
||||
run-fullscreen: $(TARGET)
|
||||
ifeq ($(ARCH), x86_64)
|
||||
qemu-system-x86_64 -drive format=raw,file=$(IMAGE) \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80 \
|
||||
-device e1000,netdev=net0 $(QEMU_VGA) \
|
||||
-display gtk,zoom-to-fit=on,show-cursor=on -full-screen -serial stdio
|
||||
else
|
||||
@echo "run-fullscreen targets x86_64 only"
|
||||
endif
|
||||
|
||||
run-vnc: $(TARGET)
|
||||
ifeq ($(ARCH), x86_64)
|
||||
@echo "=== [QEMU VNC Server running at localhost:5900 (VNC Display :0)] ==="
|
||||
qemu-system-x86_64 -drive format=raw,file=$(IMAGE) \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80 \
|
||||
-device e1000,netdev=net0 $(QEMU_VGA) -vnc :0 -serial stdio
|
||||
else
|
||||
@echo "=== [QEMU VNC Server running at localhost:5900 (VNC Display :0)] ==="
|
||||
qemu-system-aarch64 -M virt -cpu cortex-a57 -m 128M -kernel $(BUILD_DIR)/kernel.elf -device virtio-gpu-pci -vnc :0 -serial stdio
|
||||
endif
|
||||
|
||||
run-server: $(IMAGE)
|
||||
|
||||
@@ -27,16 +27,6 @@
|
||||
|
||||
## 🌟 Key Architecture & Capabilities
|
||||
|
||||
- 🎨 **Boot Logo & Splash Screen (`init/bootlogo.c`, `scripts/mklogo.py`)**:
|
||||
- **ASCII rendition** on the text console and serial line from the first moments of boot, before any display driver exists.
|
||||
- **Optional graphical splash**: pass `splash` on the kernel command line to show the bitmap logo with an animated progress bar once the framebuffer is up. It is off by default on this branch so the text console stays visible; `splashtime=<ms>` tunes how long it is held.
|
||||
- **4-bit palette-indexed asset**: 160x160 pixels in 12.5 KB rather than 100 KB, with a transparency mask so the mark composites onto the background instead of a flat rectangle.
|
||||
- **Regenerable**: `python3 scripts/mklogo.py path/to/logo.png` rewrites `init/logo_data.c` from source artwork.
|
||||
- 🔤 **Shared 8x8 Console Font (`lib/font8x8.c`)**: one glyph table behind the framebuffer console and the 2D canvas rasterizer, replacing divergent private copies.
|
||||
- 🖵 **Framebuffer Console (`drivers/video/fbcon.c`)**:
|
||||
- Once the VBE adapter is in graphics mode the VGA text buffer stops being scanned out, so the console would be invisible on the display even though it still reaches serial. fbcon renders the console into the framebuffer instead, the way Linux fbcon does.
|
||||
- **VT100 subset**: SGR colours, cursor addressing, relative moves, erase-display and erase-line, save/restore — enough for full-screen curses-style programs such as the **nano** editor to render correctly.
|
||||
- Presents a fixed **80x25** grid (matching `TTY_WIDTH`/`TTY_HEIGHT`, which the userland assumes) with the glyph cell scaled to fill whatever resolution is active. `video=WIDTHxHEIGHT` on the kernel command line selects the mode, defaulting to `1280x720`.
|
||||
- 🧶 **Weave Scheduler & Real Context Switching (`kernel/sched.c`, `arch/*/cpu/switch.*`)**:
|
||||
- **Genuine multitasking, not a state relabel**: `sub_ctx_switch()` is per-architecture assembly (x86_64/aarch64/armv8i) that saves and restores the callee-saved register set plus flags on each thread's own kernel stack, with a first-run trampoline that launches a freshly forged thread frame.
|
||||
- **Tiered rotation with behavioural weaving**: runnable threads thread through `WEAVE_NLANES` priority lanes; the dispatcher always pulls the lowest non-empty lane round-robin. A thread that yields voluntarily is *woven up* toward low latency, one that burns its whole quantum and is preempted is *woven down* toward throughput — the lane floats on observed behaviour, not a static nice value.
|
||||
@@ -55,6 +45,26 @@
|
||||
- 🧪 **In-Kernel Test Harness (`kernel/ktest.c`)**:
|
||||
- KUnit-style suites with assertion macros, per-case pass/fail reporting and timing.
|
||||
- **6 built-in suites, 21 cases, 735 assertions** covering rbtree, kfifo, hashtable, RCU, futex, wait queues, page cache and libcore — runnable from the shell (`ktest`) or the desktop KTest Runner.
|
||||
- 🎨 **Boot Logo & Splash Screen (`init/bootlogo.c`, `scripts/mklogo.py`)**:
|
||||
- **Two presentations of one artwork**: an ASCII rendition on the text console and serial line from the first moments of boot, then the real bitmap as a graphical splash once the video driver is up at step [5/14].
|
||||
- **4-bit palette-indexed asset**: 160x160 pixels in 12.5 KB rather than 100 KB, with a transparency mask so the mark composites onto the gradient instead of a flat rectangle.
|
||||
- **Animated progress bar** tied to the real boot stages, held for a minimum duration (`splashtime=<ms>`) so a sub-second boot still shows it; `nosplash`, `nogui` or `text` skip it.
|
||||
- **Regenerable**: `python3 scripts/mklogo.py path/to/logo.png` rewrites `init/logo_data.c` from source artwork.
|
||||
- 🔤 **Shared 8x8 Console Font (`lib/font8x8.c`)**: one glyph table behind the framebuffer console, the 2D canvas rasterizer and the desktop compositor, replacing three divergent private copies.
|
||||
- 🖵 **Framebuffer Console (`drivers/video/fbcon.c`)**:
|
||||
- Once the VBE adapter is in graphics mode the VGA text buffer stops being scanned out, so anything printed afterwards is invisible on the display even though it still reaches serial. fbcon renders the console into the framebuffer instead, the way Linux fbcon does.
|
||||
- **VT100 subset**: SGR colours, cursor addressing (`H`/`f`), relative moves, erase-display and erase-line, save/restore — enough for full-screen curses-style programs such as the **nano** editor to render correctly.
|
||||
- Presents a fixed **80x25** grid (matching `TTY_WIDTH`/`TTY_HEIGHT`, which the userland assumes) with the glyph cell scaled to fill whatever resolution is active.
|
||||
- The TTY driver dispatches `tty_write`, `tty_clear` and `tty_set_cursor` to it, so leaving the desktop with `Esc` drops to a working console **inside the emulator**, not just on the host serial line.
|
||||
- 🖥️ **SUB-OS Graphical Desktop Environment (`gui/`)**:
|
||||
- **SUB-WM Compositor**: 800x600 TrueColor double-buffered window manager with drag, corner resize, minimize/maximize/restore, edge snapping (left/right/maximize), tile and cascade layouts, and z-ordered painter's-algorithm compositing.
|
||||
- **SUB-WT Widget Toolkit (`gui/gui_widgets.c`)**: Immediate-mode controls -- buttons, labels, checkboxes, radios, sliders, list boxes, text fields, tab bars, scrollbars, progress bars, sparklines and badges -- so an app rebuilds its interface from state each frame instead of maintaining a retained widget tree.
|
||||
- **Icon Set (`gui/gui_icons.c`)**: 14 hand-drawn 16x16 palette-indexed glyphs, integer-scalable and tintable, driving the desktop grid, start menu and window chrome.
|
||||
- **Modal Dialog Layer (`gui/gui_dialog.c`)**: Info, warning, error and confirm dialogs that dim the desktop and take exclusive input, with word-wrapped messages and callback results.
|
||||
- **Desktop Shell (`gui/gui_desktop.c`)**: Gradient wallpaper with grid overlay, launcher icon grid, start menu, right-click context menu (tile / cascade / toggle grid), and a taskbar with per-window buttons, live CPU trace, memory badge and RTC clock.
|
||||
- **Terminal Emulator (`gui/gui_terminal.c`)**: dispatches to the real LazyBox applet table and the shared shell builtins, capturing each command's `printk` output through a console sink. ANSI colours are translated per line, CP437 box and block characters fall back to ASCII, and there is 256 lines of scrollback plus command history.
|
||||
- **Applications**: Terminal, File Explorer, System Monitor, Task Manager, Kernel Log viewer (live dmesg with severity colouring and scrollback), KTest Runner (drives the in-kernel suites and reports pass/fail), Text Editor, Calculator, Paint Studio, Clock & Calendar, Settings and About.
|
||||
- **Keyboard Shortcuts**: `F5` cycle focus, `F6` cascade, `F7` tile, `F8` maximize, `F9` minimize, `Esc` exit to the kernel TTY.
|
||||
- ⚡ **Freestanding C++17 OOP Kernel Engine (`kernel/cpp/`)**:
|
||||
- **Bare-Metal C++ Runtime**: `operator new`/`delete`, sized deallocation, placement `new`, pure virtual handlers (`__cxa_pure_virtual`), and `.init_array` global constructor dispatcher.
|
||||
- **Modern Generic Containers**: Pure freestanding `Vector<T>`, `UniquePtr<T>` (with polymorphic converting constructors and custom deleters), dynamic `String`, and move semantics (`kernel::move`, `kernel::forward`).
|
||||
@@ -224,9 +234,20 @@ make ARCH=armv8i
|
||||
make run ARCH=armv8i
|
||||
```
|
||||
|
||||
#### Graphical Window Emulation (Optional SDL/GTK GUI)
|
||||
#### Graphical Desktop Environment
|
||||
SUB-OS boots straight into the desktop. Pass `nogui`, `text`, `emergency` or
|
||||
`single` on the kernel command line to go directly to the kernel TTY instead;
|
||||
from inside the desktop, `Esc` (or Start -> Exit to Kernel TTY) drops to the
|
||||
same shell.
|
||||
|
||||
The display mode is set with `video=WIDTHxHEIGHT` on the kernel command line
|
||||
(default `1280x720`, 16:9). Both the desktop and the framebuffer console adapt
|
||||
to whatever mode is active.
|
||||
|
||||
```bash
|
||||
make run-gui
|
||||
make run-gui # Windowed, aspect-preserving scaling
|
||||
make run-fullscreen # Fullscreen without stretching
|
||||
make run-vnc # Headless: VNC server on localhost:5900
|
||||
```
|
||||
|
||||
---
|
||||
@@ -244,7 +265,7 @@ make run-gui
|
||||
| **Entertainment & Games** | `snake`, `matrix`, `calc`, `vtart` |
|
||||
| **Kernel & Tracing** | `lsmod`, `insmod`, `rmmod`, `slabinfo`, `trace`, `unshare`, `io_uring_test` |
|
||||
| **Diagnostics & Metrics**| `neofetch`, `uname`, `free`, `uptime`, `top`, `htop`, `ps`, `dmesg`, `vmstat`, `iostat` |
|
||||
| **System Control** | `clear`, `help`, `sleep`, `reboot`, `shutdown`, `poweroff`, `tty` |
|
||||
| **System Control & GUI** | `clear`, `help`, `sleep`, `reboot`, `shutdown`, `poweroff`, `tty`, `startx`, `gui`, `desktop` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+150
-6
@@ -1,15 +1,157 @@
|
||||
// PS/2 Mouse Controller & Interrupt Driver for SUB-OS
|
||||
#include <drivers/mouse.h>
|
||||
#include <arch/x86_64/io.h>
|
||||
#include <arch/x86_64/isr.h>
|
||||
#include <arch/x86_64/pic.h>
|
||||
#include <kernel/printk.h>
|
||||
|
||||
static mouse_state_t mouse_state = {0, 0, false, false, false};
|
||||
static int mouse_bounds_x = 80;
|
||||
static int mouse_bounds_y = 25;
|
||||
#define MOUSE_PORT_DATA 0x60
|
||||
#define MOUSE_PORT_STATUS 0x64
|
||||
#define MOUSE_PORT_CMD 0x64
|
||||
|
||||
#define MOUSE_CMD_ENABLE_AUX 0xA8
|
||||
#define MOUSE_CMD_GET_BYTE 0x20
|
||||
#define MOUSE_CMD_SET_BYTE 0x60
|
||||
#define MOUSE_CMD_WRITE_AUX 0xD4
|
||||
|
||||
#define MOUSE_CMD_SET_DEFAULTS 0xF6
|
||||
#define MOUSE_CMD_ENABLE_DATA 0xF4
|
||||
#define MOUSE_CMD_SET_SAMPLE 0xF3
|
||||
|
||||
static mouse_state_t mouse_state = {400, 300, false, false, false};
|
||||
static int mouse_bounds_x = 800;
|
||||
static int mouse_bounds_y = 600;
|
||||
|
||||
static uint8_t mouse_cycle = 0;
|
||||
static uint8_t mouse_packet[3];
|
||||
|
||||
static inline void mouse_wait_write(void) {
|
||||
uint32_t timeout = 100000;
|
||||
while ((inb(MOUSE_PORT_STATUS) & 0x02) && timeout--) {
|
||||
io_wait();
|
||||
}
|
||||
}
|
||||
|
||||
static inline void mouse_wait_read(void) {
|
||||
uint32_t timeout = 100000;
|
||||
while (!(inb(MOUSE_PORT_STATUS) & 0x01) && timeout--) {
|
||||
io_wait();
|
||||
}
|
||||
}
|
||||
|
||||
static void mouse_write_cmd(uint8_t cmd) {
|
||||
mouse_wait_write();
|
||||
outb(MOUSE_PORT_CMD, cmd);
|
||||
}
|
||||
|
||||
static void mouse_write_data(uint8_t data) {
|
||||
mouse_wait_write();
|
||||
outb(MOUSE_PORT_CMD, MOUSE_CMD_WRITE_AUX);
|
||||
mouse_wait_write();
|
||||
outb(MOUSE_PORT_DATA, data);
|
||||
}
|
||||
|
||||
static uint8_t mouse_read_data(void) {
|
||||
mouse_wait_read();
|
||||
return inb(MOUSE_PORT_DATA);
|
||||
}
|
||||
|
||||
static void mouse_interrupt_handler(registers_t* regs) {
|
||||
(void)regs;
|
||||
uint8_t status = inb(MOUSE_PORT_STATUS);
|
||||
if (!(status & 0x20)) {
|
||||
// Not mouse data
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t data = inb(MOUSE_PORT_DATA);
|
||||
|
||||
switch (mouse_cycle) {
|
||||
case 0:
|
||||
// Byte 0 must have bit 3 set to 1 in standard PS/2 packet
|
||||
if ((data & 0x08) == 0x08) {
|
||||
mouse_packet[0] = data;
|
||||
mouse_cycle = 1;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
mouse_packet[1] = data;
|
||||
mouse_cycle = 2;
|
||||
break;
|
||||
case 2:
|
||||
mouse_packet[2] = data;
|
||||
mouse_cycle = 0;
|
||||
|
||||
// Process Packet
|
||||
uint8_t flags = mouse_packet[0];
|
||||
int32_t dx = (int32_t)mouse_packet[1];
|
||||
int32_t dy = (int32_t)mouse_packet[2];
|
||||
|
||||
// Sign extension
|
||||
if (flags & 0x10) dx |= 0xFFFFFF00;
|
||||
if (flags & 0x20) dy |= 0xFFFFFF00;
|
||||
|
||||
// Check overflow
|
||||
if (flags & 0xC0) {
|
||||
// Discard invalid/overflow packet
|
||||
break;
|
||||
}
|
||||
|
||||
// Update Coordinates (PS/2 Y delta is inverted)
|
||||
mouse_state.x += dx;
|
||||
mouse_state.y -= dy;
|
||||
|
||||
// Clamp coordinates to screen bounds
|
||||
if (mouse_state.x < 0) mouse_state.x = 0;
|
||||
if (mouse_state.y < 0) mouse_state.y = 0;
|
||||
if (mouse_state.x >= mouse_bounds_x) mouse_state.x = mouse_bounds_x - 1;
|
||||
if (mouse_state.y >= mouse_bounds_y) mouse_state.y = mouse_bounds_y - 1;
|
||||
|
||||
// Update button states
|
||||
mouse_state.left_btn = (flags & 0x01) != 0;
|
||||
mouse_state.right_btn = (flags & 0x02) != 0;
|
||||
mouse_state.middle_btn = (flags & 0x04) != 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void mouse_init(void) {
|
||||
#if defined(__x86_64__)
|
||||
// 1. Enable Auxiliary Device
|
||||
mouse_write_cmd(MOUSE_CMD_ENABLE_AUX);
|
||||
|
||||
// 2. Read Controller Command Byte
|
||||
mouse_write_cmd(MOUSE_CMD_GET_BYTE);
|
||||
uint8_t status = mouse_read_data();
|
||||
|
||||
// 3. Enable IRQ12 (Bit 1) & Enable Mouse Clock (Clear Bit 5)
|
||||
status |= 0x02;
|
||||
status &= ~0x20;
|
||||
mouse_write_cmd(MOUSE_CMD_SET_BYTE);
|
||||
mouse_wait_write();
|
||||
outb(MOUSE_PORT_DATA, status);
|
||||
|
||||
// 4. Set Defaults & Enable Streaming
|
||||
mouse_write_data(MOUSE_CMD_SET_DEFAULTS);
|
||||
mouse_read_data(); // ACK (0xFA)
|
||||
|
||||
mouse_write_data(MOUSE_CMD_ENABLE_DATA);
|
||||
mouse_read_data(); // ACK (0xFA)
|
||||
|
||||
// 5. Register IRQ 12 Handler (Vector 44 = 32 + 12)
|
||||
isr_register_handler(44, mouse_interrupt_handler);
|
||||
|
||||
// 6. Unmask PIC IRQ 12
|
||||
pic_clear_mask(12);
|
||||
#endif
|
||||
|
||||
mouse_state.x = mouse_bounds_x / 2;
|
||||
mouse_state.y = mouse_bounds_y / 2;
|
||||
printk(KERN_INFO "MOUSE: PS/2 Mouse Controller initialized (Bounds: %dx%d)\n",
|
||||
mouse_state.left_btn = false;
|
||||
mouse_state.right_btn = false;
|
||||
mouse_state.middle_btn = false;
|
||||
|
||||
printk(KERN_INFO "MOUSE: PS/2 Mouse Controller initialized (IRQ 12, Bounds: %dx%d)\n",
|
||||
mouse_bounds_x, mouse_bounds_y);
|
||||
}
|
||||
|
||||
@@ -18,6 +160,8 @@ const mouse_state_t* mouse_get_state(void) {
|
||||
}
|
||||
|
||||
void mouse_set_bounds(int max_x, int max_y) {
|
||||
mouse_bounds_x = max_x;
|
||||
mouse_bounds_y = max_y;
|
||||
mouse_bounds_x = (max_x > 0) ? max_x : 800;
|
||||
mouse_bounds_y = (max_y > 0) ? max_y : 600;
|
||||
if (mouse_state.x >= mouse_bounds_x) mouse_state.x = mouse_bounds_x / 2;
|
||||
if (mouse_state.y >= mouse_bounds_y) mouse_state.y = mouse_bounds_y / 2;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SUB-OS Graphical User Interface Subsystem
|
||||
#include <gui/gui.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <gui/gui_dialog.h>
|
||||
#include <gui/gui_apps_ext.h>
|
||||
#include <kernel/printk.h>
|
||||
|
||||
void gui_init(void) {
|
||||
gui_icons_init();
|
||||
gui_dialog_init();
|
||||
printk(KERN_INFO "GUI: SUB-OS Desktop Environment ready "
|
||||
"(SUB-WM compositor, SUB-WT widgets, %d icons)\n",
|
||||
GUI_ICON_COUNT);
|
||||
}
|
||||
|
||||
int gui_start_desktop(void) {
|
||||
return gui_desktop_run();
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
// High-Resolution GUI Applications for SUB-OS Desktop (800x600)
|
||||
#include <gui/gui_apps.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <drivers/rtc.h>
|
||||
#include <drivers/cpufreq.h>
|
||||
#include <kernel/tsc.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <mm/pmm.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
|
||||
static int simple_atoi(const char* s) {
|
||||
if (!s) return 0;
|
||||
int res = 0;
|
||||
int sign = 1;
|
||||
if (*s == '-') { sign = -1; s++; }
|
||||
while (*s >= '0' && *s <= '9') {
|
||||
res = res * 10 + (*s - '0');
|
||||
s++;
|
||||
}
|
||||
return res * sign;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 1. System Performance Monitor App (High-Res 320x240)
|
||||
// =================================================================
|
||||
static uint8_t g_cpu_history[60] = {
|
||||
12, 15, 18, 22, 30, 25, 20, 35, 45, 50, 42, 38, 55, 60, 48, 52, 35, 30, 25, 20,
|
||||
28, 32, 45, 60, 55, 40, 30, 25, 20, 18, 22, 25, 15, 18, 22, 30, 35, 40, 38, 42,
|
||||
30, 25, 20, 18, 15, 22, 28, 35, 40, 48, 52, 45, 38, 30, 25, 20, 18, 15, 20, 24
|
||||
};
|
||||
|
||||
static void sysmon_paint(gui_window_t* win) {
|
||||
int cx = win->x + 12;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 10;
|
||||
|
||||
gui_gfx_draw_string_16_shadow(cx, cy, "CPU Load History (Real-Time)", GUI_THEME_PRIMARY, GUI_COLOR_BLACK);
|
||||
|
||||
// CPU Graph Box
|
||||
int graph_x = cx;
|
||||
int graph_y = cy + 20;
|
||||
int graph_w = win->width - 24;
|
||||
int graph_h = 70;
|
||||
gui_gfx_fill_rect(graph_x, graph_y, graph_w, graph_h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(graph_x, graph_y, graph_w, graph_h, GUI_THEME_BORDER);
|
||||
|
||||
// Plot Grid Lines
|
||||
for (int gy = graph_y + 14; gy < graph_y + graph_h; gy += 14) {
|
||||
gui_gfx_draw_line(graph_x + 1, gy, graph_x + graph_w - 2, gy, 0xFF1E293B);
|
||||
}
|
||||
for (int gx = graph_x + 30; gx < graph_x + graph_w; gx += 30) {
|
||||
gui_gfx_draw_line(gx, graph_y + 1, gx, graph_y + graph_h - 2, 0xFF1E293B);
|
||||
}
|
||||
|
||||
// Plot Waveform
|
||||
for (int i = 0; i < 59 && (i * 5 + 5) < graph_w; i++) {
|
||||
int y1 = graph_y + graph_h - 3 - (g_cpu_history[i] * (graph_h - 6)) / 100;
|
||||
int y2 = graph_y + graph_h - 3 - (g_cpu_history[i + 1] * (graph_h - 6)) / 100;
|
||||
gui_gfx_draw_line(graph_x + 4 + i * 5, y1, graph_x + 4 + (i + 1) * 5, y2, GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
// Memory Usage Bar
|
||||
int mem_y = graph_y + graph_h + 12;
|
||||
gui_gfx_draw_string_16(cx, mem_y, "RAM Usage: 24 MB / 128 MB (18% Heap)", GUI_THEME_TEXT_MAIN);
|
||||
|
||||
int bar_y = mem_y + 20;
|
||||
int bar_w = win->width - 24;
|
||||
gui_gfx_fill_rect(cx, bar_y, bar_w, 12, GUI_THEME_BG_DARK);
|
||||
gui_gfx_fill_rect(cx, bar_y, (bar_w * 24) / 128, 12, GUI_THEME_ACCENT);
|
||||
gui_gfx_draw_rect(cx, bar_y, bar_w, 12, GUI_THEME_BORDER);
|
||||
|
||||
// System Telemetry Metrics
|
||||
char buf[64];
|
||||
cpufreq_stats_t stats = cpufreq_get_stats();
|
||||
snprintf(buf, sizeof(buf), "Clock: %u.%03u GHz | Governor: ondemand",
|
||||
stats.current_khz / 1000000, (stats.current_khz % 1000000) / 1000);
|
||||
gui_gfx_draw_string(cx, bar_y + 20, buf, GUI_THEME_TEXT_MUTED);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Core Tasks: 6 Active | VFS Cache: 98.4%% Hit", 0);
|
||||
gui_gfx_draw_string(cx, bar_y + 32, buf, GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
|
||||
void gui_app_sysmon_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("System Monitor", x, y, (w > 0) ? w : 330, (h > 0) ? h : 230);
|
||||
if (win) {
|
||||
win->paint = sysmon_paint;
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 3. File Manager Explorer App (High-Res 380x240)
|
||||
// =================================================================
|
||||
static void fileman_paint(gui_window_t* win) {
|
||||
int cx = win->x + 12;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 10;
|
||||
|
||||
// Address Bar
|
||||
gui_gfx_fill_rect(cx, cy, win->width - 24, 22, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(cx, cy, win->width - 24, 22, GUI_THEME_BORDER);
|
||||
gui_gfx_draw_string_16(cx + 8, cy + 3, "Path: /", GUI_THEME_PRIMARY);
|
||||
|
||||
// Separator line
|
||||
int list_y = cy + 30;
|
||||
gui_gfx_draw_line(win->x + 2, list_y, win->x + win->width - 3, list_y, GUI_THEME_BORDER);
|
||||
|
||||
const char* items[] = {
|
||||
"[DIR] bin", "[DIR] etc",
|
||||
"[DIR] dev", "[DIR] proc",
|
||||
"[DIR] sys", "[DIR] home",
|
||||
"[DIR] mnt", "[DIR] usr",
|
||||
"[FILE] kernel.elf", "[FILE] readme.txt",
|
||||
"[FILE] config.h", "[FILE] banner.art"
|
||||
};
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int ix = cx + (i % 2) * 160;
|
||||
int iy = list_y + 10 + (i / 2) * 22;
|
||||
if (iy + 18 > win->y + win->height) break;
|
||||
bool is_dir = (items[i][1] == 'D');
|
||||
uint32_t col = is_dir ? GUI_THEME_ACCENT : GUI_THEME_TEXT_MAIN;
|
||||
gui_gfx_draw_string_16(ix, iy, items[i], col);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_app_fileman_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("File Explorer", x, y, (w > 0) ? w : 360, (h > 0) ? h : 220);
|
||||
if (win) {
|
||||
win->paint = fileman_paint;
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. GUI Calculator App (High-Res 220x240)
|
||||
// =================================================================
|
||||
typedef struct {
|
||||
char display[16];
|
||||
int op1;
|
||||
char op;
|
||||
bool clear_on_digit;
|
||||
} calc_data_t;
|
||||
|
||||
static void calc_paint(gui_window_t* win) {
|
||||
calc_data_t* cd = (calc_data_t*)win->user_data;
|
||||
if (!cd) return;
|
||||
|
||||
int cx = win->x + 12;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 10;
|
||||
|
||||
// Display Screen
|
||||
gui_gfx_fill_rect(cx, cy, win->width - 24, 28, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(cx, cy, win->width - 24, 28, GUI_THEME_BORDER);
|
||||
gui_gfx_draw_string_16(cx + 8, cy + 6, cd->display, GUI_THEME_SUCCESS);
|
||||
|
||||
// Button Grid (4x4)
|
||||
const char* btns[4][4] = {
|
||||
{"7", "8", "9", "/"},
|
||||
{"4", "5", "6", "*"},
|
||||
{"1", "2", "3", "-"},
|
||||
{"C", "0", "=", "+"}
|
||||
};
|
||||
|
||||
int btn_w = (win->width - 36) / 4;
|
||||
int btn_h = 28;
|
||||
int start_y = cy + 38;
|
||||
|
||||
for (int r = 0; r < 4; r++) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int bx = cx + c * (btn_w + 4);
|
||||
int by = start_y + r * (btn_h + 4);
|
||||
uint32_t bg = (c == 3 || (r == 3 && c == 2)) ? GUI_THEME_PRIMARY_DARK : GUI_THEME_BG_ELEVATED;
|
||||
gui_gfx_fill_rect(bx, by, btn_w, btn_h, bg);
|
||||
gui_gfx_draw_rect(bx, by, btn_w, btn_h, GUI_THEME_BORDER);
|
||||
gui_gfx_draw_string_16(bx + (btn_w - 8) / 2, by + 6, btns[r][c], GUI_THEME_TEXT_MAIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void calc_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
calc_data_t* cd = (calc_data_t*)win->user_data;
|
||||
if (!cd) return;
|
||||
|
||||
if (ev->type == GUI_EVENT_MOUSE_DOWN && ev->btn_left) {
|
||||
int btn_w = (win->width - 36) / 4;
|
||||
int btn_h = 28;
|
||||
int start_y = 48; // Relative to client area
|
||||
int rel_y = ev->rel_y - start_y;
|
||||
int rel_x = ev->rel_x - 12;
|
||||
|
||||
if (rel_y >= 0 && rel_x >= 0) {
|
||||
int row = rel_y / (btn_h + 4);
|
||||
int col = rel_x / (btn_w + 4);
|
||||
|
||||
if (row >= 0 && row < 4 && col >= 0 && col < 4) {
|
||||
const char* btns[4][4] = {
|
||||
{"7", "8", "9", "/"},
|
||||
{"4", "5", "6", "*"},
|
||||
{"1", "2", "3", "-"},
|
||||
{"C", "0", "=", "+"}
|
||||
};
|
||||
char val = btns[row][col][0];
|
||||
if (val >= '0' && val <= '9') {
|
||||
if (cd->clear_on_digit || strcmp(cd->display, "0") == 0) {
|
||||
cd->display[0] = val;
|
||||
cd->display[1] = '\0';
|
||||
cd->clear_on_digit = false;
|
||||
} else if (strlen(cd->display) < 10) {
|
||||
int len = strlen(cd->display);
|
||||
cd->display[len] = val;
|
||||
cd->display[len + 1] = '\0';
|
||||
}
|
||||
} else if (val == 'C') {
|
||||
strcpy(cd->display, "0");
|
||||
cd->op1 = 0; cd->op = 0;
|
||||
} else if (val == '=') {
|
||||
int op2 = simple_atoi(cd->display);
|
||||
int res = 0;
|
||||
if (cd->op == '+') res = cd->op1 + op2;
|
||||
else if (cd->op == '-') res = cd->op1 - op2;
|
||||
else if (cd->op == '*') res = cd->op1 * op2;
|
||||
else if (cd->op == '/') res = (op2 != 0) ? (cd->op1 / op2) : 0;
|
||||
snprintf(cd->display, sizeof(cd->display), "%d", res);
|
||||
cd->clear_on_digit = true;
|
||||
} else {
|
||||
cd->op1 = simple_atoi(cd->display);
|
||||
cd->op = val;
|
||||
cd->clear_on_digit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ev->type == GUI_EVENT_CLOSE) {
|
||||
if (win->user_data) {
|
||||
kfree(win->user_data);
|
||||
win->user_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_app_calc_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Calculator", x, y, (w > 0) ? w : 220, (h > 0) ? h : 220);
|
||||
if (win) {
|
||||
calc_data_t* cd = (calc_data_t*)kzalloc(sizeof(calc_data_t));
|
||||
strcpy(cd->display, "0");
|
||||
win->user_data = cd;
|
||||
win->paint = calc_paint;
|
||||
win->handle_event = calc_event;
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 5. Paint Studio App (High-Res 340x240)
|
||||
// =================================================================
|
||||
typedef struct {
|
||||
uint32_t selected_color;
|
||||
} paint_data_t;
|
||||
|
||||
static void paint_app_paint(gui_window_t* win) {
|
||||
paint_data_t* pd = (paint_data_t*)win->user_data;
|
||||
if (!pd) return;
|
||||
|
||||
int cx = win->x + 8;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 8;
|
||||
|
||||
// Palette Color Swatches
|
||||
uint32_t colors[] = {GUI_THEME_PRIMARY, GUI_THEME_SUCCESS, GUI_THEME_WARNING, GUI_THEME_DANGER, 0xFFEC4899, GUI_COLOR_WHITE, GUI_COLOR_BLACK};
|
||||
for (int i = 0; i < 7; i++) {
|
||||
gui_gfx_fill_rect(cx + i * 28, cy, 24, 18, colors[i]);
|
||||
gui_gfx_draw_rect(cx + i * 28, cy, 24, 18, (colors[i] == pd->selected_color) ? GUI_COLOR_WHITE : GUI_THEME_BORDER);
|
||||
}
|
||||
gui_gfx_draw_line(win->x + 2, cy + 24, win->x + win->width - 3, cy + 24, GUI_THEME_BORDER);
|
||||
}
|
||||
|
||||
static void paint_app_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
paint_data_t* pd = (paint_data_t*)win->user_data;
|
||||
if (!pd) return;
|
||||
|
||||
if (ev->btn_left) {
|
||||
if (ev->rel_y <= 24) {
|
||||
uint32_t colors[] = {GUI_THEME_PRIMARY, GUI_THEME_SUCCESS, GUI_THEME_WARNING, GUI_THEME_DANGER, 0xFFEC4899, GUI_COLOR_WHITE, GUI_COLOR_BLACK};
|
||||
int idx = (ev->rel_x - 8) / 28;
|
||||
if (idx >= 0 && idx < 7) {
|
||||
pd->selected_color = colors[idx];
|
||||
}
|
||||
} else {
|
||||
// Draw on canvas with 4x4 brush
|
||||
gui_gfx_fill_rect(win->x + ev->rel_x - 2, win->y + GUI_TITLEBAR_HEIGHT + ev->rel_y - 2, 4, 4, pd->selected_color);
|
||||
}
|
||||
} else if (ev->type == GUI_EVENT_CLOSE) {
|
||||
if (win->user_data) {
|
||||
kfree(win->user_data);
|
||||
win->user_data = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_app_paint_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Paint Studio", x, y, (w > 0) ? w : 340, (h > 0) ? h : 240);
|
||||
if (win) {
|
||||
paint_data_t* pd = (paint_data_t*)kzalloc(sizeof(paint_data_t));
|
||||
pd->selected_color = GUI_THEME_PRIMARY;
|
||||
win->user_data = pd;
|
||||
win->paint = paint_app_paint;
|
||||
win->handle_event = paint_app_event;
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 6. About SUB-OS Dialog App (High-Res 320x180)
|
||||
// =================================================================
|
||||
static void about_paint(gui_window_t* win) {
|
||||
int cx = win->x + 16;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 14;
|
||||
|
||||
gui_gfx_draw_string_16_shadow(cx, cy, "SUB-OS Modern Desktop", GUI_THEME_PRIMARY, GUI_COLOR_BLACK);
|
||||
gui_gfx_draw_string_16(cx, cy + 22, "Version 0.2.0-LTS (x86_64)", GUI_THEME_TEXT_MAIN);
|
||||
gui_gfx_draw_string(cx, cy + 48, "Architecture : Modular Monolithic Kernel", GUI_THEME_TEXT_MUTED);
|
||||
gui_gfx_draw_string(cx, cy + 62, "Languages : Modern C++17, Rust, SUB-Lang", GUI_THEME_SUCCESS);
|
||||
gui_gfx_draw_string(cx, cy + 76, "Author : subhobhai943", GUI_THEME_TEXT_DIM);
|
||||
gui_gfx_draw_string(cx, cy + 90, "Display : 800x600 TrueColor Double-Buffered", GUI_THEME_ACCENT);
|
||||
}
|
||||
|
||||
void gui_app_about_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("About SUB-OS", x, y, (w > 0) ? w : 320, (h > 0) ? h : 160);
|
||||
if (win) {
|
||||
win->paint = about_paint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
// SUB-OS Desktop Applications, second generation
|
||||
//
|
||||
// These apps are written entirely against the SUB-WT immediate-mode toolkit:
|
||||
// each one keeps a small state struct in win->user_data, forwards WM events
|
||||
// into the toolkit, and rebuilds its interface every frame from that state.
|
||||
|
||||
#include <gui/gui_apps_ext.h>
|
||||
#include <gui/gui_widgets.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <gui/gui_dialog.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <kernel/ktest.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <kernel/task.h>
|
||||
#include <kernel/rcu.h>
|
||||
#include <kernel/futex.h>
|
||||
#include <kernel/tsc.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <mm/pmm.h>
|
||||
#include <mm/page_cache.h>
|
||||
#include <drivers/rtc.h>
|
||||
#include <drivers/cpufreq.h>
|
||||
#include <drivers/mouse.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <arch/arch.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
|
||||
// Shared plumbing: every app forwards WM events into the toolkit, and frees
|
||||
// its state struct when the window closes.
|
||||
static void app_forward_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
gui_widget_feed_event(win, ev);
|
||||
if (ev->type == GUI_EVENT_CLOSE && win->user_data) {
|
||||
kfree(win->user_data);
|
||||
win->user_data = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 1. Settings
|
||||
// ===========================================================================
|
||||
typedef struct {
|
||||
int active_tab;
|
||||
bool show_grid;
|
||||
bool window_shadows;
|
||||
bool sound_enabled;
|
||||
bool show_clock_seconds;
|
||||
int theme_choice; // 0 = Nord Dark, 1 = Midnight, 2 = Slate
|
||||
int cursor_speed;
|
||||
int brightness;
|
||||
int governor_choice; // 0 = powersave, 1 = ondemand, 2 = performance
|
||||
} settings_data_t;
|
||||
|
||||
static const char* const settings_tabs[] = { "Appearance", "Input", "Power" };
|
||||
|
||||
static void settings_paint(gui_window_t* win) {
|
||||
settings_data_t* sd = (settings_data_t*)win->user_data;
|
||||
if (!sd) return;
|
||||
|
||||
gui_widget_begin(win);
|
||||
|
||||
int w = gui_widget_client_width();
|
||||
gui_tabbar(1, 6, 6, w - 12, settings_tabs, 3, &sd->active_tab);
|
||||
|
||||
int y = 40;
|
||||
char buf[64];
|
||||
|
||||
if (sd->active_tab == 0) {
|
||||
gui_label_bold(10, y, "Desktop Appearance", GUI_THEME_PRIMARY);
|
||||
y += 24;
|
||||
gui_checkbox(10, 10, y, "Show wallpaper grid overlay", &sd->show_grid);
|
||||
y += 22;
|
||||
gui_checkbox(11, 10, y, "Drop shadows under windows", &sd->window_shadows);
|
||||
y += 26;
|
||||
|
||||
gui_label(10, y, "Theme", GUI_THEME_TEXT_MUTED);
|
||||
y += 14;
|
||||
gui_radio(12, 14, y, "Nord Dark", &sd->theme_choice, 0); y += 18;
|
||||
gui_radio(13, 14, y, "Midnight", &sd->theme_choice, 1); y += 18;
|
||||
gui_radio(14, 14, y, "Slate Grey", &sd->theme_choice, 2); y += 24;
|
||||
|
||||
snprintf(buf, sizeof(buf), "Brightness: %d%%", sd->brightness);
|
||||
gui_label(10, y, buf, GUI_THEME_TEXT_MUTED);
|
||||
gui_slider(15, 150, y + 4, w - 170, &sd->brightness, 20, 100);
|
||||
|
||||
} else if (sd->active_tab == 1) {
|
||||
gui_label_bold(10, y, "Input Devices", GUI_THEME_PRIMARY);
|
||||
y += 24;
|
||||
|
||||
snprintf(buf, sizeof(buf), "Pointer speed: %d", sd->cursor_speed);
|
||||
gui_label(10, y, buf, GUI_THEME_TEXT_MUTED);
|
||||
gui_slider(20, 150, y + 4, w - 170, &sd->cursor_speed, 1, 10);
|
||||
y += 30;
|
||||
|
||||
gui_checkbox(21, 10, y, "Enable UI sound effects", &sd->sound_enabled);
|
||||
y += 22;
|
||||
gui_checkbox(22, 10, y, "Show seconds in taskbar clock", &sd->show_clock_seconds);
|
||||
y += 30;
|
||||
|
||||
const mouse_state_t* ms = mouse_get_state();
|
||||
snprintf(buf, sizeof(buf), "PS/2 mouse at (%d, %d) L:%s R:%s",
|
||||
ms ? ms->x : 0, ms ? ms->y : 0,
|
||||
(ms && ms->left_btn) ? "down" : "up",
|
||||
(ms && ms->right_btn) ? "down" : "up");
|
||||
gui_label(10, y, buf, GUI_THEME_TEXT_DIM);
|
||||
|
||||
} else {
|
||||
gui_label_bold(10, y, "Power & Frequency", GUI_THEME_PRIMARY);
|
||||
y += 24;
|
||||
|
||||
gui_label(10, y, "CPUFreq governor", GUI_THEME_TEXT_MUTED);
|
||||
y += 16;
|
||||
gui_radio(30, 14, y, "powersave", &sd->governor_choice, 0); y += 18;
|
||||
gui_radio(31, 14, y, "ondemand", &sd->governor_choice, 1); y += 18;
|
||||
gui_radio(32, 14, y, "performance", &sd->governor_choice, 2); y += 26;
|
||||
|
||||
cpufreq_stats_t cf = cpufreq_get_stats();
|
||||
snprintf(buf, sizeof(buf), "Current clock: %u.%03u GHz",
|
||||
cf.current_khz / 1000000, (cf.current_khz % 1000000) / 1000);
|
||||
gui_label(10, y, buf, GUI_THEME_TEXT_MAIN);
|
||||
y += 16;
|
||||
|
||||
int load = (int)(cf.current_khz / 30000);
|
||||
if (load > 100) load = 100;
|
||||
gui_progress_bar(10, y, w - 24, 12, load, GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
int btn_y = gui_widget_client_height() - 32;
|
||||
if (gui_button(90, 10, btn_y, 96, 24, "Apply")) {
|
||||
gui_dialog_show(GUI_DIALOG_INFO, "Settings",
|
||||
"Preferences applied to the current desktop session.");
|
||||
}
|
||||
if (gui_button_colored(91, 114, btn_y, 96, 24, "Defaults", GUI_THEME_WARNING)) {
|
||||
sd->show_grid = true;
|
||||
sd->window_shadows = true;
|
||||
sd->sound_enabled = true;
|
||||
sd->show_clock_seconds = true;
|
||||
sd->theme_choice = 0;
|
||||
sd->cursor_speed = 5;
|
||||
sd->brightness = 100;
|
||||
sd->governor_choice = 1;
|
||||
}
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_settings_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Settings", x, y,
|
||||
(w > 0) ? w : 380, (h > 0) ? h : 300);
|
||||
if (!win) return;
|
||||
|
||||
settings_data_t* sd = (settings_data_t*)kzalloc(sizeof(settings_data_t));
|
||||
if (!sd) return;
|
||||
|
||||
sd->show_grid = true;
|
||||
sd->window_shadows = true;
|
||||
sd->sound_enabled = true;
|
||||
sd->show_clock_seconds = true;
|
||||
sd->cursor_speed = 5;
|
||||
sd->brightness = 100;
|
||||
sd->governor_choice = 1;
|
||||
|
||||
win->user_data = sd;
|
||||
win->paint = settings_paint;
|
||||
win->handle_event = app_forward_event;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 2. Task Manager
|
||||
// ===========================================================================
|
||||
#define TASKMGR_MAX_ROWS 24
|
||||
|
||||
typedef struct {
|
||||
int selected;
|
||||
int row_count;
|
||||
char rows[TASKMGR_MAX_ROWS][40];
|
||||
const char* row_ptrs[TASKMGR_MAX_ROWS];
|
||||
pid_t pids[TASKMGR_MAX_ROWS];
|
||||
uint64_t last_refresh;
|
||||
} taskmgr_data_t;
|
||||
|
||||
static const char* task_state_name(task_state_t s) {
|
||||
switch (s) {
|
||||
case TASK_STATE_READY: return "READY";
|
||||
case TASK_STATE_RUNNING: return "RUN ";
|
||||
case TASK_STATE_BLOCKED: return "BLOCK";
|
||||
case TASK_STATE_SLEEPING: return "SLEEP";
|
||||
case TASK_STATE_ZOMBIE: return "ZOMBI";
|
||||
default: return "DEAD ";
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the low PID space; the kernel hands out small, dense pids.
|
||||
static void taskmgr_refresh(taskmgr_data_t* td) {
|
||||
td->row_count = 0;
|
||||
|
||||
for (pid_t pid = 0; pid < 64 && td->row_count < TASKMGR_MAX_ROWS; pid++) {
|
||||
task_t* t = task_find_by_pid(pid);
|
||||
if (!t) continue;
|
||||
|
||||
snprintf(td->rows[td->row_count], sizeof(td->rows[0]),
|
||||
"%4d %-16s %s p%d", t->pid, t->name,
|
||||
task_state_name(t->state), t->priority);
|
||||
td->row_ptrs[td->row_count] = td->rows[td->row_count];
|
||||
td->pids[td->row_count] = t->pid;
|
||||
td->row_count++;
|
||||
}
|
||||
|
||||
if (td->row_count == 0) {
|
||||
strcpy(td->rows[0], " 0 kernel_idle RUN p0");
|
||||
td->row_ptrs[0] = td->rows[0];
|
||||
td->pids[0] = 0;
|
||||
td->row_count = 1;
|
||||
}
|
||||
|
||||
if (td->selected >= td->row_count) td->selected = td->row_count - 1;
|
||||
td->last_refresh = pit_get_ticks();
|
||||
}
|
||||
|
||||
static void taskmgr_paint(gui_window_t* win) {
|
||||
taskmgr_data_t* td = (taskmgr_data_t*)win->user_data;
|
||||
if (!td) return;
|
||||
|
||||
// Re-scan roughly twice a second rather than on every composited frame.
|
||||
if (pit_get_ticks() - td->last_refresh > 50) taskmgr_refresh(td);
|
||||
|
||||
gui_widget_begin(win);
|
||||
|
||||
int w = gui_widget_client_width();
|
||||
int h = gui_widget_client_height();
|
||||
char buf[64];
|
||||
|
||||
gui_icon_draw(GUI_ICON_TASKS, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
|
||||
gui_label_bold(30, 6, "Process Table", GUI_THEME_PRIMARY);
|
||||
|
||||
gui_label(10, 28, " PID NAME STATE PRIO", GUI_THEME_TEXT_DIM);
|
||||
gui_listbox(1, 10, 40, w - 20, h - 108, td->row_ptrs, td->row_count, &td->selected);
|
||||
|
||||
int info_y = h - 62;
|
||||
gui_separator(10, info_y - 6, w - 20);
|
||||
|
||||
uint64_t total = pmm_get_total_pages();
|
||||
uint64_t used = pmm_get_used_pages();
|
||||
int mem_pct = total ? (int)((used * 100) / total) : 0;
|
||||
|
||||
snprintf(buf, sizeof(buf), "Memory %llu / %llu MB",
|
||||
(unsigned long long)((used * 4096) / (1024 * 1024)),
|
||||
(unsigned long long)((total * 4096) / (1024 * 1024)));
|
||||
gui_label(10, info_y, buf, GUI_THEME_TEXT_MUTED);
|
||||
gui_progress_bar(160, info_y - 2, w - 176, 12, mem_pct,
|
||||
mem_pct > 80 ? GUI_THEME_DANGER : GUI_THEME_SUCCESS);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Heap %llu KB used, %d task(s) live",
|
||||
(unsigned long long)(heap_get_used_bytes() / 1024), td->row_count);
|
||||
gui_label(10, info_y + 16, buf, GUI_THEME_TEXT_DIM);
|
||||
|
||||
int btn_y = h - 30;
|
||||
if (gui_button(2, 10, btn_y, 84, 24, "Refresh")) {
|
||||
taskmgr_refresh(td);
|
||||
}
|
||||
if (gui_button_colored(3, 102, btn_y, 84, 24, "End Task", GUI_THEME_DANGER)) {
|
||||
pid_t victim = (td->selected < td->row_count) ? td->pids[td->selected] : -1;
|
||||
if (victim <= 0) {
|
||||
gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager",
|
||||
"PID 0 is the idle task and cannot be terminated.");
|
||||
} else {
|
||||
gui_dialog_show(GUI_DIALOG_WARNING, "Task Manager",
|
||||
"Terminating kernel tasks from the desktop is disabled "
|
||||
"in this build.");
|
||||
}
|
||||
}
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_taskmgr_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Task Manager", x, y,
|
||||
(w > 0) ? w : 360, (h > 0) ? h : 300);
|
||||
if (!win) return;
|
||||
|
||||
taskmgr_data_t* td = (taskmgr_data_t*)kzalloc(sizeof(taskmgr_data_t));
|
||||
if (!td) return;
|
||||
|
||||
taskmgr_refresh(td);
|
||||
win->user_data = td;
|
||||
win->paint = taskmgr_paint;
|
||||
win->handle_event = app_forward_event;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 3. Kernel Log Viewer
|
||||
// ===========================================================================
|
||||
#define LOG_MAX_LINES 200
|
||||
#define LOG_LINE_CHARS 72
|
||||
|
||||
typedef struct {
|
||||
int scroll;
|
||||
int total_lines;
|
||||
bool wrap_long_lines;
|
||||
char lines[LOG_MAX_LINES][LOG_LINE_CHARS + 1];
|
||||
} logview_data_t;
|
||||
|
||||
// The dmesg ring holds raw bytes with embedded log-level markers; split it
|
||||
// into printable rows and drop the control prefixes.
|
||||
static void logview_reload(logview_data_t* lv) {
|
||||
size_t size = 0;
|
||||
const char* buf = dmesg_get_buffer(&size);
|
||||
|
||||
lv->total_lines = 0;
|
||||
if (!buf || size == 0) {
|
||||
strcpy(lv->lines[0], "(kernel ring buffer is empty)");
|
||||
lv->total_lines = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t i = 0;
|
||||
while (i < size && lv->total_lines < LOG_MAX_LINES) {
|
||||
int col = 0;
|
||||
char* out = lv->lines[lv->total_lines];
|
||||
|
||||
while (i < size && buf[i] != '\n') {
|
||||
char c = buf[i++];
|
||||
|
||||
if (c == '\033') {
|
||||
// Skip an ANSI CSI sequence entirely.
|
||||
while (i < size && buf[i] != 'm' && buf[i] != '\n') i++;
|
||||
if (i < size && buf[i] == 'm') i++;
|
||||
continue;
|
||||
}
|
||||
if ((unsigned char)c < 32) continue; // Log-level markers and tabs
|
||||
|
||||
if (col < LOG_LINE_CHARS) out[col++] = c;
|
||||
}
|
||||
if (i < size) i++; // Consume the newline
|
||||
|
||||
out[col] = '\0';
|
||||
if (col > 0) lv->total_lines++;
|
||||
}
|
||||
|
||||
if (lv->total_lines == 0) {
|
||||
strcpy(lv->lines[0], "(no printable log records)");
|
||||
lv->total_lines = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void logview_paint(gui_window_t* win) {
|
||||
logview_data_t* lv = (logview_data_t*)win->user_data;
|
||||
if (!lv) return;
|
||||
|
||||
gui_widget_begin(win);
|
||||
|
||||
int w = gui_widget_client_width();
|
||||
int h = gui_widget_client_height();
|
||||
char buf[64];
|
||||
|
||||
gui_icon_draw(GUI_ICON_LOG, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
|
||||
gui_label_bold(30, 6, "Kernel Ring Buffer", GUI_THEME_PRIMARY);
|
||||
|
||||
int view_y = 30;
|
||||
int view_h = h - 66;
|
||||
int row_h = 11;
|
||||
int visible = view_h / row_h;
|
||||
|
||||
gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y,
|
||||
w - 32, view_h, 0xFF080B14);
|
||||
gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + view_y,
|
||||
w - 32, view_h, GUI_THEME_BORDER);
|
||||
|
||||
if (lv->scroll > lv->total_lines - visible) lv->scroll = lv->total_lines - visible;
|
||||
if (lv->scroll < 0) lv->scroll = 0;
|
||||
|
||||
for (int i = 0; i < visible && lv->scroll + i < lv->total_lines; i++) {
|
||||
const char* line = lv->lines[lv->scroll + i];
|
||||
|
||||
// Colour-code by the severity words the kernel prints.
|
||||
uint32_t fg = GUI_THEME_TEXT_MUTED;
|
||||
if (strstr(line, "PANIC") || strstr(line, "ERROR")) fg = GUI_THEME_DANGER;
|
||||
else if (strstr(line, "WARN")) fg = GUI_THEME_WARNING;
|
||||
else if (strstr(line, "OK") || strstr(line, "online") || strstr(line, "initialized"))
|
||||
fg = GUI_THEME_SUCCESS;
|
||||
|
||||
gui_label(14, view_y + 3 + i * row_h, line, fg);
|
||||
}
|
||||
|
||||
gui_scrollbar(1, w - 21, view_y, view_h, &lv->scroll, lv->total_lines, visible);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d lines, showing %d-%d",
|
||||
lv->total_lines, lv->scroll + 1,
|
||||
(lv->scroll + visible < lv->total_lines) ? lv->scroll + visible : lv->total_lines);
|
||||
gui_label(10, h - 30, buf, GUI_THEME_TEXT_DIM);
|
||||
|
||||
if (gui_button(2, w - 190, h - 34, 84, 24, "Reload")) {
|
||||
logview_reload(lv);
|
||||
lv->scroll = 0;
|
||||
}
|
||||
if (gui_button(3, w - 100, h - 34, 84, 24, "End")) {
|
||||
lv->scroll = lv->total_lines - visible;
|
||||
if (lv->scroll < 0) lv->scroll = 0;
|
||||
}
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_logviewer_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Kernel Log", x, y,
|
||||
(w > 0) ? w : 620, (h > 0) ? h : 320);
|
||||
if (!win) return;
|
||||
|
||||
logview_data_t* lv = (logview_data_t*)kzalloc(sizeof(logview_data_t));
|
||||
if (!lv) return;
|
||||
|
||||
logview_reload(lv);
|
||||
win->user_data = lv;
|
||||
win->paint = logview_paint;
|
||||
win->handle_event = app_forward_event;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 4. KTest Runner
|
||||
// ===========================================================================
|
||||
typedef struct {
|
||||
int selected_suite;
|
||||
bool has_run;
|
||||
ktest_result_t result;
|
||||
char suite_name[24];
|
||||
} ktest_data_t;
|
||||
|
||||
static const char* const ktest_suite_names[] = {
|
||||
"(all suites)", "rbtree", "kfifo", "hashtable",
|
||||
"concurrency", "page_cache", "libcore"
|
||||
};
|
||||
#define KTEST_SUITE_COUNT 7
|
||||
|
||||
static void ktest_paint(gui_window_t* win) {
|
||||
ktest_data_t* kd = (ktest_data_t*)win->user_data;
|
||||
if (!kd) return;
|
||||
|
||||
gui_widget_begin(win);
|
||||
|
||||
int w = gui_widget_client_width();
|
||||
int h = gui_widget_client_height();
|
||||
char buf[64];
|
||||
|
||||
gui_icon_draw(GUI_ICON_FLASK, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 7);
|
||||
gui_label_bold(30, 6, "In-Kernel Test Harness", GUI_THEME_PRIMARY);
|
||||
|
||||
gui_label(10, 30, "Suite", GUI_THEME_TEXT_DIM);
|
||||
gui_listbox(1, 10, 42, 150, 108, ktest_suite_names, KTEST_SUITE_COUNT,
|
||||
&kd->selected_suite);
|
||||
|
||||
if (gui_button_colored(2, 10, 156, 150, 26, "Run Tests", GUI_THEME_SUCCESS)) {
|
||||
// Suite 0 is the "all" pseudo-entry; the rest map 1:1 by name.
|
||||
if (kd->selected_suite == 0) {
|
||||
ktest_run_all(&kd->result);
|
||||
strcpy(kd->suite_name, "all suites");
|
||||
} else {
|
||||
ktest_run_suite(ktest_suite_names[kd->selected_suite], &kd->result);
|
||||
strncpy(kd->suite_name, ktest_suite_names[kd->selected_suite],
|
||||
sizeof(kd->suite_name) - 1);
|
||||
}
|
||||
kd->has_run = true;
|
||||
}
|
||||
|
||||
int rx = 174;
|
||||
gui_panel(rx, 42, w - rx - 14, 140, "Results");
|
||||
|
||||
if (!kd->has_run) {
|
||||
gui_label(rx + 12, 66, "No run yet.", GUI_THEME_TEXT_DIM);
|
||||
gui_label(rx + 12, 82, "Pick a suite and press", GUI_THEME_TEXT_DIM);
|
||||
gui_label(rx + 12, 94, "Run Tests. Full per-case", GUI_THEME_TEXT_DIM);
|
||||
gui_label(rx + 12, 106, "output goes to the serial", GUI_THEME_TEXT_DIM);
|
||||
gui_label(rx + 12, 118, "console and dmesg.", GUI_THEME_TEXT_DIM);
|
||||
} else {
|
||||
const ktest_result_t* r = &kd->result;
|
||||
bool green = (r->tests_failed == 0);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Suite: %s", kd->suite_name);
|
||||
gui_label(rx + 12, 56, buf, GUI_THEME_TEXT_MAIN);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%u tests, %u passed, %u failed",
|
||||
r->tests_run, r->tests_passed, r->tests_failed);
|
||||
gui_label(rx + 12, 72, buf, GUI_THEME_TEXT_MUTED);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%u assertions, %u failures",
|
||||
r->assertions, r->assertion_failures);
|
||||
gui_label(rx + 12, 86, buf, GUI_THEME_TEXT_MUTED);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Elapsed: %llu ms",
|
||||
(unsigned long long)(r->elapsed_ticks * 10));
|
||||
gui_label(rx + 12, 100, buf, GUI_THEME_TEXT_DIM);
|
||||
|
||||
int pct = r->tests_run ? (int)((r->tests_passed * 100) / r->tests_run) : 0;
|
||||
gui_progress_bar(rx + 12, 118, w - rx - 40, 14, pct,
|
||||
green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER);
|
||||
|
||||
gui_badge(rx + 12, 140, green ? "ALL PASSED" : "FAILURES",
|
||||
green ? GUI_THEME_SUCCESS : GUI_THEME_DANGER, GUI_COLOR_BLACK);
|
||||
}
|
||||
|
||||
gui_separator(10, h - 42, w - 20);
|
||||
gui_label(10, h - 34, "Suites cover rbtree, kfifo, hashtable, RCU,",
|
||||
GUI_THEME_TEXT_DIM);
|
||||
gui_label(10, h - 22, "futex, wait queues, page cache and libcore.",
|
||||
GUI_THEME_TEXT_DIM);
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_ktest_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("KTest Runner", x, y,
|
||||
(w > 0) ? w : 460, (h > 0) ? h : 250);
|
||||
if (!win) return;
|
||||
|
||||
ktest_data_t* kd = (ktest_data_t*)kzalloc(sizeof(ktest_data_t));
|
||||
if (!kd) return;
|
||||
|
||||
win->user_data = kd;
|
||||
win->paint = ktest_paint;
|
||||
win->handle_event = app_forward_event;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 5. Clock & Calendar
|
||||
// ===========================================================================
|
||||
typedef struct {
|
||||
bool show_seconds;
|
||||
int view_month; // 1-12; 0 means "follow the RTC"
|
||||
} clock_data_t;
|
||||
|
||||
static const char* const month_names[] = {
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
};
|
||||
|
||||
static int days_in_month(int month, int year) {
|
||||
static const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
if (month < 1 || month > 12) return 30;
|
||||
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) return 29;
|
||||
return days[month - 1];
|
||||
}
|
||||
|
||||
// Zeller's congruence, returning 0 = Sunday.
|
||||
static int day_of_week(int day, int month, int year) {
|
||||
if (month < 3) { month += 12; year -= 1; }
|
||||
int k = year % 100;
|
||||
int j = year / 100;
|
||||
int hh = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
|
||||
return (hh + 6) % 7;
|
||||
}
|
||||
|
||||
static void clock_paint(gui_window_t* win) {
|
||||
clock_data_t* cd = (clock_data_t*)win->user_data;
|
||||
if (!cd) return;
|
||||
|
||||
rtc_time_t now;
|
||||
rtc_get_time(&now);
|
||||
|
||||
gui_widget_begin(win);
|
||||
int w = gui_widget_client_width();
|
||||
char buf[64];
|
||||
|
||||
int month = cd->view_month ? cd->view_month : (int)now.month;
|
||||
int year = (int)now.year;
|
||||
|
||||
// Big digital readout
|
||||
if (cd->show_seconds) {
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u:%02u", now.hour, now.minute, now.second);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u", now.hour, now.minute);
|
||||
}
|
||||
|
||||
int tw = (int)strlen(buf) * 8;
|
||||
gui_gfx_draw_string_16_shadow(win->x + 1 + (w - tw) / 2,
|
||||
win->y + GUI_TITLEBAR_HEIGHT + 9,
|
||||
buf, GUI_THEME_PRIMARY, GUI_THEME_PRIMARY_DARK);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%s %u, %d", month_names[month - 1], now.day, year);
|
||||
gui_label_aligned(0, 32, w, buf, GUI_THEME_TEXT_MAIN, GUI_ALIGN_CENTER);
|
||||
|
||||
// Month grid
|
||||
int gx = 12, gy = 50;
|
||||
int cell_w = (w - 24) / 7;
|
||||
int cell_h = 16;
|
||||
|
||||
const char* const dows[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
|
||||
for (int i = 0; i < 7; i++) {
|
||||
gui_label(gx + i * cell_w + (cell_w - 16) / 2, gy, dows[i],
|
||||
(i == 0 || i == 6) ? GUI_THEME_DANGER : GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
gui_separator(gx, gy + 12, w - 24);
|
||||
|
||||
int first_dow = day_of_week(1, month, year);
|
||||
int total_days = days_in_month(month, year);
|
||||
|
||||
for (int d = 1; d <= total_days; d++) {
|
||||
int slot = first_dow + d - 1;
|
||||
int col = slot % 7;
|
||||
int row = slot / 7;
|
||||
int cx = gx + col * cell_w;
|
||||
int cy = gy + 18 + row * cell_h;
|
||||
|
||||
bool today = (d == (int)now.day && month == (int)now.month);
|
||||
if (today) {
|
||||
gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + cy - 2,
|
||||
cell_w - 2, cell_h - 2, GUI_THEME_PRIMARY_DARK);
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "%2d", d);
|
||||
gui_label(cx + (cell_w - 16) / 2, cy, buf,
|
||||
today ? GUI_COLOR_WHITE
|
||||
: (col == 0 || col == 6) ? GUI_THEME_TEXT_DIM
|
||||
: GUI_THEME_TEXT_MUTED);
|
||||
}
|
||||
|
||||
int h = gui_widget_client_height();
|
||||
if (gui_button(1, 12, h - 30, 30, 22, "<")) {
|
||||
cd->view_month = (month == 1) ? 12 : month - 1;
|
||||
}
|
||||
if (gui_button(2, 46, h - 30, 30, 22, ">")) {
|
||||
cd->view_month = (month == 12) ? 1 : month + 1;
|
||||
}
|
||||
if (gui_button(3, 80, h - 30, 62, 22, "Today")) {
|
||||
cd->view_month = 0;
|
||||
}
|
||||
gui_checkbox(4, 152, h - 28, "Seconds", &cd->show_seconds);
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_clock_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Clock & Calendar", x, y,
|
||||
(w > 0) ? w : 270, (h > 0) ? h : 250);
|
||||
if (!win) return;
|
||||
|
||||
clock_data_t* cd = (clock_data_t*)kzalloc(sizeof(clock_data_t));
|
||||
if (!cd) return;
|
||||
|
||||
cd->show_seconds = true;
|
||||
win->user_data = cd;
|
||||
win->paint = clock_paint;
|
||||
win->handle_event = app_forward_event;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 6. Text Editor
|
||||
// ===========================================================================
|
||||
#define EDITOR_ROWS 14
|
||||
#define EDITOR_ROW_CHARS 56
|
||||
|
||||
typedef struct {
|
||||
char lines[EDITOR_ROWS][EDITOR_ROW_CHARS + 1];
|
||||
int line_count;
|
||||
int cursor_line;
|
||||
char filename[48];
|
||||
bool modified;
|
||||
bool word_wrap;
|
||||
} editor_data_t;
|
||||
|
||||
static void editor_insert_char(editor_data_t* ed, char c) {
|
||||
if (ed->cursor_line >= EDITOR_ROWS) return;
|
||||
|
||||
char* line = ed->lines[ed->cursor_line];
|
||||
size_t len = strlen(line);
|
||||
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (ed->line_count < EDITOR_ROWS) {
|
||||
// Push the following lines down to make room for the new one.
|
||||
for (int i = ed->line_count; i > ed->cursor_line + 1; i--) {
|
||||
strcpy(ed->lines[i], ed->lines[i - 1]);
|
||||
}
|
||||
ed->line_count++;
|
||||
ed->cursor_line++;
|
||||
ed->lines[ed->cursor_line][0] = '\0';
|
||||
ed->modified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (c == '\b' || (uint8_t)c == 0x7F) {
|
||||
if (len > 0) {
|
||||
line[len - 1] = '\0';
|
||||
ed->modified = true;
|
||||
} else if (ed->cursor_line > 0) {
|
||||
ed->cursor_line--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (c >= 32 && c <= 126 && len < EDITOR_ROW_CHARS) {
|
||||
line[len] = c;
|
||||
line[len + 1] = '\0';
|
||||
ed->modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void editor_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
editor_data_t* ed = (editor_data_t*)win->user_data;
|
||||
|
||||
if (ed && ev->type == GUI_EVENT_KEY_DOWN) {
|
||||
uint16_t key = ev->key;
|
||||
if (key == KEY_UP) {
|
||||
if (ed->cursor_line > 0) ed->cursor_line--;
|
||||
return;
|
||||
}
|
||||
if (key == KEY_DOWN) {
|
||||
if (ed->cursor_line < ed->line_count - 1) ed->cursor_line++;
|
||||
return;
|
||||
}
|
||||
if (!(key & KEY_SPECIAL_FLAG)) {
|
||||
editor_insert_char(ed, (char)(key & 0xFF));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
app_forward_event(win, ev);
|
||||
}
|
||||
|
||||
static void editor_paint(gui_window_t* win) {
|
||||
editor_data_t* ed = (editor_data_t*)win->user_data;
|
||||
if (!ed) return;
|
||||
|
||||
gui_widget_begin(win);
|
||||
|
||||
int w = gui_widget_client_width();
|
||||
int h = gui_widget_client_height();
|
||||
char buf[80];
|
||||
|
||||
// Toolbar
|
||||
gui_icon_draw(GUI_ICON_FILE, win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + 6);
|
||||
snprintf(buf, sizeof(buf), "%s%s", ed->filename, ed->modified ? " *" : "");
|
||||
gui_label(30, 10, buf, ed->modified ? GUI_THEME_WARNING : GUI_THEME_TEXT_MAIN);
|
||||
|
||||
if (gui_button(1, w - 176, 6, 52, 20, "New")) {
|
||||
memset(ed->lines, 0, sizeof(ed->lines));
|
||||
ed->line_count = 1;
|
||||
ed->cursor_line = 0;
|
||||
ed->modified = false;
|
||||
strcpy(ed->filename, "untitled.txt");
|
||||
}
|
||||
if (gui_button(2, w - 120, 6, 52, 20, "Save")) {
|
||||
ed->modified = false;
|
||||
gui_dialog_show(GUI_DIALOG_INFO, "Text Editor",
|
||||
"This build keeps the buffer in memory only; VFS write-back "
|
||||
"is not wired up yet.");
|
||||
}
|
||||
if (gui_button(3, w - 64, 6, 52, 20, "Wrap")) {
|
||||
ed->word_wrap = !ed->word_wrap;
|
||||
}
|
||||
|
||||
// Text area with a gutter
|
||||
int area_y = 32;
|
||||
int area_h = h - area_y - 26;
|
||||
int row_h = 14;
|
||||
|
||||
gui_gfx_fill_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y,
|
||||
w - 18, area_h, 0xFF080B14);
|
||||
gui_gfx_draw_rect(win->x + 9, win->y + GUI_TITLEBAR_HEIGHT + area_y,
|
||||
w - 18, area_h, GUI_THEME_BORDER);
|
||||
gui_gfx_draw_line(win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + 1,
|
||||
win->x + 40, win->y + GUI_TITLEBAR_HEIGHT + area_y + area_h - 2,
|
||||
GUI_THEME_BORDER);
|
||||
|
||||
int visible = (area_h - 6) / row_h;
|
||||
for (int i = 0; i < visible && i < ed->line_count; i++) {
|
||||
int ry = area_y + 4 + i * row_h;
|
||||
bool is_cursor = (i == ed->cursor_line);
|
||||
|
||||
if (is_cursor) {
|
||||
gui_gfx_fill_rect(win->x + 42, win->y + GUI_TITLEBAR_HEIGHT + ry - 2,
|
||||
w - 54, row_h, 0xFF11182B);
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "%3d", i + 1);
|
||||
gui_label(14, ry, buf, is_cursor ? GUI_THEME_PRIMARY : GUI_THEME_TEXT_DIM);
|
||||
gui_label(38, ry, ed->lines[i], GUI_THEME_TEXT_MAIN);
|
||||
|
||||
if (is_cursor) {
|
||||
int cx = 38 + (int)strlen(ed->lines[i]) * 8;
|
||||
gui_gfx_fill_rect(win->x + 1 + cx, win->y + GUI_TITLEBAR_HEIGHT + 1 + ry - 1,
|
||||
7, 10, GUI_THEME_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "Ln %d/%d Col %d %s",
|
||||
ed->cursor_line + 1, ed->line_count,
|
||||
(int)strlen(ed->lines[ed->cursor_line]) + 1,
|
||||
ed->word_wrap ? "WRAP" : "NOWRAP");
|
||||
gui_label(10, h - 16, buf, GUI_THEME_TEXT_DIM);
|
||||
|
||||
gui_widget_end();
|
||||
}
|
||||
|
||||
void gui_app_editor_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Text Editor", x, y,
|
||||
(w > 0) ? w : 500, (h > 0) ? h : 320);
|
||||
if (!win) return;
|
||||
|
||||
editor_data_t* ed = (editor_data_t*)kzalloc(sizeof(editor_data_t));
|
||||
if (!ed) return;
|
||||
|
||||
strcpy(ed->filename, "untitled.txt");
|
||||
strcpy(ed->lines[0], "# SUB-OS Text Editor");
|
||||
strcpy(ed->lines[1], "");
|
||||
strcpy(ed->lines[2], "Type to edit. Enter adds a line,");
|
||||
strcpy(ed->lines[3], "Up/Down move between lines.");
|
||||
ed->line_count = 4;
|
||||
ed->cursor_line = 3;
|
||||
|
||||
win->user_data = ed;
|
||||
win->paint = editor_paint;
|
||||
win->handle_event = editor_event;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// High-Resolution Smooth Mouse Cursor for SUB-OS GUI
|
||||
#include <gui/gui_cursor.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
|
||||
#define CURSOR_W 16
|
||||
#define CURSOR_H 24
|
||||
|
||||
static int g_cur_x = 400;
|
||||
static int g_cur_y = 300;
|
||||
|
||||
// 16x24 Modern Pointer Cursor (0: Transparent, 1: Black Outline, 2: White Fill, 3: Shadow)
|
||||
static const uint8_t cursor_bitmap[24][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,2,2,2,1,0,0,0,0,0,0,0},
|
||||
{1,2,2,2,2,2,2,2,2,1,0,0,0,0,0,0},
|
||||
{1,2,2,2,2,2,2,2,2,2,1,0,0,0,0,0},
|
||||
{1,2,2,2,2,2,2,2,2,2,2,1,0,0,0,0},
|
||||
{1,2,2,2,2,2,2,1,1,1,1,1,1,0,0,0},
|
||||
{1,2,2,2,1,2,2,1,0,0,0,0,0,0,0,0},
|
||||
{1,2,2,1,0,1,2,2,1,0,0,0,0,0,0,0},
|
||||
{1,2,1,0,0,1,2,2,1,0,0,0,0,0,0,0},
|
||||
{1,1,0,0,0,0,1,2,2,1,0,0,0,0,0,0},
|
||||
{1,0,0,0,0,0,1,2,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,1,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,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,0,0,0,0}
|
||||
};
|
||||
|
||||
void gui_cursor_init(void) {
|
||||
g_cur_x = gui_gfx_get_width() / 2;
|
||||
g_cur_y = gui_gfx_get_height() / 2;
|
||||
}
|
||||
|
||||
void gui_cursor_set_pos(int x, int y) {
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x >= gui_gfx_get_width()) x = gui_gfx_get_width() - 1;
|
||||
if (y >= gui_gfx_get_height()) y = gui_gfx_get_height() - 1;
|
||||
g_cur_x = x;
|
||||
g_cur_y = y;
|
||||
}
|
||||
|
||||
int gui_cursor_get_x(void) { return g_cur_x; }
|
||||
int gui_cursor_get_y(void) { return g_cur_y; }
|
||||
|
||||
void gui_cursor_draw(void) {
|
||||
// 1. Draw Subtle Cursor Drop Shadow
|
||||
for (int j = 0; j < CURSOR_H; j++) {
|
||||
int py = g_cur_y + j + 2;
|
||||
for (int i = 0; i < CURSOR_W; i++) {
|
||||
int px = g_cur_x + i + 2;
|
||||
uint8_t pixel = cursor_bitmap[j][i];
|
||||
if (pixel != 0) {
|
||||
gui_gfx_draw_pixel_blend(px, py, GUI_COLOR_BLACK, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Cursor Body
|
||||
for (int j = 0; j < CURSOR_H; j++) {
|
||||
int py = g_cur_y + j;
|
||||
for (int i = 0; i < CURSOR_W; i++) {
|
||||
int px = g_cur_x + i;
|
||||
uint8_t pixel = cursor_bitmap[j][i];
|
||||
if (pixel == 1) {
|
||||
gui_gfx_draw_pixel(px, py, 0xFF0F172A); // Dark Slate Outline
|
||||
} else if (pixel == 2) {
|
||||
gui_gfx_draw_pixel(px, py, GUI_COLOR_WHITE); // Pure White Interior
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
// SUB-OS Desktop Shell: wallpaper, icon grid, taskbar, menus and compositor loop
|
||||
//
|
||||
// The shell owns the frame loop. Every iteration it samples the input devices,
|
||||
// routes the result through the modal dialog layer, then the desktop chrome,
|
||||
// then the window manager, and finally composites the scene back to front:
|
||||
// wallpaper, icons, windows, taskbar, menus, dialog, cursor.
|
||||
|
||||
#include <gui/gui_desktop.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_cursor.h>
|
||||
#include <gui/gui_wm.h>
|
||||
#include <gui/gui_apps.h>
|
||||
#include <gui/gui_terminal.h>
|
||||
#include <gui/gui_apps_ext.h>
|
||||
#include <gui/gui_widgets.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <gui/gui_dialog.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <drivers/fb.h>
|
||||
#include <drivers/bochs.h>
|
||||
#include <drivers/fbcon.h>
|
||||
#include <drivers/tty.h>
|
||||
#include <drivers/mouse.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <drivers/rtc.h>
|
||||
#include <drivers/cpufreq.h>
|
||||
#include <mm/pmm.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <arch/arch.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
#include <kernel/printk.h>
|
||||
|
||||
#define START_MENU_W 232
|
||||
#define START_ITEM_H 26
|
||||
#define CONTEXT_MENU_W 150
|
||||
#define CONTEXT_ITEM_H 22
|
||||
|
||||
typedef void (*launch_fn_t)(int x, int y, int w, int h);
|
||||
|
||||
typedef struct {
|
||||
const char* label;
|
||||
gui_icon_id_t icon;
|
||||
launch_fn_t launch;
|
||||
int x, y, w, h;
|
||||
} desktop_app_t;
|
||||
|
||||
// Launchers used by both the icon grid and the start menu.
|
||||
static const desktop_app_t g_apps[] = {
|
||||
{ "Terminal", GUI_ICON_TERMINAL, gui_app_terminal_launch, 30, 40, 560, 340 },
|
||||
{ "Files", GUI_ICON_FOLDER, gui_app_fileman_launch, 80, 70, 380, 240 },
|
||||
{ "Monitor", GUI_ICON_MONITOR, gui_app_sysmon_launch, 420, 40, 340, 240 },
|
||||
{ "Tasks", GUI_ICON_TASKS, gui_app_taskmgr_launch, 180, 60, 380, 300 },
|
||||
{ "Kernel Log", GUI_ICON_LOG, gui_app_logviewer_launch, 60, 90, 640, 330 },
|
||||
{ "KTest", GUI_ICON_FLASK, gui_app_ktest_launch, 140, 80, 470, 250 },
|
||||
{ "Editor", GUI_ICON_FILE, gui_app_editor_launch, 120, 50, 500, 330 },
|
||||
{ "Calculator", GUI_ICON_CALC, gui_app_calc_launch, 260, 100, 220, 240 },
|
||||
{ "Paint", GUI_ICON_PAINT, gui_app_paint_launch, 200, 60, 360, 260 },
|
||||
{ "Clock", GUI_ICON_CLOCK, gui_app_clock_launch, 460, 120, 280, 260 },
|
||||
{ "Settings", GUI_ICON_SETTINGS, gui_app_settings_launch, 160, 60, 400, 320 },
|
||||
{ "About", GUI_ICON_INFO, gui_app_about_launch, 240, 140, 330, 170 },
|
||||
};
|
||||
#define APP_COUNT ((int)(sizeof(g_apps) / sizeof(g_apps[0])))
|
||||
|
||||
// Which of the above appear on the desktop itself.
|
||||
static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 10 };
|
||||
#define DESKTOP_ICON_COUNT ((int)(sizeof(g_desktop_icons) / sizeof(g_desktop_icons[0])))
|
||||
|
||||
static bool g_start_menu_open = false;
|
||||
static bool g_context_menu_open = false;
|
||||
static int g_context_x = 0, g_context_y = 0;
|
||||
static bool g_desktop_running = false;
|
||||
static bool g_prev_left = false, g_prev_right = false;
|
||||
static bool g_show_grid = true;
|
||||
static int g_selected_icon = -1;
|
||||
static int g_hover_menu_item = -1;
|
||||
static uint64_t g_frames = 0;
|
||||
|
||||
/*
|
||||
* Cached wallpaper.
|
||||
*
|
||||
* The background is a full-screen vertical gradient with an optional blended
|
||||
* grid and three strings on top. None of it changes between frames, but it was
|
||||
* being recomputed pixel by pixel on every pass of the compositor loop. It is
|
||||
* drawn once into this buffer instead and blitted back each frame; the cache
|
||||
* is dropped whenever the resolution or the grid setting changes.
|
||||
*/
|
||||
/* Rolling count of pixels actually written to the framebuffer, so the cost of
|
||||
* the compositor is visible on the console without a profiler attached. */
|
||||
static uint64_t g_pushed_pixels = 0;
|
||||
|
||||
static uint32_t* g_wallpaper = NULL;
|
||||
static int g_wallpaper_w = 0;
|
||||
static int g_wallpaper_h = 0;
|
||||
static bool g_wallpaper_grid = false;
|
||||
static uint8_t g_cpu_history[48];
|
||||
|
||||
// The launcher geometry below is authored against an 800x600 work area.
|
||||
#define DESIGN_W 800
|
||||
#define DESIGN_H 568
|
||||
|
||||
static void launch_app(int index) {
|
||||
if (index < 0 || index >= APP_COUNT) return;
|
||||
|
||||
const desktop_app_t* a = &g_apps[index];
|
||||
if (!a->launch) return;
|
||||
|
||||
int sw = gui_gfx_get_width();
|
||||
int wh = gui_desktop_workarea_height();
|
||||
|
||||
// Scale the design geometry to the live resolution, but never shrink below
|
||||
// it: a window sized for 800x600 is already at its comfortable minimum.
|
||||
int x = (a->x * sw) / DESIGN_W;
|
||||
int y = (a->y * wh) / DESIGN_H;
|
||||
int w = (a->w * sw) / DESIGN_W;
|
||||
int h = (a->h * wh) / DESIGN_H;
|
||||
|
||||
if (w < a->w) w = a->w;
|
||||
if (h < a->h) h = a->h;
|
||||
|
||||
// Leave the desktop breathing room: a window scaled straight from the
|
||||
// design size swallows most of a 1280x720 screen and buries the wallpaper.
|
||||
int max_w = (sw * 62) / 100;
|
||||
int max_h = (wh * 72) / 100;
|
||||
if (w > max_w) w = max_w;
|
||||
if (h > max_h) h = max_h;
|
||||
|
||||
// Keep clear of the icon column so launcher labels stay readable.
|
||||
int icon_strip = GUI_DESKTOP_ICON_W + 28;
|
||||
if (x < icon_strip) x = icon_strip;
|
||||
if (x + w > sw - 8) x = sw - 8 - w;
|
||||
if (y + h > wh - 8) y = wh - 8 - h;
|
||||
if (y < 8) y = 8;
|
||||
|
||||
a->launch(x, y, w, h);
|
||||
}
|
||||
|
||||
int gui_desktop_workarea_height(void) {
|
||||
return gui_gfx_get_height() - GUI_TASKBAR_HEIGHT;
|
||||
}
|
||||
|
||||
void gui_desktop_init(void) {
|
||||
const fb_info_t* fb = fb_get_info();
|
||||
uint32_t* fb_addr = fb ? fb->address : NULL;
|
||||
int w = (fb && fb->width > 0) ? fb->width : GUI_DEFAULT_WIDTH;
|
||||
int h = (fb && fb->height > 0) ? fb->height : GUI_DEFAULT_HEIGHT;
|
||||
|
||||
gui_gfx_init(fb_addr, w, h);
|
||||
/* The console owned the framebuffer until now, so nothing on screen
|
||||
* matches the compositor's idea of it; drop both caches. */
|
||||
gui_gfx_invalidate();
|
||||
gui_desktop_invalidate_background();
|
||||
gui_icons_init();
|
||||
gui_cursor_init();
|
||||
gui_dialog_init();
|
||||
gui_wm_init();
|
||||
gui_wm_set_workarea(w, h - GUI_TASKBAR_HEIGHT);
|
||||
mouse_set_bounds(w, h);
|
||||
|
||||
g_start_menu_open = false;
|
||||
g_context_menu_open = false;
|
||||
g_selected_icon = -1;
|
||||
|
||||
// Seed the taskbar CPU trace with a plausible idle curve.
|
||||
for (int i = 0; i < 48; i++) {
|
||||
g_cpu_history[i] = (uint8_t)(12 + ((i * 7) % 23));
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Wallpaper and desktop icons
|
||||
// ===========================================================================
|
||||
|
||||
/* Paint the wallpaper straight into the back buffer. */
|
||||
static void desktop_paint_background(int sw, int wh) {
|
||||
gui_gfx_draw_gradient_v(0, 0, sw, wh, 0xFF0B132B, 0xFF1C2541);
|
||||
|
||||
if (g_show_grid) {
|
||||
// Blend rather than stamp a fixed colour: the gradient's lower half is
|
||||
// lighter than any single grid tone, so a constant colour disappears
|
||||
// there and only shows near the top of the screen.
|
||||
for (int y = 0; y < wh; y += 40) {
|
||||
gui_gfx_fill_rect_blend(0, y, sw, 1, GUI_COLOR_BLACK, 40);
|
||||
}
|
||||
for (int x = 0; x < sw; x += 40) {
|
||||
gui_gfx_fill_rect_blend(x, 0, 1, wh, GUI_COLOR_BLACK, 40);
|
||||
}
|
||||
}
|
||||
|
||||
// Branding, offset to the right so it clears the icon column.
|
||||
int bx = sw / 2 + 40;
|
||||
int by = wh / 2 - 30;
|
||||
gui_gfx_draw_string_16_shadow(bx - 60, by, "SUB-OS DESKTOP", 0xFF38BDF8, 0xFF0284C7);
|
||||
gui_gfx_draw_string(bx - 76, by + 22, "Modular Monolithic Kernel v0.2.0-LTS", 0xFF64748B);
|
||||
gui_gfx_draw_string(bx - 76, by + 36, "F5 cycle F6 cascade F7 tile F8 max", 0xFF475569);
|
||||
}
|
||||
|
||||
void gui_desktop_invalidate_background(void) {
|
||||
if (g_wallpaper) {
|
||||
kfree(g_wallpaper);
|
||||
g_wallpaper = NULL;
|
||||
}
|
||||
g_wallpaper_w = 0;
|
||||
g_wallpaper_h = 0;
|
||||
}
|
||||
|
||||
void gui_desktop_render_background(void) {
|
||||
int sw = gui_gfx_get_width();
|
||||
int wh = gui_desktop_workarea_height();
|
||||
uint32_t* back = gui_gfx_get_backbuffer();
|
||||
|
||||
if (sw <= 0 || wh <= 0 || !back) return;
|
||||
|
||||
bool stale = (!g_wallpaper || g_wallpaper_w != sw || g_wallpaper_h != wh ||
|
||||
g_wallpaper_grid != g_show_grid);
|
||||
|
||||
if (stale) {
|
||||
if (g_wallpaper) {
|
||||
kfree(g_wallpaper);
|
||||
g_wallpaper = NULL;
|
||||
}
|
||||
/* Paint it once through the normal primitives, then keep a copy. The
|
||||
* back buffer is the scratch space, which is safe because the caller
|
||||
* redraws everything above the wallpaper straight afterwards. */
|
||||
desktop_paint_background(sw, wh);
|
||||
|
||||
g_wallpaper = (uint32_t*)kmalloc((size_t)sw * (size_t)wh * sizeof(uint32_t));
|
||||
if (g_wallpaper) {
|
||||
for (int y = 0; y < wh; y++) {
|
||||
memcpy(g_wallpaper + (size_t)y * sw,
|
||||
back + (size_t)y * sw,
|
||||
(size_t)sw * sizeof(uint32_t));
|
||||
}
|
||||
g_wallpaper_w = sw;
|
||||
g_wallpaper_h = wh;
|
||||
g_wallpaper_grid = g_show_grid;
|
||||
}
|
||||
return; /* the back buffer already holds the freshly painted copy */
|
||||
}
|
||||
|
||||
for (int y = 0; y < wh; y++) {
|
||||
memcpy(back + (size_t)y * sw,
|
||||
g_wallpaper + (size_t)y * sw,
|
||||
(size_t)sw * sizeof(uint32_t));
|
||||
}
|
||||
}
|
||||
|
||||
void gui_desktop_render_icons(void) {
|
||||
for (int i = 0; i < DESKTOP_ICON_COUNT; i++) {
|
||||
const desktop_app_t* a = &g_apps[g_desktop_icons[i]];
|
||||
int ix = 16;
|
||||
int iy = 16 + i * GUI_DESKTOP_ICON_H;
|
||||
bool selected = (g_selected_icon == i);
|
||||
|
||||
// A translucent tile behind every icon: several glyphs are dark by
|
||||
// design and would otherwise vanish into the dark wallpaper.
|
||||
if (selected) {
|
||||
gui_gfx_fill_rect_blend(ix - 4, iy - 4, GUI_DESKTOP_ICON_W,
|
||||
GUI_DESKTOP_ICON_H - 4, GUI_THEME_PRIMARY, 70);
|
||||
gui_gfx_draw_rect(ix - 4, iy - 4, GUI_DESKTOP_ICON_W,
|
||||
GUI_DESKTOP_ICON_H - 4, GUI_THEME_PRIMARY);
|
||||
} else {
|
||||
gui_gfx_fill_rect_blend(ix + 10, iy - 4, 40, 40, GUI_THEME_BG_ELEVATED, 150);
|
||||
gui_gfx_draw_rect(ix + 10, iy - 4, 40, 40, 0xFF3D4C63);
|
||||
}
|
||||
|
||||
gui_icon_draw_scaled(a->icon, ix + 14, iy, 2);
|
||||
|
||||
int tw = (int)strlen(a->label) * 8;
|
||||
int tx = ix + 30 - tw / 2;
|
||||
if (tx < 0) tx = 0;
|
||||
gui_gfx_draw_string_shadow(tx, iy + 40, a->label,
|
||||
GUI_THEME_TEXT_MAIN, GUI_COLOR_BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the icon index under the point, or -1.
|
||||
static int icon_hit_test(int mx, int my) {
|
||||
for (int i = 0; i < DESKTOP_ICON_COUNT; i++) {
|
||||
int ix = 12;
|
||||
int iy = 12 + i * GUI_DESKTOP_ICON_H;
|
||||
if (mx >= ix && mx < ix + GUI_DESKTOP_ICON_W &&
|
||||
my >= iy && my < iy + GUI_DESKTOP_ICON_H - 4) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Taskbar
|
||||
// ===========================================================================
|
||||
|
||||
void gui_desktop_render_taskbar(void) {
|
||||
int sw = gui_gfx_get_width();
|
||||
int ty = gui_desktop_workarea_height();
|
||||
|
||||
gui_gfx_draw_gradient_v(0, ty, sw, GUI_TASKBAR_HEIGHT, 0xFF111827, GUI_THEME_TASKBAR_BG);
|
||||
gui_gfx_draw_line(0, ty, sw - 1, ty, GUI_THEME_BORDER);
|
||||
|
||||
// Start button
|
||||
uint32_t start_bg = g_start_menu_open ? GUI_THEME_PRIMARY_DARK : GUI_THEME_PRIMARY;
|
||||
gui_gfx_fill_rect(6, ty + 4, 92, 24, start_bg);
|
||||
gui_gfx_draw_rect(6, ty + 4, 92, 24, GUI_THEME_PRIMARY_DARK);
|
||||
gui_icon_draw(GUI_ICON_TERMINAL, 11, ty + 8);
|
||||
gui_gfx_draw_string_16_shadow(32, ty + 8, "SUB-OS", GUI_COLOR_WHITE, GUI_COLOR_BLACK);
|
||||
|
||||
// One button per window, in z-order
|
||||
int tab_x = 106;
|
||||
int tray_x = sw - 200;
|
||||
for (int i = 0; i < gui_wm_get_window_count() && tab_x < tray_x - 106; i++) {
|
||||
gui_window_t* win = gui_wm_get_window_at_index(i);
|
||||
if (!win || !win->visible) continue;
|
||||
|
||||
uint32_t tab_bg = win->active && !win->minimized ? GUI_THEME_BG_ELEVATED
|
||||
: GUI_THEME_BG_SURFACE;
|
||||
gui_gfx_fill_rect(tab_x, ty + 4, 100, 24, tab_bg);
|
||||
gui_gfx_draw_rect(tab_x, ty + 4, 100, 24,
|
||||
win->active && !win->minimized ? GUI_THEME_PRIMARY : GUI_THEME_BORDER);
|
||||
|
||||
// Active window gets an accent underline; minimized ones are dimmed.
|
||||
if (win->active && !win->minimized) {
|
||||
gui_gfx_fill_rect(tab_x + 1, ty + 25, 98, 2, GUI_THEME_PRIMARY);
|
||||
}
|
||||
|
||||
char short_title[12];
|
||||
strncpy(short_title, win->title, 11);
|
||||
short_title[11] = '\0';
|
||||
gui_gfx_draw_string(tab_x + 8, ty + 12, short_title,
|
||||
win->minimized ? GUI_THEME_TEXT_DIM
|
||||
: (win->active ? GUI_THEME_TEXT_MAIN
|
||||
: GUI_THEME_TEXT_MUTED));
|
||||
tab_x += 106;
|
||||
}
|
||||
|
||||
// System tray: CPU trace, memory badge, clock
|
||||
for (int i = 0; i < 47; i++) {
|
||||
int x0 = tray_x + i;
|
||||
int y0 = ty + 26 - (g_cpu_history[i] * 18) / 100;
|
||||
int y1 = ty + 26 - (g_cpu_history[i + 1] * 18) / 100;
|
||||
gui_gfx_draw_line(x0, y0, x0 + 1, y1, GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
uint64_t total = pmm_get_total_pages();
|
||||
uint64_t used = pmm_get_used_pages();
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "%llu MB",
|
||||
(unsigned long long)((used * 4096) / (1024 * 1024)));
|
||||
gui_gfx_fill_rect(sw - 145, ty + 6, 52, 20, GUI_THEME_SUCCESS);
|
||||
gui_gfx_draw_rect(sw - 145, ty + 6, 52, 20, 0xFF059669);
|
||||
gui_gfx_draw_string(sw - 140, ty + 12, buf, GUI_COLOR_BLACK);
|
||||
(void)total;
|
||||
|
||||
rtc_time_t now;
|
||||
rtc_get_time(&now);
|
||||
snprintf(buf, sizeof(buf), "%02u:%02u:%02u", now.hour, now.minute, now.second);
|
||||
gui_gfx_draw_string_16_shadow(sw - 84, ty + 8, buf, GUI_THEME_PRIMARY, GUI_COLOR_BLACK);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Start menu
|
||||
// ===========================================================================
|
||||
|
||||
static int start_menu_height(void) {
|
||||
// Header + one row per app + separator + power row.
|
||||
return 32 + APP_COUNT * START_ITEM_H + 10 + START_ITEM_H + 8;
|
||||
}
|
||||
|
||||
static void start_menu_origin(int* mx, int* my) {
|
||||
*mx = 6;
|
||||
*my = gui_desktop_workarea_height() - start_menu_height() - 6;
|
||||
if (*my < 4) *my = 4;
|
||||
}
|
||||
|
||||
void gui_desktop_render_start_menu(void) {
|
||||
if (!g_start_menu_open) return;
|
||||
|
||||
int mx, my;
|
||||
start_menu_origin(&mx, &my);
|
||||
int mh = start_menu_height();
|
||||
|
||||
gui_gfx_draw_shadow(mx, my, START_MENU_W, mh, 10);
|
||||
gui_gfx_fill_rect(mx, my, START_MENU_W, mh, GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_rect(mx, my, START_MENU_W, mh, GUI_THEME_PRIMARY);
|
||||
|
||||
gui_gfx_draw_gradient_v(mx + 1, my + 1, START_MENU_W - 2, 30,
|
||||
GUI_THEME_PRIMARY, GUI_THEME_PRIMARY_DARK);
|
||||
gui_gfx_draw_string_16_shadow(mx + 12, my + 7, "Applications",
|
||||
GUI_COLOR_WHITE, GUI_COLOR_BLACK);
|
||||
|
||||
for (int i = 0; i < APP_COUNT; i++) {
|
||||
int iy = my + 32 + i * START_ITEM_H;
|
||||
bool hot = (g_hover_menu_item == i);
|
||||
|
||||
if (hot) {
|
||||
gui_gfx_fill_rect(mx + 2, iy, START_MENU_W - 4, START_ITEM_H,
|
||||
GUI_THEME_BG_ELEVATED);
|
||||
gui_gfx_fill_rect(mx + 2, iy, 3, START_ITEM_H, GUI_THEME_PRIMARY);
|
||||
}
|
||||
|
||||
gui_icon_draw(g_apps[i].icon, mx + 12, iy + (START_ITEM_H - 16) / 2);
|
||||
gui_gfx_draw_string(mx + 36, iy + (START_ITEM_H - 8) / 2, g_apps[i].label,
|
||||
hot ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED);
|
||||
}
|
||||
|
||||
int sep_y = my + 32 + APP_COUNT * START_ITEM_H + 4;
|
||||
gui_gfx_draw_line(mx + 8, sep_y, mx + START_MENU_W - 9, sep_y, GUI_THEME_BORDER);
|
||||
|
||||
int power_y = sep_y + 6;
|
||||
bool power_hot = (g_hover_menu_item == APP_COUNT);
|
||||
if (power_hot) {
|
||||
gui_gfx_fill_rect(mx + 2, power_y, START_MENU_W - 4, START_ITEM_H, 0xFF3B1418);
|
||||
gui_gfx_fill_rect(mx + 2, power_y, 3, START_ITEM_H, GUI_THEME_DANGER);
|
||||
}
|
||||
gui_icon_draw(GUI_ICON_POWER, mx + 12, power_y + (START_ITEM_H - 16) / 2);
|
||||
gui_gfx_draw_string(mx + 36, power_y + (START_ITEM_H - 8) / 2,
|
||||
"Exit to Kernel TTY", GUI_THEME_DANGER);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Desktop context menu
|
||||
// ===========================================================================
|
||||
|
||||
static const char* const g_context_items[] = {
|
||||
"Tile Windows", "Cascade Windows", "Toggle Grid", "New Terminal", "About SUB-OS"
|
||||
};
|
||||
#define CONTEXT_ITEM_COUNT 5
|
||||
|
||||
void gui_desktop_open_context_menu(int mx, int my) {
|
||||
g_context_menu_open = true;
|
||||
g_start_menu_open = false;
|
||||
|
||||
int menu_h = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8;
|
||||
g_context_x = mx;
|
||||
g_context_y = my;
|
||||
|
||||
// Flip the menu back on screen when opened near an edge.
|
||||
if (g_context_x + CONTEXT_MENU_W > gui_gfx_get_width()) {
|
||||
g_context_x = gui_gfx_get_width() - CONTEXT_MENU_W - 2;
|
||||
}
|
||||
if (g_context_y + menu_h > gui_desktop_workarea_height()) {
|
||||
g_context_y = gui_desktop_workarea_height() - menu_h - 2;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_desktop_render_context_menu(void) {
|
||||
if (!g_context_menu_open) return;
|
||||
|
||||
int mh = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8;
|
||||
gui_gfx_draw_shadow(g_context_x, g_context_y, CONTEXT_MENU_W, mh, 6);
|
||||
gui_gfx_fill_rect(g_context_x, g_context_y, CONTEXT_MENU_W, mh, GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_rect(g_context_x, g_context_y, CONTEXT_MENU_W, mh, GUI_THEME_ACCENT);
|
||||
|
||||
for (int i = 0; i < CONTEXT_ITEM_COUNT; i++) {
|
||||
int iy = g_context_y + 4 + i * CONTEXT_ITEM_H;
|
||||
bool hot = (g_hover_menu_item == 100 + i);
|
||||
|
||||
if (hot) {
|
||||
gui_gfx_fill_rect(g_context_x + 2, iy, CONTEXT_MENU_W - 4,
|
||||
CONTEXT_ITEM_H, GUI_THEME_BG_ELEVATED);
|
||||
}
|
||||
gui_gfx_draw_string(g_context_x + 12, iy + (CONTEXT_ITEM_H - 8) / 2,
|
||||
g_context_items[i],
|
||||
hot ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_desktop_toggle_start_menu(void) {
|
||||
g_start_menu_open = !g_start_menu_open;
|
||||
if (g_start_menu_open) g_context_menu_open = false;
|
||||
}
|
||||
|
||||
bool gui_desktop_is_start_menu_open(void) { return g_start_menu_open; }
|
||||
|
||||
void gui_desktop_close_menus(void) {
|
||||
g_start_menu_open = false;
|
||||
g_context_menu_open = false;
|
||||
}
|
||||
|
||||
void gui_desktop_request_exit(void) { g_desktop_running = false; }
|
||||
|
||||
static void exit_confirm_cb(gui_dialog_result_t result, void* ctx) {
|
||||
(void)ctx;
|
||||
if (result == GUI_DIALOG_RESULT_OK) g_desktop_running = false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Input routing
|
||||
// ===========================================================================
|
||||
|
||||
// Track which menu row the pointer is over so rendering can highlight it.
|
||||
static void update_menu_hover(int mx, int my) {
|
||||
g_hover_menu_item = -1;
|
||||
|
||||
if (g_start_menu_open) {
|
||||
int smx, smy;
|
||||
start_menu_origin(&smx, &smy);
|
||||
|
||||
if (mx >= smx && mx < smx + START_MENU_W) {
|
||||
int rel = my - (smy + 32);
|
||||
if (rel >= 0) {
|
||||
int idx = rel / START_ITEM_H;
|
||||
if (idx >= 0 && idx < APP_COUNT) {
|
||||
g_hover_menu_item = idx;
|
||||
} else {
|
||||
int power_y = smy + 32 + APP_COUNT * START_ITEM_H + 10;
|
||||
if (my >= power_y && my < power_y + START_ITEM_H) {
|
||||
g_hover_menu_item = APP_COUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_context_menu_open) {
|
||||
if (mx >= g_context_x && mx < g_context_x + CONTEXT_MENU_W) {
|
||||
int rel = my - (g_context_y + 4);
|
||||
int idx = rel / CONTEXT_ITEM_H;
|
||||
if (rel >= 0 && idx < CONTEXT_ITEM_COUNT) g_hover_menu_item = 100 + idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool handle_start_menu_click(int mx, int my) {
|
||||
int smx, smy;
|
||||
start_menu_origin(&smx, &smy);
|
||||
int mh = start_menu_height();
|
||||
|
||||
if (mx < smx || mx >= smx + START_MENU_W || my < smy || my >= smy + mh) {
|
||||
g_start_menu_open = false;
|
||||
return true; // The dismissing click is consumed, not passed through.
|
||||
}
|
||||
|
||||
int rel = my - (smy + 32);
|
||||
int idx = rel / START_ITEM_H;
|
||||
|
||||
if (rel >= 0 && idx >= 0 && idx < APP_COUNT) {
|
||||
g_start_menu_open = false;
|
||||
launch_app(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
int power_y = smy + 32 + APP_COUNT * START_ITEM_H + 10;
|
||||
if (my >= power_y && my < power_y + START_ITEM_H) {
|
||||
g_start_menu_open = false;
|
||||
gui_dialog_confirm("Exit Desktop",
|
||||
"Leave the graphical session and return to the kernel TTY?",
|
||||
exit_confirm_cb, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_context_menu_click(int mx, int my) {
|
||||
int mh = CONTEXT_ITEM_COUNT * CONTEXT_ITEM_H + 8;
|
||||
|
||||
if (mx < g_context_x || mx >= g_context_x + CONTEXT_MENU_W ||
|
||||
my < g_context_y || my >= g_context_y + mh) {
|
||||
g_context_menu_open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int rel = my - (g_context_y + 4);
|
||||
g_context_menu_open = false;
|
||||
if (rel < 0) return true;
|
||||
|
||||
int idx = rel / CONTEXT_ITEM_H;
|
||||
|
||||
switch (idx) {
|
||||
case 0: gui_wm_tile_windows(); break;
|
||||
case 1: gui_wm_cascade_windows(); break;
|
||||
case 2: g_show_grid = !g_show_grid; break;
|
||||
case 3: launch_app(0); break;
|
||||
case 4: launch_app(APP_COUNT - 1); break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_taskbar_click(int mx, int my) {
|
||||
int ty = gui_desktop_workarea_height();
|
||||
|
||||
if (mx >= 6 && mx < 98 && my >= ty + 4 && my < ty + 28) {
|
||||
gui_desktop_toggle_start_menu();
|
||||
return true;
|
||||
}
|
||||
|
||||
int tab_x = 106;
|
||||
int tray_x = gui_gfx_get_width() - 200;
|
||||
for (int i = 0; i < gui_wm_get_window_count() && tab_x < tray_x - 106; i++) {
|
||||
gui_window_t* win = gui_wm_get_window_at_index(i);
|
||||
if (!win || !win->visible) continue;
|
||||
|
||||
if (mx >= tab_x && mx < tab_x + 100 && my >= ty + 4 && my < ty + 28) {
|
||||
// Clicking the focused window's button minimizes it; otherwise
|
||||
// the window is restored and raised.
|
||||
if (win->active && !win->minimized) {
|
||||
gui_wm_minimize_window(win->id);
|
||||
} else {
|
||||
gui_wm_restore_window(win->id);
|
||||
gui_wm_focus_window(win->id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
tab_x += 106;
|
||||
}
|
||||
|
||||
return true; // Bare taskbar clicks are absorbed
|
||||
}
|
||||
|
||||
bool gui_desktop_handle_click(int mx, int my) {
|
||||
if (g_start_menu_open) return handle_start_menu_click(mx, my);
|
||||
if (g_context_menu_open) return handle_context_menu_click(mx, my);
|
||||
|
||||
if (my >= gui_desktop_workarea_height()) return handle_taskbar_click(mx, my);
|
||||
|
||||
// Icons only claim the click when no window covers that spot.
|
||||
for (int i = gui_wm_get_window_count() - 1; i >= 0; i--) {
|
||||
gui_window_t* win = gui_wm_get_window_at_index(i);
|
||||
if (!win || !win->visible || win->minimized) continue;
|
||||
if (mx >= win->x && mx < win->x + win->width &&
|
||||
my >= win->y && my < win->y + win->height) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int icon = icon_hit_test(mx, my);
|
||||
if (icon >= 0) {
|
||||
if (g_selected_icon == icon) {
|
||||
// Second click on an already-selected icon launches it.
|
||||
launch_app(g_desktop_icons[icon]);
|
||||
g_selected_icon = -1;
|
||||
} else {
|
||||
g_selected_icon = icon;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
g_selected_icon = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Frame loop
|
||||
// ===========================================================================
|
||||
|
||||
static void sample_cpu_history(void) {
|
||||
// Shift in a fresh sample derived from the current P-state.
|
||||
for (int i = 0; i < 47; i++) g_cpu_history[i] = g_cpu_history[i + 1];
|
||||
|
||||
cpufreq_stats_t cf = cpufreq_get_stats();
|
||||
uint32_t span = (cf.max_khz > cf.min_khz) ? (cf.max_khz - cf.min_khz) : 1;
|
||||
uint32_t rel = (cf.current_khz > cf.min_khz) ? (cf.current_khz - cf.min_khz) : 0;
|
||||
uint32_t pct = (rel * 100) / span;
|
||||
if (pct > 100) pct = 100;
|
||||
|
||||
g_cpu_history[47] = (uint8_t)pct;
|
||||
}
|
||||
|
||||
int gui_desktop_run(void) {
|
||||
fbcon_enable(false);
|
||||
gui_desktop_init();
|
||||
|
||||
// Open a default session so the desktop is not empty on first boot.
|
||||
launch_app(2); // System Monitor
|
||||
launch_app(0); // Terminal
|
||||
|
||||
// Discard anything the console left in the keyboard queue; a stale key
|
||||
// arriving on frame 0 would otherwise act as a desktop shortcut.
|
||||
for (int drained = 0; drained < 16 && keyboard_has_key(); drained++) {
|
||||
(void)keyboard_get_key();
|
||||
}
|
||||
|
||||
g_desktop_running = true;
|
||||
printk(KERN_INFO "GUI: Desktop session started (ESC returns to the kernel TTY)\n");
|
||||
|
||||
while (g_desktop_running) {
|
||||
// --- 1. Sample input -----------------------------------------------
|
||||
const mouse_state_t* ms = mouse_get_state();
|
||||
int mx = ms ? ms->x : gui_gfx_get_width() / 2;
|
||||
int my = ms ? ms->y : gui_gfx_get_height() / 2;
|
||||
bool left = ms ? ms->left_btn : false;
|
||||
bool right = ms ? ms->right_btn : false;
|
||||
|
||||
gui_cursor_set_pos(mx, my);
|
||||
bool click_edge = left && !g_prev_left;
|
||||
bool right_click_edge = right && !g_prev_right;
|
||||
|
||||
// --- 2. Route the pointer, modal layer first ------------------------
|
||||
if (gui_dialog_is_open()) {
|
||||
gui_dialog_handle_mouse(mx, my, click_edge);
|
||||
gui_wm_sync_button_state(left);
|
||||
} else {
|
||||
update_menu_hover(mx, my);
|
||||
|
||||
if (right_click_edge && my < gui_desktop_workarea_height()) {
|
||||
gui_desktop_open_context_menu(mx, my);
|
||||
gui_wm_sync_button_state(left);
|
||||
} else if (click_edge) {
|
||||
if (gui_desktop_handle_click(mx, my)) {
|
||||
gui_wm_sync_button_state(left);
|
||||
} else {
|
||||
gui_wm_handle_mouse(mx, my, left, right);
|
||||
}
|
||||
} else {
|
||||
gui_wm_handle_mouse(mx, my, left, right);
|
||||
}
|
||||
}
|
||||
|
||||
g_prev_left = left;
|
||||
g_prev_right = right;
|
||||
|
||||
// --- 3. Route the keyboard ------------------------------------------
|
||||
// keyboard_get_key() blocks until a key arrives, which would freeze
|
||||
// the compositor, so only call it once the driver reports one ready.
|
||||
uint16_t key = keyboard_has_key() ? keyboard_get_key() : 0;
|
||||
if (key != 0) {
|
||||
if (gui_dialog_is_open()) {
|
||||
gui_dialog_handle_key(key);
|
||||
} else if ((char)(key & 0xFF) == 27) {
|
||||
if (g_start_menu_open || g_context_menu_open) {
|
||||
gui_desktop_close_menus();
|
||||
} else {
|
||||
gui_dialog_confirm("Exit Desktop",
|
||||
"Leave the graphical session and return to "
|
||||
"the kernel TTY?", exit_confirm_cb, NULL);
|
||||
}
|
||||
} else {
|
||||
gui_wm_handle_key(key);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Composite the scene, back to front --------------------------
|
||||
if ((g_frames & 0x1F) == 0) sample_cpu_history();
|
||||
|
||||
gui_desktop_render_background();
|
||||
gui_desktop_render_icons();
|
||||
gui_wm_render();
|
||||
gui_desktop_render_taskbar();
|
||||
gui_desktop_render_start_menu();
|
||||
gui_desktop_render_context_menu();
|
||||
gui_dialog_render();
|
||||
gui_cursor_draw();
|
||||
|
||||
gui_gfx_present();
|
||||
g_frames++;
|
||||
g_pushed_pixels += gui_gfx_last_present_pixels();
|
||||
|
||||
if ((g_frames & 0xFF) == 0) {
|
||||
uint64_t full = (uint64_t)gui_gfx_get_width() * (uint64_t)gui_gfx_get_height();
|
||||
uint64_t avg = g_pushed_pixels / 256;
|
||||
printk(KERN_INFO "GUI: %llu frames, %llu px/frame pushed of %llu (%llu%%)\n",
|
||||
(unsigned long long)g_frames,
|
||||
(unsigned long long)avg,
|
||||
(unsigned long long)full,
|
||||
(unsigned long long)(full ? (avg * 100) / full : 0));
|
||||
g_pushed_pixels = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Hand the screen to the framebuffer console. Without this the shell writes
|
||||
// into the VGA text buffer while the adapter is still scanning out the
|
||||
// framebuffer, so the TTY is visible only on the serial line -- which is
|
||||
// the host terminal, not the emulator window.
|
||||
fbcon_enable(true);
|
||||
|
||||
/* The console is about to draw over the framebuffer, so the shadow copy
|
||||
* stops describing the screen; release the wallpaper cache with it. */
|
||||
gui_gfx_invalidate();
|
||||
gui_desktop_invalidate_background();
|
||||
|
||||
printk(KERN_INFO "GUI: Desktop session ended after %llu frames, TTY restored\n",
|
||||
(unsigned long long)g_frames);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Modal dialog layer for the SUB-OS desktop
|
||||
#include <gui/gui_dialog.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <lib/string.h>
|
||||
|
||||
#define DIALOG_W 300
|
||||
#define DIALOG_H 132
|
||||
#define DIALOG_TITLEBAR 24
|
||||
#define DIALOG_BTN_W 84
|
||||
#define DIALOG_BTN_H 24
|
||||
#define DIALOG_MAX_LINES 4
|
||||
#define DIALOG_LINE_CHARS 28
|
||||
|
||||
typedef struct {
|
||||
bool open;
|
||||
gui_dialog_kind_t kind;
|
||||
char title[48];
|
||||
char lines[DIALOG_MAX_LINES][DIALOG_LINE_CHARS + 1];
|
||||
int line_count;
|
||||
bool has_cancel;
|
||||
gui_dialog_cb_t callback;
|
||||
void* ctx;
|
||||
int hover_button; // 0 = none, 1 = OK, 2 = Cancel
|
||||
} dialog_state_t;
|
||||
|
||||
static dialog_state_t g_dlg;
|
||||
|
||||
void gui_dialog_init(void) {
|
||||
memset(&g_dlg, 0, sizeof(g_dlg));
|
||||
}
|
||||
|
||||
// Break the message on spaces so words are not split across rows.
|
||||
static void wrap_message(const char* message) {
|
||||
g_dlg.line_count = 0;
|
||||
if (!message) return;
|
||||
|
||||
const char* p = message;
|
||||
while (*p && g_dlg.line_count < DIALOG_MAX_LINES) {
|
||||
int take = 0;
|
||||
int last_space = -1;
|
||||
|
||||
while (p[take] && take < DIALOG_LINE_CHARS) {
|
||||
if (p[take] == ' ') last_space = take;
|
||||
take++;
|
||||
}
|
||||
|
||||
if (p[take] && last_space > 0) take = last_space;
|
||||
|
||||
int n = take;
|
||||
if (n > DIALOG_LINE_CHARS) n = DIALOG_LINE_CHARS;
|
||||
memcpy(g_dlg.lines[g_dlg.line_count], p, (size_t)n);
|
||||
g_dlg.lines[g_dlg.line_count][n] = '\0';
|
||||
g_dlg.line_count++;
|
||||
|
||||
p += take;
|
||||
while (*p == ' ') p++;
|
||||
}
|
||||
}
|
||||
|
||||
static void open_dialog(gui_dialog_kind_t kind, const char* title, const char* message,
|
||||
bool has_cancel, gui_dialog_cb_t cb, void* ctx) {
|
||||
memset(&g_dlg, 0, sizeof(g_dlg));
|
||||
g_dlg.open = true;
|
||||
g_dlg.kind = kind;
|
||||
g_dlg.has_cancel = has_cancel;
|
||||
g_dlg.callback = cb;
|
||||
g_dlg.ctx = ctx;
|
||||
|
||||
strncpy(g_dlg.title, title ? title : "SUB-OS", sizeof(g_dlg.title) - 1);
|
||||
wrap_message(message);
|
||||
}
|
||||
|
||||
void gui_dialog_show(gui_dialog_kind_t kind, const char* title, const char* message) {
|
||||
open_dialog(kind, title, message, false, NULL, NULL);
|
||||
}
|
||||
|
||||
void gui_dialog_confirm(const char* title, const char* message,
|
||||
gui_dialog_cb_t on_result, void* ctx) {
|
||||
open_dialog(GUI_DIALOG_CONFIRM, title, message, true, on_result, ctx);
|
||||
}
|
||||
|
||||
void gui_dialog_dismiss(void) {
|
||||
g_dlg.open = false;
|
||||
}
|
||||
|
||||
bool gui_dialog_is_open(void) {
|
||||
return g_dlg.open;
|
||||
}
|
||||
|
||||
static void finish(gui_dialog_result_t result) {
|
||||
gui_dialog_cb_t cb = g_dlg.callback;
|
||||
void* ctx = g_dlg.ctx;
|
||||
|
||||
// Clear before invoking so a callback may immediately open another dialog.
|
||||
g_dlg.open = false;
|
||||
g_dlg.callback = NULL;
|
||||
|
||||
if (cb) cb(result, ctx);
|
||||
}
|
||||
|
||||
static void dialog_geometry(int* dx, int* dy) {
|
||||
*dx = (gui_gfx_get_width() - DIALOG_W) / 2;
|
||||
*dy = (gui_gfx_get_height() - DIALOG_H) / 2 - 20;
|
||||
}
|
||||
|
||||
bool gui_dialog_handle_mouse(int mx, int my, bool clicked) {
|
||||
if (!g_dlg.open) return false;
|
||||
|
||||
int dx, dy;
|
||||
dialog_geometry(&dx, &dy);
|
||||
|
||||
int btn_y = dy + DIALOG_H - DIALOG_BTN_H - 12;
|
||||
int ok_x, cancel_x;
|
||||
if (g_dlg.has_cancel) {
|
||||
ok_x = dx + DIALOG_W - (DIALOG_BTN_W * 2) - 22;
|
||||
cancel_x = dx + DIALOG_W - DIALOG_BTN_W - 12;
|
||||
} else {
|
||||
ok_x = dx + (DIALOG_W - DIALOG_BTN_W) / 2;
|
||||
cancel_x = -1000;
|
||||
}
|
||||
|
||||
g_dlg.hover_button = 0;
|
||||
if (my >= btn_y && my < btn_y + DIALOG_BTN_H) {
|
||||
if (mx >= ok_x && mx < ok_x + DIALOG_BTN_W) g_dlg.hover_button = 1;
|
||||
else if (g_dlg.has_cancel && mx >= cancel_x && mx < cancel_x + DIALOG_BTN_W) g_dlg.hover_button = 2;
|
||||
}
|
||||
|
||||
if (clicked) {
|
||||
if (g_dlg.hover_button == 1) finish(GUI_DIALOG_RESULT_OK);
|
||||
else if (g_dlg.hover_button == 2) finish(GUI_DIALOG_RESULT_CANCEL);
|
||||
}
|
||||
|
||||
// Swallow every click while modal, including ones outside the frame.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gui_dialog_handle_key(uint16_t key) {
|
||||
if (!g_dlg.open) return false;
|
||||
|
||||
char c = (char)(key & 0xFF);
|
||||
if (c == '\n' || c == '\r') {
|
||||
finish(GUI_DIALOG_RESULT_OK);
|
||||
} else if (c == 27) { // Escape
|
||||
finish(g_dlg.has_cancel ? GUI_DIALOG_RESULT_CANCEL : GUI_DIALOG_RESULT_OK);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void draw_button(int x, int y, const char* label, bool hovered, uint32_t accent) {
|
||||
gui_gfx_fill_rect(x, y, DIALOG_BTN_W, DIALOG_BTN_H,
|
||||
hovered ? GUI_THEME_BG_ELEVATED : GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_rect(x, y, DIALOG_BTN_W, DIALOG_BTN_H, hovered ? accent : GUI_THEME_BORDER);
|
||||
|
||||
int tw = (int)strlen(label) * 8;
|
||||
gui_gfx_draw_string(x + (DIALOG_BTN_W - tw) / 2, y + (DIALOG_BTN_H - 8) / 2,
|
||||
label, hovered ? GUI_COLOR_WHITE : GUI_THEME_TEXT_MUTED);
|
||||
}
|
||||
|
||||
void gui_dialog_render(void) {
|
||||
if (!g_dlg.open) return;
|
||||
|
||||
// Dim the whole desktop so the modal reads as the only live surface.
|
||||
gui_gfx_fill_rect_blend(0, 0, gui_gfx_get_width(), gui_gfx_get_height(),
|
||||
GUI_COLOR_BLACK, 120);
|
||||
|
||||
int dx, dy;
|
||||
dialog_geometry(&dx, &dy);
|
||||
|
||||
uint32_t accent;
|
||||
gui_icon_id_t icon;
|
||||
switch (g_dlg.kind) {
|
||||
case GUI_DIALOG_WARNING: accent = GUI_THEME_WARNING; icon = GUI_ICON_WARNING; break;
|
||||
case GUI_DIALOG_ERROR: accent = GUI_THEME_DANGER; icon = GUI_ICON_WARNING; break;
|
||||
case GUI_DIALOG_CONFIRM: accent = GUI_THEME_ACCENT; icon = GUI_ICON_INFO; break;
|
||||
default: accent = GUI_THEME_PRIMARY; icon = GUI_ICON_INFO; break;
|
||||
}
|
||||
|
||||
gui_gfx_draw_shadow(dx, dy, DIALOG_W, DIALOG_H, 10);
|
||||
gui_gfx_fill_rect(dx, dy, DIALOG_W, DIALOG_H, GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_rect(dx, dy, DIALOG_W, DIALOG_H, accent);
|
||||
|
||||
// Title bar
|
||||
gui_gfx_draw_gradient_v(dx + 1, dy + 1, DIALOG_W - 2, DIALOG_TITLEBAR - 1,
|
||||
GUI_THEME_BG_ELEVATED, GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_line(dx, dy + DIALOG_TITLEBAR, dx + DIALOG_W - 1, dy + DIALOG_TITLEBAR, accent);
|
||||
gui_gfx_draw_string_16_shadow(dx + 10, dy + 4, g_dlg.title, GUI_COLOR_WHITE, GUI_COLOR_BLACK);
|
||||
|
||||
// Icon + wrapped message
|
||||
gui_icon_draw_scaled(icon, dx + 14, dy + DIALOG_TITLEBAR + 14, 2);
|
||||
for (int i = 0; i < g_dlg.line_count; i++) {
|
||||
gui_gfx_draw_string(dx + 58, dy + DIALOG_TITLEBAR + 14 + i * 13,
|
||||
g_dlg.lines[i], GUI_THEME_TEXT_MAIN);
|
||||
}
|
||||
|
||||
int btn_y = dy + DIALOG_H - DIALOG_BTN_H - 12;
|
||||
if (g_dlg.has_cancel) {
|
||||
draw_button(dx + DIALOG_W - (DIALOG_BTN_W * 2) - 22, btn_y, "OK",
|
||||
g_dlg.hover_button == 1, accent);
|
||||
draw_button(dx + DIALOG_W - DIALOG_BTN_W - 12, btn_y, "Cancel",
|
||||
g_dlg.hover_button == 2, GUI_THEME_DANGER);
|
||||
} else {
|
||||
draw_button(dx + (DIALOG_W - DIALOG_BTN_W) / 2, btn_y, "OK",
|
||||
g_dlg.hover_button == 1, accent);
|
||||
}
|
||||
|
||||
gui_gfx_draw_string(dx + 12, dy + DIALOG_H - 12, "Enter = OK Esc = Dismiss",
|
||||
GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
// High-Performance 2D Graphics Engine & Software Compositor for SUB-OS
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <lib/font8x8.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <lib/string.h>
|
||||
|
||||
static uint32_t* g_frontbuffer = NULL;
|
||||
static uint32_t* g_backbuffer = NULL;
|
||||
/*
|
||||
* Mirror of what the framebuffer currently holds.
|
||||
*
|
||||
* The front buffer lives in device memory, where a write costs far more than
|
||||
* the same write to RAM and a read costs more still. Keeping a RAM copy of the
|
||||
* last presented frame lets gui_gfx_present() work out which pixels actually
|
||||
* changed without ever reading the framebuffer back, and push only those.
|
||||
*/
|
||||
static uint32_t* g_shadow = NULL;
|
||||
static bool g_shadow_valid = false;
|
||||
static int g_width = 800;
|
||||
static int g_height = 600;
|
||||
static uint64_t g_present_pixels = 0;
|
||||
|
||||
|
||||
void gui_gfx_init(uint32_t* fb, int width, int height) {
|
||||
int new_w = (width > 0) ? width : 800;
|
||||
int new_h = (height > 0) ? height : 600;
|
||||
|
||||
/* A resolution change invalidates both RAM buffers. */
|
||||
if (g_backbuffer && (new_w != g_width || new_h != g_height)) {
|
||||
kfree(g_backbuffer);
|
||||
g_backbuffer = NULL;
|
||||
if (g_shadow) {
|
||||
kfree(g_shadow);
|
||||
g_shadow = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
g_frontbuffer = fb;
|
||||
g_width = new_w;
|
||||
g_height = new_h;
|
||||
|
||||
size_t pixels = (size_t)g_width * (size_t)g_height;
|
||||
if (!g_backbuffer) {
|
||||
g_backbuffer = (uint32_t*)kzalloc(pixels * sizeof(uint32_t));
|
||||
}
|
||||
if (!g_shadow) {
|
||||
/* Optional: without it present() just falls back to a full copy. */
|
||||
g_shadow = (uint32_t*)kzalloc(pixels * sizeof(uint32_t));
|
||||
}
|
||||
g_shadow_valid = false;
|
||||
}
|
||||
|
||||
void gui_gfx_invalidate(void) {
|
||||
g_shadow_valid = false;
|
||||
}
|
||||
|
||||
uint64_t gui_gfx_last_present_pixels(void) {
|
||||
return g_present_pixels;
|
||||
}
|
||||
|
||||
int gui_gfx_get_width(void) { return g_width; }
|
||||
int gui_gfx_get_height(void) { return g_height; }
|
||||
uint32_t* gui_gfx_get_backbuffer(void) { return g_backbuffer; }
|
||||
|
||||
void gui_gfx_clear(uint32_t color) {
|
||||
if (!g_backbuffer) return;
|
||||
size_t count = g_width * g_height;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
g_backbuffer[i] = color;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_pixel(int x, int y, uint32_t color) {
|
||||
if (!g_backbuffer || x < 0 || y < 0 || x >= g_width || y >= g_height) return;
|
||||
g_backbuffer[y * g_width + x] = color;
|
||||
}
|
||||
|
||||
void gui_gfx_draw_pixel_blend(int x, int y, uint32_t color, uint8_t alpha) {
|
||||
if (!g_backbuffer || x < 0 || y < 0 || x >= g_width || y >= g_height) return;
|
||||
uint32_t bg = g_backbuffer[y * g_width + x];
|
||||
g_backbuffer[y * g_width + x] = gui_color_alpha_blend(bg, color, alpha);
|
||||
}
|
||||
|
||||
void gui_gfx_draw_line(int x0, int y0, int x1, int y1, uint32_t color) {
|
||||
int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
|
||||
int dy = (y1 > y0) ? (y1 - y0) : (y0 - y1);
|
||||
int sx = (x0 < x1) ? 1 : -1;
|
||||
int sy = (y0 < y1) ? 1 : -1;
|
||||
int err = dx - dy;
|
||||
|
||||
while (1) {
|
||||
gui_gfx_draw_pixel(x0, y0, color);
|
||||
if (x0 == x1 && y0 == y1) break;
|
||||
int e2 = 2 * err;
|
||||
if (e2 > -dy) { err -= dy; x0 += sx; }
|
||||
if (e2 < dx) { err += dx; y0 += sy; }
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_rect(int x, int y, int w, int h, uint32_t color) {
|
||||
gui_gfx_draw_line(x, y, x + w - 1, y, color);
|
||||
gui_gfx_draw_line(x, y + h - 1, x + w - 1, y + h - 1, color);
|
||||
gui_gfx_draw_line(x, y, x, y + h - 1, color);
|
||||
gui_gfx_draw_line(x + w - 1, y, x + w - 1, y + h - 1, color);
|
||||
}
|
||||
|
||||
void gui_gfx_fill_rect(int x, int y, int w, int h, uint32_t color) {
|
||||
if (!g_backbuffer) return;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > g_width) w = g_width - x;
|
||||
if (y + h > g_height) h = g_height - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
for (int j = y; j < y + h; j++) {
|
||||
uint32_t* row = &g_backbuffer[j * g_width + x];
|
||||
for (int i = 0; i < w; i++) {
|
||||
row[i] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_fill_rect_blend(int x, int y, int w, int h, uint32_t color, uint8_t alpha) {
|
||||
if (!g_backbuffer) return;
|
||||
if (x < 0) { w += x; x = 0; }
|
||||
if (y < 0) { h += y; y = 0; }
|
||||
if (x + w > g_width) w = g_width - x;
|
||||
if (y + h > g_height) h = g_height - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
for (int j = y; j < y + h; j++) {
|
||||
uint32_t* row = &g_backbuffer[j * g_width + x];
|
||||
for (int i = 0; i < w; i++) {
|
||||
row[i] = gui_color_alpha_blend(row[i], color, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_rounded_rect(int x, int y, int w, int h, int r, uint32_t color, bool filled) {
|
||||
if (r <= 0) {
|
||||
if (filled) gui_gfx_fill_rect(x, y, w, h, color);
|
||||
else gui_gfx_draw_rect(x, y, w, h, color);
|
||||
return;
|
||||
}
|
||||
|
||||
if (filled) {
|
||||
gui_gfx_fill_rect(x + r, y, w - 2 * r, h, color);
|
||||
gui_gfx_fill_rect(x, y + r, r, h - 2 * r, color);
|
||||
gui_gfx_fill_rect(x + w - r, y + r, r, h - 2 * r, color);
|
||||
} else {
|
||||
gui_gfx_draw_line(x + r, y, x + w - r, y, color);
|
||||
gui_gfx_draw_line(x + r, y + h - 1, x + w - r, y + h - 1, color);
|
||||
gui_gfx_draw_line(x, y + r, x, y + h - r, color);
|
||||
gui_gfx_draw_line(x + w - 1, y + r, x + w - 1, y + h - r, color);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_gradient_v(int x, int y, int w, int h, uint32_t top_col, uint32_t bot_col) {
|
||||
if (h <= 0 || w <= 0) return;
|
||||
for (int j = 0; j < h; j++) {
|
||||
uint8_t factor = (uint8_t)((j * 255) / h);
|
||||
uint32_t row_col = gui_color_alpha_blend(top_col, bot_col, factor);
|
||||
gui_gfx_fill_rect(x, y + j, w, 1, row_col);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_gradient_h(int x, int y, int w, int h, uint32_t left_col, uint32_t right_col) {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
for (int i = 0; i < w; i++) {
|
||||
uint8_t factor = (uint8_t)((i * 255) / w);
|
||||
uint32_t col = gui_color_alpha_blend(left_col, right_col, factor);
|
||||
gui_gfx_fill_rect(x + i, y, 1, h, col);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_char(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg) {
|
||||
if (c < 32 || c > 126) c = '?';
|
||||
const uint8_t* glyph = font8x8_glyph(c);
|
||||
|
||||
for (int row = 0; row < 8; row++) {
|
||||
uint8_t bits = glyph[row];
|
||||
for (int col = 0; col < 8; col++) {
|
||||
if (bits & (1 << (7 - col))) {
|
||||
gui_gfx_draw_pixel(x + col, y + row, fg);
|
||||
} else if (!transparent_bg) {
|
||||
gui_gfx_draw_pixel(x + col, y + row, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sharp High-DPI 8x16 font renderer with 2x vertical pixel fidelity
|
||||
void gui_gfx_draw_char_16(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg) {
|
||||
if (c < 32 || c > 126) c = '?';
|
||||
const uint8_t* glyph = font8x8_glyph(c);
|
||||
|
||||
for (int row = 0; row < 8; row++) {
|
||||
uint8_t bits = glyph[row];
|
||||
for (int col = 0; col < 8; col++) {
|
||||
if (bits & (1 << (7 - col))) {
|
||||
gui_gfx_draw_pixel(x + col, y + row * 2, fg);
|
||||
gui_gfx_draw_pixel(x + col, y + row * 2 + 1, fg);
|
||||
} else if (!transparent_bg) {
|
||||
gui_gfx_draw_pixel(x + col, y + row * 2, bg);
|
||||
gui_gfx_draw_pixel(x + col, y + row * 2 + 1, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_string(int x, int y, const char* str, uint32_t fg) {
|
||||
if (!str) return;
|
||||
int cx = x;
|
||||
while (*str) {
|
||||
gui_gfx_draw_char(cx, y, *str, fg, 0, true);
|
||||
cx += 8;
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_string_16(int x, int y, const char* str, uint32_t fg) {
|
||||
if (!str) return;
|
||||
int cx = x;
|
||||
while (*str) {
|
||||
gui_gfx_draw_char_16(cx, y, *str, fg, 0, true);
|
||||
cx += 8;
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_string_bg(int x, int y, const char* str, uint32_t fg, uint32_t bg) {
|
||||
if (!str) return;
|
||||
int cx = x;
|
||||
while (*str) {
|
||||
gui_gfx_draw_char(cx, y, *str, fg, bg, false);
|
||||
cx += 8;
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_draw_string_shadow(int x, int y, const char* str, uint32_t fg, uint32_t shadow) {
|
||||
gui_gfx_draw_string(x + 1, y + 1, str, shadow);
|
||||
gui_gfx_draw_string(x, y, str, fg);
|
||||
}
|
||||
|
||||
void gui_gfx_draw_string_16_shadow(int x, int y, const char* str, uint32_t fg, uint32_t shadow) {
|
||||
gui_gfx_draw_string_16(x + 1, y + 1, str, shadow);
|
||||
gui_gfx_draw_string_16(x, y, str, fg);
|
||||
}
|
||||
|
||||
void gui_gfx_draw_shadow(int x, int y, int w, int h, int blur) {
|
||||
for (int r = 1; r <= blur; r++) {
|
||||
uint8_t alpha = (uint8_t)(50 / r);
|
||||
gui_gfx_fill_rect_blend(x + r, y + h, w, r, GUI_COLOR_BLACK, alpha);
|
||||
gui_gfx_fill_rect_blend(x + w, y + r, r, h, GUI_COLOR_BLACK, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_gfx_blit(int dx, int dy, int w, int h, const uint32_t* src_buf) {
|
||||
if (!g_backbuffer || !src_buf) return;
|
||||
for (int j = 0; j < h; j++) {
|
||||
int py = dy + j;
|
||||
if (py < 0 || py >= g_height) continue;
|
||||
for (int i = 0; i < w; i++) {
|
||||
int px = dx + i;
|
||||
if (px < 0 || px >= g_width) continue;
|
||||
uint32_t pixel = src_buf[j * w + i];
|
||||
if ((pixel >> 24) > 0) {
|
||||
g_backbuffer[py * g_width + px] = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Push the back buffer to the screen, writing only what changed.
|
||||
*
|
||||
* Every frame is still composited in full, so the cheapest reliable way to
|
||||
* find the damage is to diff against the shadow copy: a RAM read is orders of
|
||||
* magnitude cheaper than the framebuffer write it avoids. A typical idle
|
||||
* desktop frame only moves the cursor and the clock, which turns a 3.6 MB
|
||||
* blit at 1280x720 into a few kilobytes.
|
||||
*
|
||||
* Each row is narrowed to the span between its first and last differing pixel
|
||||
* rather than tracked as a set of runs: one memcpy per row keeps the inner
|
||||
* loop tight, and a row that changed in two places is rare enough that
|
||||
* bridging the gap costs less than the extra bookkeeping.
|
||||
*/
|
||||
void gui_gfx_present(void) {
|
||||
if (!g_frontbuffer || !g_backbuffer) return;
|
||||
|
||||
size_t pixels = (size_t)g_width * (size_t)g_height;
|
||||
|
||||
/* No shadow (allocation failed) or it does not describe the screen yet:
|
||||
* push the whole frame and start tracking from there. */
|
||||
if (!g_shadow || !g_shadow_valid) {
|
||||
memcpy(g_frontbuffer, g_backbuffer, pixels * sizeof(uint32_t));
|
||||
if (g_shadow) {
|
||||
memcpy(g_shadow, g_backbuffer, pixels * sizeof(uint32_t));
|
||||
g_shadow_valid = true;
|
||||
}
|
||||
g_present_pixels = pixels;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t pushed = 0;
|
||||
for (int y = 0; y < g_height; y++) {
|
||||
size_t row = (size_t)y * (size_t)g_width;
|
||||
const uint32_t* back = g_backbuffer + row;
|
||||
uint32_t* shadow = g_shadow + row;
|
||||
|
||||
int x0 = 0;
|
||||
while (x0 < g_width && back[x0] == shadow[x0]) x0++;
|
||||
if (x0 == g_width) continue; /* row is untouched */
|
||||
|
||||
int x1 = g_width - 1;
|
||||
while (x1 > x0 && back[x1] == shadow[x1]) x1--;
|
||||
|
||||
size_t span = (size_t)(x1 - x0 + 1);
|
||||
memcpy(g_frontbuffer + row + x0, back + x0, span * sizeof(uint32_t));
|
||||
memcpy(shadow + x0, back + x0, span * sizeof(uint32_t));
|
||||
pushed += span;
|
||||
}
|
||||
|
||||
g_present_pixels = pushed;
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// SUB-OS Desktop Icon Set: 16x16 palette-indexed glyphs
|
||||
//
|
||||
// Each icon is 16 rows of 16 hex digits, where the digit indexes the shared
|
||||
// palette below. Storing the art as text keeps it editable by hand, and the
|
||||
// indirection lets every glyph pick up a theme change for free.
|
||||
|
||||
#include <gui/gui_icons.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
|
||||
#define ICON_PALETTE_SIZE 16
|
||||
|
||||
static const uint32_t g_palette[ICON_PALETTE_SIZE] = {
|
||||
0x00000000, // 0 transparent
|
||||
0xFF0B0F19, // 1 outline / near-black
|
||||
0xFFF8FAFC, // 2 white
|
||||
0xFF94A3B8, // 3 muted grey
|
||||
0xFF475569, // 4 dark grey
|
||||
0xFF38BDF8, // 5 primary sky
|
||||
0xFF0284C7, // 6 primary dark
|
||||
0xFF34D399, // 7 success green
|
||||
0xFFFBBF24, // 8 warning amber
|
||||
0xFFF87171, // 9 danger red
|
||||
0xFF818CF8, // A accent indigo
|
||||
0xFFEC4899, // B pink
|
||||
0xFF1E293B, // C surface
|
||||
0xFF334155, // D elevated
|
||||
0xFF000000, // E pure black
|
||||
0xFFD9A441 // F folder tan
|
||||
};
|
||||
|
||||
static const char* g_icons[GUI_ICON_COUNT][GUI_ICON_SIZE] = {
|
||||
// GUI_ICON_TERMINAL
|
||||
{
|
||||
"4111111111111114",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C7CCCCCCCCCCCC4",
|
||||
"4C77CCCCCCCCCCC4",
|
||||
"4C777CCCCCCCCCC4",
|
||||
"4C7777CCCCCCCCC4",
|
||||
"4C777CCCCCCCCCC4",
|
||||
"4C77CCCCCCCCCCC4",
|
||||
"4C7CCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CC77777777CCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4111111111111114",
|
||||
},
|
||||
// GUI_ICON_MONITOR
|
||||
{
|
||||
"4111111111111114",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCC5C4",
|
||||
"4CCCCCCCCCCC5CC4",
|
||||
"4CCCCCCCCC55CCC4",
|
||||
"4CCCCCCCC5CCCCC4",
|
||||
"4CCCCCC55CCCCCC4",
|
||||
"4CCCC55CCCCCCCC4",
|
||||
"4CC55CCCCCCCCCC4",
|
||||
"4C5CCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4111111111111114",
|
||||
"0000444444440000",
|
||||
"0000004444000000",
|
||||
"0000444444440000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_FOLDER
|
||||
{
|
||||
"0000000000000000",
|
||||
"0FFFFF0000000000",
|
||||
"0F888FF000000000",
|
||||
"0F88888FFFFFFF00",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0F8888888888F000",
|
||||
"0FFFFFFFFFFFF000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_CALC
|
||||
{
|
||||
"4111111111111114",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C777777777777C4",
|
||||
"4C777777777777C4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C2CC2CC2CC2CCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C2CC2CC2CC2CCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C2CC2CC2CC2CCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4C2CC2CC2CC5CCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4CCCCCCCCCCCCCC4",
|
||||
"4111111111111114",
|
||||
},
|
||||
// GUI_ICON_PAINT
|
||||
{
|
||||
"0000000000000000",
|
||||
"0000333333000000",
|
||||
"0003222222300000",
|
||||
"0032955992230000",
|
||||
"0329999999923000",
|
||||
"0329B99997923000",
|
||||
"0329999999923000",
|
||||
"0032999999230000",
|
||||
"0003299992300000",
|
||||
"0000322223000000",
|
||||
"0000033330000000",
|
||||
"0000004440000000",
|
||||
"0000004440000000",
|
||||
"0000004440000000",
|
||||
"0000000000000000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_INFO
|
||||
{
|
||||
"0000055555000000",
|
||||
"0000555555550000",
|
||||
"0005566665550000",
|
||||
"0055666666655000",
|
||||
"0556662266665500",
|
||||
"0556662266665500",
|
||||
"0556666666665500",
|
||||
"0556662266665500",
|
||||
"0556622226665500",
|
||||
"0556666226665500",
|
||||
"0556666226665500",
|
||||
"0556662222265500",
|
||||
"0055666666655000",
|
||||
"0005566665550000",
|
||||
"0000555555550000",
|
||||
"0000055555000000",
|
||||
},
|
||||
// GUI_ICON_SETTINGS
|
||||
{
|
||||
"0000034443000000",
|
||||
"0000034443000000",
|
||||
"0003344444330000",
|
||||
"0034444444443000",
|
||||
"0344444444444300",
|
||||
"3444442224444430",
|
||||
"4444422222444444",
|
||||
"4444422222444444",
|
||||
"4444422222444444",
|
||||
"3444442224444430",
|
||||
"0344444444444300",
|
||||
"0034444444443000",
|
||||
"0003344444330000",
|
||||
"0000034443000000",
|
||||
"0000034443000000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_TASKS
|
||||
{
|
||||
"0000000000000000",
|
||||
"0555555555555550",
|
||||
"0500000000000050",
|
||||
"0507000000000050",
|
||||
"0507700000000050",
|
||||
"0507770000000050",
|
||||
"0507777000000050",
|
||||
"0500000000000050",
|
||||
"0508880000000050",
|
||||
"0508888000000050",
|
||||
"0500000000000050",
|
||||
"0509990000000050",
|
||||
"0509999000000050",
|
||||
"0500000000000050",
|
||||
"0555555555555550",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_LOG
|
||||
{
|
||||
"0022222222222000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0240000000042000",
|
||||
"0244444444442000",
|
||||
"0022222222222000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_FLASK
|
||||
{
|
||||
"0000022222000000",
|
||||
"0000020002000000",
|
||||
"0000020002000000",
|
||||
"0000020002000000",
|
||||
"0000020002000000",
|
||||
"0000200000200000",
|
||||
"0000200000200000",
|
||||
"0002000000020000",
|
||||
"0002077777020000",
|
||||
"0020777777702000",
|
||||
"0020777777702000",
|
||||
"0207777777770200",
|
||||
"0207777777770200",
|
||||
"0027777777772000",
|
||||
"0002222222220000",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_CLOCK
|
||||
{
|
||||
"0000055555000000",
|
||||
"0005522222550000",
|
||||
"0052222222225000",
|
||||
"0522222222222500",
|
||||
"0522221222222500",
|
||||
"5222221222222250",
|
||||
"5222221222222250",
|
||||
"5222221222222250",
|
||||
"5222221111122250",
|
||||
"5222222222222250",
|
||||
"5222222222222250",
|
||||
"0522222222222500",
|
||||
"0522222222222500",
|
||||
"0052222222225000",
|
||||
"0005522222550000",
|
||||
"0000055555000000",
|
||||
},
|
||||
// GUI_ICON_FILE
|
||||
{
|
||||
"0022222222200000",
|
||||
"0244444444200000",
|
||||
"0244444444420000",
|
||||
"0244444444442000",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0244444444444200",
|
||||
"0222222222222200",
|
||||
"0000000000000000",
|
||||
},
|
||||
// GUI_ICON_POWER
|
||||
{
|
||||
"0000009900000000",
|
||||
"0000009900000000",
|
||||
"0000999999000000",
|
||||
"0009900009900000",
|
||||
"0099009900990000",
|
||||
"0990009900099000",
|
||||
"0990009900099000",
|
||||
"9900009900009900",
|
||||
"9900009900009900",
|
||||
"9900000000009900",
|
||||
"9900000000009900",
|
||||
"0990000000099000",
|
||||
"0990000000099000",
|
||||
"0099000000990000",
|
||||
"0009999999900000",
|
||||
"0000099999000000",
|
||||
},
|
||||
// GUI_ICON_WARNING
|
||||
{
|
||||
"0000008800000000",
|
||||
"0000008800000000",
|
||||
"0000088880000000",
|
||||
"0000088880000000",
|
||||
"0000881188000000",
|
||||
"0000881188000000",
|
||||
"0008811118800000",
|
||||
"0008811118800000",
|
||||
"0088111111880000",
|
||||
"0088111111880000",
|
||||
"0881111111188000",
|
||||
"0881118811188000",
|
||||
"8811118811118800",
|
||||
"8811118811118800",
|
||||
"8888888888888888",
|
||||
"8888888888888888",
|
||||
},};
|
||||
|
||||
static const char* g_icon_names[GUI_ICON_COUNT] = {
|
||||
"Terminal", "Monitor", "Folder", "Calculator", "Paint", "Info",
|
||||
"Settings", "Tasks", "Log", "Tests", "Clock", "File", "Power", "Warning"
|
||||
};
|
||||
|
||||
static uint8_t hex_index(char c) {
|
||||
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
|
||||
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
|
||||
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void gui_icons_init(void) {
|
||||
// The glyph table is static const; nothing to build at runtime.
|
||||
}
|
||||
|
||||
const char* gui_icon_name(gui_icon_id_t id) {
|
||||
if (id < 0 || id >= GUI_ICON_COUNT) return "Unknown";
|
||||
return g_icon_names[id];
|
||||
}
|
||||
|
||||
void gui_icon_draw_scaled(gui_icon_id_t id, int x, int y, int scale) {
|
||||
if (id < 0 || id >= GUI_ICON_COUNT) return;
|
||||
if (scale < 1) scale = 1;
|
||||
|
||||
for (int row = 0; row < GUI_ICON_SIZE; row++) {
|
||||
const char* line = g_icons[id][row];
|
||||
if (!line) continue;
|
||||
|
||||
for (int col = 0; col < GUI_ICON_SIZE; col++) {
|
||||
uint32_t color = g_palette[hex_index(line[col])];
|
||||
if ((color >> 24) == 0) continue; // Transparent pixel
|
||||
|
||||
if (scale == 1) {
|
||||
gui_gfx_draw_pixel(x + col, y + row, color);
|
||||
} else {
|
||||
gui_gfx_fill_rect(x + col * scale, y + row * scale, scale, scale, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_icon_draw(gui_icon_id_t id, int x, int y) {
|
||||
gui_icon_draw_scaled(id, x, y, 1);
|
||||
}
|
||||
|
||||
void gui_icon_draw_tinted(gui_icon_id_t id, int x, int y, int scale, uint32_t tint) {
|
||||
if (id < 0 || id >= GUI_ICON_COUNT) return;
|
||||
if (scale < 1) scale = 1;
|
||||
|
||||
// Every opaque pixel becomes the tint; the glyph acts purely as a stencil.
|
||||
for (int row = 0; row < GUI_ICON_SIZE; row++) {
|
||||
const char* line = g_icons[id][row];
|
||||
if (!line) continue;
|
||||
|
||||
for (int col = 0; col < GUI_ICON_SIZE; col++) {
|
||||
if ((g_palette[hex_index(line[col])] >> 24) == 0) continue;
|
||||
|
||||
if (scale == 1) {
|
||||
gui_gfx_draw_pixel(x + col, y + row, tint);
|
||||
} else {
|
||||
gui_gfx_fill_rect(x + col * scale, y + row * scale, scale, scale, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
// SUB-OS Desktop Terminal Emulator
|
||||
//
|
||||
// Commands typed here run through the same LazyBox applet table the kernel TTY
|
||||
// uses. LazyBox reports everything via printk, so before dispatching an applet
|
||||
// the terminal installs a printk sink; the applet's output is streamed into
|
||||
// the window's scrollback instead of the text console, ANSI colour codes are
|
||||
// translated to per-line colours, and the sink is removed afterwards.
|
||||
|
||||
#include <gui/gui_terminal.h>
|
||||
#include <gui/gui_widgets.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <userland/lazybox.h>
|
||||
#include <userland/shell.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <fs/vfs.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
|
||||
#define TERM_SCROLLBACK 256
|
||||
#define TERM_LINE_CHARS 118
|
||||
#define TERM_INPUT_MAX 120
|
||||
#define TERM_HISTORY 16
|
||||
#define TERM_ROW_H 12
|
||||
#define TERM_ARGV_MAX 16
|
||||
|
||||
typedef struct {
|
||||
char text[TERM_LINE_CHARS + 1];
|
||||
uint32_t color;
|
||||
} term_line_t;
|
||||
|
||||
typedef struct {
|
||||
term_line_t lines[TERM_SCROLLBACK];
|
||||
int line_count; // Total lines ever written, capped at TERM_SCROLLBACK
|
||||
int scroll; // First visible line
|
||||
bool follow_tail; // Auto-scroll while new output arrives
|
||||
|
||||
char input[TERM_INPUT_MAX + 1];
|
||||
int input_len;
|
||||
|
||||
char history[TERM_HISTORY][TERM_INPUT_MAX + 1];
|
||||
int history_count;
|
||||
int history_pos; // -1 when editing a fresh line
|
||||
|
||||
// Partial line being assembled from sink writes
|
||||
char pending[TERM_LINE_CHARS + 1];
|
||||
int pending_len;
|
||||
uint32_t pending_color;
|
||||
bool busy;
|
||||
} term_data_t;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scrollback
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void term_scroll_to_tail(term_data_t* td, int visible_rows) {
|
||||
td->scroll = td->line_count - visible_rows;
|
||||
if (td->scroll < 0) td->scroll = 0;
|
||||
}
|
||||
|
||||
static void term_push_line(term_data_t* td, const char* text, uint32_t color) {
|
||||
if (td->line_count == TERM_SCROLLBACK) {
|
||||
// Full: drop the oldest line and shift the window up by one.
|
||||
for (int i = 0; i < TERM_SCROLLBACK - 1; i++) {
|
||||
td->lines[i] = td->lines[i + 1];
|
||||
}
|
||||
td->line_count--;
|
||||
if (td->scroll > 0) td->scroll--;
|
||||
}
|
||||
|
||||
term_line_t* line = &td->lines[td->line_count++];
|
||||
strncpy(line->text, text ? text : "", TERM_LINE_CHARS);
|
||||
line->text[TERM_LINE_CHARS] = '\0';
|
||||
line->color = color;
|
||||
}
|
||||
|
||||
static void term_flush_pending(term_data_t* td) {
|
||||
td->pending[td->pending_len] = '\0';
|
||||
term_push_line(td, td->pending, td->pending_color);
|
||||
td->pending_len = 0;
|
||||
td->pending[0] = '\0';
|
||||
}
|
||||
|
||||
static void term_printf(term_data_t* td, uint32_t color, const char* fmt, ...) {
|
||||
char buf[TERM_LINE_CHARS + 1];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
term_push_line(td, buf, color);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ANSI translation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Map an SGR parameter to the palette. Only the colour attributes LazyBox
|
||||
// actually emits are handled; everything else keeps the current colour.
|
||||
static uint32_t sgr_to_color(int code, uint32_t current) {
|
||||
switch (code) {
|
||||
case 0: return GUI_THEME_TEXT_MAIN; // reset
|
||||
case 30: case 90: return GUI_THEME_TEXT_DIM;
|
||||
case 31: case 91: return GUI_THEME_DANGER;
|
||||
case 32: case 92: return GUI_THEME_SUCCESS;
|
||||
case 33: case 93: return GUI_THEME_WARNING;
|
||||
case 34: case 94: return GUI_THEME_ACCENT;
|
||||
case 35: case 95: return 0xFFEC4899; // magenta
|
||||
case 36: case 96: return GUI_THEME_PRIMARY;
|
||||
case 37: case 97: return GUI_THEME_TEXT_MAIN;
|
||||
default: return current;
|
||||
}
|
||||
}
|
||||
|
||||
// CP437 fallback.
|
||||
//
|
||||
// The VGA text console renders code page 437 natively, so LazyBox draws boxes
|
||||
// and colour swatches with high-ASCII bytes such as 0xDB (full block). The GUI
|
||||
// font only covers printable ASCII, so translate the shapes those applets
|
||||
// actually use rather than dropping them to '?'. Returns 0 to skip the byte.
|
||||
static char cp437_to_ascii(unsigned char c) {
|
||||
switch (c) {
|
||||
case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF: return '#'; // blocks
|
||||
case 0xFE: case 0xFD: return '#'; // small squares
|
||||
case 0xB2: return '#'; // dark shade
|
||||
case 0xB1: return ':'; // medium shade
|
||||
case 0xB0: return '.'; // light shade
|
||||
case 0xC4: case 0xCD: return '-'; // horizontal rules
|
||||
case 0xB3: case 0xBA: return '|'; // vertical rules
|
||||
case 0xDA: case 0xBF: case 0xC0: case 0xD9: // single corners
|
||||
case 0xC9: case 0xBB: case 0xC8: case 0xBC: // double corners
|
||||
case 0xC3: case 0xB4: case 0xC2: case 0xC1: case 0xC5: // tees and cross
|
||||
return '+';
|
||||
case 0x07: case 0xF9: case 0xFA: return '.'; // bullets
|
||||
case 0x1A: case 0x10: return '>'; // arrows
|
||||
case 0x1B: case 0x11: return '<';
|
||||
case 0x18: case 0x1E: return '^';
|
||||
case 0x19: case 0x1F: return 'v';
|
||||
case 0xF8: return 'o'; // degree
|
||||
default: return 0; // drop
|
||||
}
|
||||
}
|
||||
|
||||
// Consume captured bytes, splitting on newlines and folding ANSI escapes into
|
||||
// the colour of the line being assembled.
|
||||
static void term_absorb(term_data_t* td, const char* text, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = text[i];
|
||||
|
||||
if (c == '\033') {
|
||||
// CSI sequence: ESC [ params letter
|
||||
size_t j = i + 1;
|
||||
if (j < len && text[j] == '[') {
|
||||
j++;
|
||||
int value = 0;
|
||||
bool have_value = false;
|
||||
|
||||
while (j < len) {
|
||||
char p = text[j];
|
||||
if (p >= '0' && p <= '9') {
|
||||
value = value * 10 + (p - '0');
|
||||
have_value = true;
|
||||
j++;
|
||||
} else if (p == ';') {
|
||||
if (have_value) td->pending_color = sgr_to_color(value, td->pending_color);
|
||||
value = 0;
|
||||
have_value = false;
|
||||
j++;
|
||||
} else {
|
||||
if (p == 'm') {
|
||||
td->pending_color = sgr_to_color(have_value ? value : 0,
|
||||
td->pending_color);
|
||||
}
|
||||
j++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i = j - 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\n') {
|
||||
term_flush_pending(td);
|
||||
continue;
|
||||
}
|
||||
if (c == '\r') {
|
||||
td->pending_len = 0;
|
||||
continue;
|
||||
}
|
||||
if (c == '\t') {
|
||||
// Expand to the next 8-column stop.
|
||||
int stop = (td->pending_len + 8) & ~7;
|
||||
while (td->pending_len < stop && td->pending_len < TERM_LINE_CHARS) {
|
||||
td->pending[td->pending_len++] = ' ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
unsigned char uc = (unsigned char)c;
|
||||
if (uc < 32 || uc > 126) {
|
||||
char sub = cp437_to_ascii(uc);
|
||||
if (!sub) continue; // Control byte or a glyph with no ASCII stand-in
|
||||
c = sub;
|
||||
}
|
||||
|
||||
if (td->pending_len >= TERM_LINE_CHARS) term_flush_pending(td);
|
||||
td->pending[td->pending_len++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
static void term_sink(const char* text, size_t len, void* ctx) {
|
||||
term_absorb((term_data_t*)ctx, text, len);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command execution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Applets that read the keyboard directly would block the compositor, since
|
||||
// the desktop drives input from its own frame loop.
|
||||
static bool applet_is_interactive(const char* name) {
|
||||
return strcmp(name, "nano") == 0 || strcmp(name, "snake") == 0 ||
|
||||
strcmp(name, "sh") == 0 || strcmp(name, "top") == 0;
|
||||
}
|
||||
|
||||
static void term_run_command(term_data_t* td, const char* cmd) {
|
||||
char work[TERM_INPUT_MAX + 1];
|
||||
strncpy(work, cmd, TERM_INPUT_MAX);
|
||||
work[TERM_INPUT_MAX] = '\0';
|
||||
|
||||
// Same quote-aware tokenizer the TTY shell uses.
|
||||
char* argv[TERM_ARGV_MAX];
|
||||
int argc = 0;
|
||||
char* token = work;
|
||||
|
||||
while (*token) {
|
||||
while (*token == ' ') *token++ = '\0';
|
||||
if (*token == '\0') break;
|
||||
|
||||
if (*token == '"' || *token == '\'') {
|
||||
char quote = *token++;
|
||||
if (argc < TERM_ARGV_MAX) argv[argc++] = token;
|
||||
while (*token && *token != quote) token++;
|
||||
if (*token == quote) *token++ = '\0';
|
||||
} else {
|
||||
if (argc < TERM_ARGV_MAX) argv[argc++] = token;
|
||||
while (*token && *token != ' ') token++;
|
||||
}
|
||||
}
|
||||
if (argc == 0) return;
|
||||
|
||||
// Terminal built-ins first.
|
||||
if (strcmp(argv[0], "clear") == 0 || strcmp(argv[0], "cls") == 0) {
|
||||
td->line_count = 0;
|
||||
td->scroll = 0;
|
||||
return;
|
||||
}
|
||||
if (strcmp(argv[0], "exit") == 0 || strcmp(argv[0], "quit") == 0) {
|
||||
term_printf(td, GUI_THEME_TEXT_DIM, "Close the window to end this session.");
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_applet = lazybox_has_applet(argv[0]);
|
||||
bool is_builtin = shell_is_builtin(argv[0]);
|
||||
|
||||
if (!is_applet && !is_builtin) {
|
||||
term_printf(td, GUI_THEME_DANGER,
|
||||
"%s: command not found. Try 'lazybox' for the applet list.", argv[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_applet && applet_is_interactive(argv[0])) {
|
||||
term_printf(td, GUI_THEME_WARNING,
|
||||
"%s reads the keyboard directly and would freeze the desktop.",
|
||||
argv[0]);
|
||||
term_printf(td, GUI_THEME_TEXT_DIM,
|
||||
"Run it from the kernel TTY instead (Esc leaves the desktop).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture printk output into this window's scrollback for the duration of
|
||||
// the command. Applets take precedence over builtins, matching the TTY.
|
||||
td->busy = true;
|
||||
td->pending_len = 0;
|
||||
td->pending_color = GUI_THEME_TEXT_MAIN;
|
||||
|
||||
printk_set_sink(term_sink, td);
|
||||
if (is_applet) {
|
||||
lazybox_run_applet(argv[0], argc, argv);
|
||||
} else {
|
||||
shell_execute_builtin(cmd, argc, argv, false);
|
||||
}
|
||||
printk_clear_sink();
|
||||
|
||||
if (td->pending_len > 0) term_flush_pending(td);
|
||||
td->busy = false;
|
||||
}
|
||||
|
||||
static void term_submit(term_data_t* td) {
|
||||
const char* cwd = vfs_getcwd();
|
||||
|
||||
term_printf(td, GUI_THEME_SUCCESS, "%s $ %s", cwd ? cwd : "/", td->input);
|
||||
|
||||
if (td->input_len > 0) {
|
||||
// Record in history, skipping an immediate repeat.
|
||||
bool repeat = (td->history_count > 0 &&
|
||||
strcmp(td->history[td->history_count - 1], td->input) == 0);
|
||||
if (!repeat) {
|
||||
if (td->history_count == TERM_HISTORY) {
|
||||
for (int i = 0; i < TERM_HISTORY - 1; i++) {
|
||||
strcpy(td->history[i], td->history[i + 1]);
|
||||
}
|
||||
td->history_count--;
|
||||
}
|
||||
strcpy(td->history[td->history_count++], td->input);
|
||||
}
|
||||
|
||||
term_run_command(td, td->input);
|
||||
}
|
||||
|
||||
td->input[0] = '\0';
|
||||
td->input_len = 0;
|
||||
td->history_pos = -1;
|
||||
td->follow_tail = true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Input handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void term_recall_history(term_data_t* td, int direction) {
|
||||
if (td->history_count == 0) return;
|
||||
|
||||
if (td->history_pos == -1) {
|
||||
td->history_pos = (direction < 0) ? td->history_count - 1 : -1;
|
||||
} else {
|
||||
td->history_pos += (direction < 0) ? -1 : 1;
|
||||
}
|
||||
|
||||
if (td->history_pos < 0) td->history_pos = 0;
|
||||
if (td->history_pos >= td->history_count) {
|
||||
// Past the newest entry: back to an empty prompt.
|
||||
td->history_pos = -1;
|
||||
td->input[0] = '\0';
|
||||
td->input_len = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(td->input, td->history[td->history_pos], TERM_INPUT_MAX);
|
||||
td->input[TERM_INPUT_MAX] = '\0';
|
||||
td->input_len = (int)strlen(td->input);
|
||||
}
|
||||
|
||||
static void terminal_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
term_data_t* td = (term_data_t*)win->user_data;
|
||||
if (!td) return;
|
||||
|
||||
if (ev->type == GUI_EVENT_CLOSE) {
|
||||
// A sink pointing at freed memory would corrupt the next printk.
|
||||
if (td->busy) printk_clear_sink();
|
||||
kfree(td);
|
||||
win->user_data = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev->type == GUI_EVENT_KEY_DOWN) {
|
||||
uint16_t key = ev->key;
|
||||
int visible = (win->height - GUI_TITLEBAR_HEIGHT - 40) / TERM_ROW_H;
|
||||
if (visible < 1) visible = 1;
|
||||
|
||||
if (key & KEY_SPECIAL_FLAG) {
|
||||
switch (key) {
|
||||
case KEY_UP: term_recall_history(td, -1); break;
|
||||
case KEY_DOWN: term_recall_history(td, +1); break;
|
||||
case KEY_PAGE_UP:
|
||||
td->follow_tail = false;
|
||||
td->scroll -= visible;
|
||||
if (td->scroll < 0) td->scroll = 0;
|
||||
break;
|
||||
case KEY_PAGE_DOWN:
|
||||
td->scroll += visible;
|
||||
term_scroll_to_tail(td, visible);
|
||||
td->follow_tail = true;
|
||||
break;
|
||||
case KEY_HOME:
|
||||
td->follow_tail = false;
|
||||
td->scroll = 0;
|
||||
break;
|
||||
case KEY_END:
|
||||
td->follow_tail = true;
|
||||
term_scroll_to_tail(td, visible);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
char c = (char)(key & 0xFF);
|
||||
if (c == '\n' || c == '\r') {
|
||||
term_submit(td);
|
||||
} else if (c == '\b' || (uint8_t)c == 0x7F) {
|
||||
if (td->input_len > 0) td->input[--td->input_len] = '\0';
|
||||
} else if (c >= 32 && c <= 126 && td->input_len < TERM_INPUT_MAX) {
|
||||
td->input[td->input_len++] = c;
|
||||
td->input[td->input_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
gui_widget_feed_event(win, ev);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void terminal_paint(gui_window_t* win) {
|
||||
term_data_t* td = (term_data_t*)win->user_data;
|
||||
if (!td) return;
|
||||
|
||||
int cx = win->x + 1;
|
||||
int cy = win->y + GUI_TITLEBAR_HEIGHT + 1;
|
||||
int cw = win->width - 2;
|
||||
int ch = win->height - GUI_TITLEBAR_HEIGHT - 2;
|
||||
|
||||
gui_gfx_fill_rect(cx, cy, cw, ch, 0xFF080B14);
|
||||
|
||||
int pad = 6;
|
||||
int prompt_h = 18;
|
||||
int status_h = 12;
|
||||
int view_h = ch - prompt_h - status_h - pad;
|
||||
int visible = view_h / TERM_ROW_H;
|
||||
if (visible < 1) visible = 1;
|
||||
|
||||
int max_cols = (cw - pad * 2) / 8;
|
||||
if (max_cols < 1) max_cols = 1;
|
||||
if (max_cols > TERM_LINE_CHARS) max_cols = TERM_LINE_CHARS;
|
||||
|
||||
if (td->follow_tail) term_scroll_to_tail(td, visible);
|
||||
if (td->scroll > td->line_count - visible) term_scroll_to_tail(td, visible);
|
||||
if (td->scroll < 0) td->scroll = 0;
|
||||
|
||||
// Scrollback
|
||||
for (int row = 0; row < visible; row++) {
|
||||
int idx = td->scroll + row;
|
||||
if (idx >= td->line_count) break;
|
||||
|
||||
char clipped[TERM_LINE_CHARS + 1];
|
||||
strncpy(clipped, td->lines[idx].text, (size_t)max_cols);
|
||||
clipped[max_cols] = '\0';
|
||||
|
||||
gui_gfx_draw_string(cx + pad, cy + pad + row * TERM_ROW_H, clipped,
|
||||
td->lines[idx].color);
|
||||
}
|
||||
|
||||
// Prompt
|
||||
int prompt_y = cy + ch - prompt_h - status_h;
|
||||
gui_gfx_draw_line(cx + 1, prompt_y - 3, cx + cw - 2, prompt_y - 3, GUI_THEME_BORDER);
|
||||
|
||||
const char* cwd = vfs_getcwd();
|
||||
char prompt[40];
|
||||
snprintf(prompt, sizeof(prompt), "%s $ ", cwd ? cwd : "/");
|
||||
int prompt_w = (int)strlen(prompt) * 8;
|
||||
|
||||
gui_gfx_draw_string(cx + pad, prompt_y + 2, prompt, GUI_THEME_PRIMARY);
|
||||
|
||||
// Show the tail of a long command line so the cursor stays visible.
|
||||
int room = max_cols - (int)strlen(prompt) - 1;
|
||||
if (room < 1) room = 1;
|
||||
const char* view = (td->input_len > room) ? td->input + (td->input_len - room)
|
||||
: td->input;
|
||||
gui_gfx_draw_string(cx + pad + prompt_w, prompt_y + 2, view, GUI_THEME_TEXT_MAIN);
|
||||
|
||||
if (win->active) {
|
||||
int cursor_x = cx + pad + prompt_w + (int)strlen(view) * 8;
|
||||
gui_gfx_fill_rect(cursor_x, prompt_y + 1, 8, 10, GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
// Status line
|
||||
char status[72];
|
||||
if (td->line_count > visible && !td->follow_tail) {
|
||||
snprintf(status, sizeof(status), "%d lines showing %d-%d [End] to follow",
|
||||
td->line_count, td->scroll + 1,
|
||||
(td->scroll + visible < td->line_count) ? td->scroll + visible
|
||||
: td->line_count);
|
||||
} else {
|
||||
snprintf(status, sizeof(status), "%d lines Up/Down history PgUp/PgDn scroll",
|
||||
td->line_count);
|
||||
}
|
||||
gui_gfx_draw_string(cx + pad, cy + ch - status_h, status, GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
|
||||
void gui_app_terminal_launch(int x, int y, int w, int h) {
|
||||
gui_window_t* win = gui_wm_create_window("Terminal", x, y,
|
||||
(w > 0) ? w : 520, (h > 0) ? h : 320);
|
||||
if (!win) return;
|
||||
|
||||
term_data_t* td = (term_data_t*)kzalloc(sizeof(term_data_t));
|
||||
if (!td) return;
|
||||
|
||||
td->history_pos = -1;
|
||||
td->follow_tail = true;
|
||||
td->pending_color = GUI_THEME_TEXT_MAIN;
|
||||
|
||||
term_printf(td, GUI_THEME_PRIMARY, "SUB-OS Terminal - LazyBox %s", LAZYBOX_VERSION);
|
||||
term_printf(td, GUI_THEME_TEXT_MUTED,
|
||||
"LazyBox applets and shell builtins both run here. Try 'ls',");
|
||||
term_printf(td, GUI_THEME_TEXT_MUTED,
|
||||
"'uname -a', 'free', 'neofetch', 'lspci', 'ktest', 'help'.");
|
||||
term_printf(td, GUI_THEME_TEXT_DIM, "");
|
||||
|
||||
win->user_data = td;
|
||||
win->paint = terminal_paint;
|
||||
win->handle_event = terminal_event;
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
// SUB-WT: Immediate-mode widget toolkit for the SUB-OS desktop
|
||||
#include <gui/gui_widgets.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
|
||||
static gui_widget_input_t g_input;
|
||||
static gui_window_t* g_win = NULL;
|
||||
static int g_origin_x = 0;
|
||||
static int g_origin_y = 0;
|
||||
static int g_client_w = 0;
|
||||
static int g_client_h = 0;
|
||||
|
||||
// Screen-space helpers: widgets are authored in client coordinates.
|
||||
static inline int SX(int x) { return g_origin_x + x; }
|
||||
static inline int SY(int y) { return g_origin_y + y; }
|
||||
|
||||
static bool hit_test(int x, int y, int w, int h) {
|
||||
return g_input.mouse_x >= x && g_input.mouse_x < x + w &&
|
||||
g_input.mouse_y >= y && g_input.mouse_y < y + h;
|
||||
}
|
||||
|
||||
static int text_width(const char* s) {
|
||||
return s ? (int)strlen(s) * GUI_FONT_W : 0;
|
||||
}
|
||||
|
||||
void gui_widget_feed_event(gui_window_t* win, const gui_event_t* ev) {
|
||||
if (!ev) return;
|
||||
|
||||
switch (ev->type) {
|
||||
case GUI_EVENT_MOUSE_DOWN:
|
||||
g_input.mouse_x = ev->rel_x;
|
||||
g_input.mouse_y = ev->rel_y;
|
||||
g_input.mouse_down = true;
|
||||
g_input.mouse_clicked = true;
|
||||
break;
|
||||
case GUI_EVENT_MOUSE_UP:
|
||||
g_input.mouse_x = ev->rel_x;
|
||||
g_input.mouse_y = ev->rel_y;
|
||||
g_input.mouse_down = false;
|
||||
g_input.mouse_released = true;
|
||||
g_input.active_id = 0;
|
||||
break;
|
||||
case GUI_EVENT_MOUSE_MOVE:
|
||||
g_input.mouse_x = ev->rel_x;
|
||||
g_input.mouse_y = ev->rel_y;
|
||||
g_input.mouse_down = ev->btn_left;
|
||||
break;
|
||||
case GUI_EVENT_KEY_DOWN:
|
||||
g_input.key = ev->key;
|
||||
break;
|
||||
case GUI_EVENT_CLOSE:
|
||||
g_input.focus_id = 0;
|
||||
g_input.active_id = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
(void)win;
|
||||
}
|
||||
|
||||
void gui_widget_begin(gui_window_t* win) {
|
||||
g_win = win;
|
||||
if (!win) return;
|
||||
|
||||
g_origin_x = win->x + 1;
|
||||
g_origin_y = win->y + GUI_TITLEBAR_HEIGHT + 1;
|
||||
g_client_w = win->width - 2;
|
||||
g_client_h = win->height - GUI_TITLEBAR_HEIGHT - 2;
|
||||
g_input.hot_id = 0;
|
||||
}
|
||||
|
||||
void gui_widget_end(void) {
|
||||
// One-shot inputs live for exactly one paint pass; anything not claimed
|
||||
// by a widget this frame is discarded rather than replayed later.
|
||||
g_input.mouse_clicked = false;
|
||||
g_input.mouse_released = false;
|
||||
g_input.key = 0;
|
||||
g_win = NULL;
|
||||
}
|
||||
|
||||
const gui_widget_input_t* gui_widget_get_input(void) { return &g_input; }
|
||||
void gui_widget_set_focus(int id) { g_input.focus_id = id; }
|
||||
int gui_widget_client_width(void) { return g_client_w; }
|
||||
int gui_widget_client_height(void) { return g_client_h; }
|
||||
|
||||
// ===========================================================================
|
||||
// Static content
|
||||
// ===========================================================================
|
||||
|
||||
void gui_label(int x, int y, const char* text, uint32_t color) {
|
||||
gui_gfx_draw_string(SX(x), SY(y), text, color);
|
||||
}
|
||||
|
||||
void gui_label_bold(int x, int y, const char* text, uint32_t color) {
|
||||
gui_gfx_draw_string_16_shadow(SX(x), SY(y), text, color, GUI_COLOR_BLACK);
|
||||
}
|
||||
|
||||
void gui_label_aligned(int x, int y, int w, const char* text, uint32_t color, gui_align_t align) {
|
||||
int tw = text_width(text);
|
||||
int tx = x;
|
||||
if (align == GUI_ALIGN_CENTER) tx = x + (w - tw) / 2;
|
||||
else if (align == GUI_ALIGN_RIGHT) tx = x + w - tw;
|
||||
gui_gfx_draw_string(SX(tx), SY(y), text, color);
|
||||
}
|
||||
|
||||
void gui_panel(int x, int y, int w, int h, const char* title) {
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER);
|
||||
|
||||
if (title && *title) {
|
||||
// Caption sits astride the top border, so clear the strip behind it.
|
||||
int tw = text_width(title) + 8;
|
||||
gui_gfx_fill_rect(SX(x + 8), SY(y) - 4, tw, 9, GUI_THEME_BG_SURFACE);
|
||||
gui_gfx_draw_string(SX(x + 12), SY(y) - 4, title, GUI_THEME_TEXT_MUTED);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_separator(int x, int y, int w) {
|
||||
gui_gfx_draw_line(SX(x), SY(y), SX(x + w - 1), SY(y), GUI_THEME_BORDER);
|
||||
}
|
||||
|
||||
void gui_progress_bar(int x, int y, int w, int h, int percent, uint32_t fill) {
|
||||
if (percent < 0) percent = 0;
|
||||
if (percent > 100) percent = 100;
|
||||
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
int filled = (w - 2) * percent / 100;
|
||||
if (filled > 0) gui_gfx_fill_rect(SX(x + 1), SY(y + 1), filled, h - 2, fill);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER);
|
||||
}
|
||||
|
||||
void gui_sparkline(int x, int y, int w, int h, const uint8_t* samples, int count, uint32_t color) {
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER);
|
||||
if (!samples || count < 2) return;
|
||||
|
||||
// Horizontal guides at 25/50/75%.
|
||||
for (int q = 1; q < 4; q++) {
|
||||
int gy = SY(y) + h * q / 4;
|
||||
gui_gfx_draw_line(SX(x) + 1, gy, SX(x) + w - 2, gy, 0xFF1E293B);
|
||||
}
|
||||
|
||||
int usable_h = h - 4;
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
int x0 = SX(x) + 1 + (i * (w - 2)) / (count - 1);
|
||||
int x1 = SX(x) + 1 + ((i + 1) * (w - 2)) / (count - 1);
|
||||
int y0 = SY(y) + h - 2 - (samples[i] * usable_h) / 100;
|
||||
int y1 = SY(y) + h - 2 - (samples[i + 1] * usable_h) / 100;
|
||||
gui_gfx_draw_line(x0, y0, x1, y1, color);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_badge(int x, int y, const char* text, uint32_t bg, uint32_t fg) {
|
||||
int w = text_width(text) + 10;
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, 14, bg);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, 14, GUI_THEME_BORDER);
|
||||
gui_gfx_draw_string(SX(x + 5), SY(y + 3), text, fg);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Interactive controls
|
||||
// ===========================================================================
|
||||
|
||||
bool gui_button_colored(int id, int x, int y, int w, int h, const char* label, uint32_t accent) {
|
||||
bool hovered = hit_test(x, y, w, h);
|
||||
bool pressed = hovered && g_input.mouse_down;
|
||||
bool clicked = false;
|
||||
|
||||
if (hovered) {
|
||||
g_input.hot_id = id;
|
||||
if (g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false; // Claim the click
|
||||
g_input.active_id = id;
|
||||
g_input.focus_id = 0;
|
||||
clicked = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t face = pressed ? GUI_THEME_BG_DARK
|
||||
: hovered ? GUI_THEME_BG_ELEVATED
|
||||
: GUI_THEME_BG_SURFACE;
|
||||
uint32_t border = hovered ? accent : GUI_THEME_BORDER;
|
||||
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, face);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, border);
|
||||
|
||||
// A pressed button shifts its label a pixel to sell the depression.
|
||||
int tx = x + (w - text_width(label)) / 2 + (pressed ? 1 : 0);
|
||||
int ty = y + (h - GUI_FONT_H) / 2 + (pressed ? 1 : 0);
|
||||
gui_gfx_draw_string(SX(tx), SY(ty), label, hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool gui_button(int id, int x, int y, int w, int h, const char* label) {
|
||||
return gui_button_colored(id, x, y, w, h, label, GUI_THEME_PRIMARY);
|
||||
}
|
||||
|
||||
bool gui_checkbox(int id, int x, int y, const char* label, bool* value) {
|
||||
if (!value) return false;
|
||||
|
||||
int box = 14;
|
||||
int w = box + 6 + text_width(label);
|
||||
bool hovered = hit_test(x, y, w, box);
|
||||
bool changed = false;
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
*value = !*value;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
gui_gfx_fill_rect(SX(x), SY(y), box, box, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), box, box, hovered ? GUI_THEME_PRIMARY : GUI_THEME_BORDER);
|
||||
|
||||
if (*value) {
|
||||
// Two strokes form the tick.
|
||||
gui_gfx_draw_line(SX(x + 3), SY(y + 7), SX(x + 6), SY(y + 10), GUI_THEME_SUCCESS);
|
||||
gui_gfx_draw_line(SX(x + 6), SY(y + 10), SX(x + 11), SY(y + 3), GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
gui_gfx_draw_string(SX(x + box + 6), SY(y + 3), label,
|
||||
hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED);
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool gui_radio(int id, int x, int y, const char* label, int* selected, int this_value) {
|
||||
if (!selected) return false;
|
||||
|
||||
int box = 12;
|
||||
int w = box + 6 + text_width(label);
|
||||
bool hovered = hit_test(x, y, w, box);
|
||||
bool changed = false;
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
if (*selected != this_value) {
|
||||
*selected = this_value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
gui_gfx_draw_rounded_rect(SX(x), SY(y), box, box, 5,
|
||||
hovered ? GUI_THEME_PRIMARY : GUI_THEME_BORDER, false);
|
||||
if (*selected == this_value) {
|
||||
gui_gfx_fill_rect(SX(x + 4), SY(y + 4), 4, 4, GUI_THEME_PRIMARY);
|
||||
}
|
||||
|
||||
gui_gfx_draw_string(SX(x + box + 6), SY(y + 2), label,
|
||||
hovered ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED);
|
||||
(void)id;
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool gui_slider(int id, int x, int y, int w, int* value, int min_val, int max_val) {
|
||||
if (!value || max_val <= min_val) return false;
|
||||
|
||||
int track_h = 6;
|
||||
int knob_w = 10;
|
||||
int hit_h = 18;
|
||||
bool hovered = hit_test(x, y - 4, w, hit_h);
|
||||
bool changed = false;
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
g_input.active_id = id;
|
||||
}
|
||||
|
||||
// Dragging continues while the button is held, even off the track.
|
||||
if (g_input.active_id == id && g_input.mouse_down) {
|
||||
int rel = g_input.mouse_x - x - knob_w / 2;
|
||||
int span = w - knob_w;
|
||||
if (span < 1) span = 1;
|
||||
if (rel < 0) rel = 0;
|
||||
if (rel > span) rel = span;
|
||||
|
||||
int nv = min_val + (rel * (max_val - min_val)) / span;
|
||||
if (nv != *value) {
|
||||
*value = nv;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (*value < min_val) *value = min_val;
|
||||
if (*value > max_val) *value = max_val;
|
||||
|
||||
int track_y = y + (hit_h - track_h) / 2 - 4;
|
||||
gui_gfx_fill_rect(SX(x), SY(track_y), w, track_h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(track_y), w, track_h, GUI_THEME_BORDER);
|
||||
|
||||
int span = w - knob_w;
|
||||
int knob_x = x + ((*value - min_val) * span) / (max_val - min_val);
|
||||
gui_gfx_fill_rect(SX(x + 1), SY(track_y + 1), knob_x - x, track_h - 2, GUI_THEME_PRIMARY_DARK);
|
||||
gui_gfx_fill_rect(SX(knob_x), SY(y - 3), knob_w, 16,
|
||||
(g_input.active_id == id) ? GUI_THEME_PRIMARY : GUI_THEME_BG_ELEVATED);
|
||||
gui_gfx_draw_rect(SX(knob_x), SY(y - 3), knob_w, 16, GUI_THEME_PRIMARY);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool gui_listbox(int id, int x, int y, int w, int h, const char* const* items, int count, int* selected) {
|
||||
if (!items || !selected) return false;
|
||||
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER);
|
||||
|
||||
int row_h = GUI_WIDGET_ROW_H - 4;
|
||||
int visible = (h - 4) / row_h;
|
||||
bool changed = false;
|
||||
|
||||
// Scroll just enough to keep the selection on screen.
|
||||
int first = 0;
|
||||
if (*selected >= visible) first = *selected - visible + 1;
|
||||
if (first > count - visible) first = count - visible;
|
||||
if (first < 0) first = 0;
|
||||
|
||||
for (int i = 0; i < visible && first + i < count; i++) {
|
||||
int idx = first + i;
|
||||
int ry = y + 2 + i * row_h;
|
||||
bool hovered = hit_test(x + 1, ry, w - 2, row_h);
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
g_input.focus_id = id;
|
||||
if (*selected != idx) {
|
||||
*selected = idx;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx == *selected) {
|
||||
gui_gfx_fill_rect(SX(x + 1), SY(ry), w - 2, row_h, GUI_THEME_PRIMARY_DARK);
|
||||
} else if (hovered) {
|
||||
gui_gfx_fill_rect(SX(x + 1), SY(ry), w - 2, row_h, GUI_THEME_BG_ELEVATED);
|
||||
}
|
||||
|
||||
uint32_t fg = (idx == *selected) ? GUI_COLOR_WHITE : GUI_THEME_TEXT_MUTED;
|
||||
gui_gfx_draw_string(SX(x + 6), SY(ry + (row_h - GUI_FONT_H) / 2), items[idx], fg);
|
||||
}
|
||||
|
||||
// Arrow keys move the selection while this list holds focus.
|
||||
if (g_input.focus_id == id && g_input.key) {
|
||||
if (g_input.key == KEY_UP && *selected > 0) {
|
||||
(*selected)--;
|
||||
changed = true;
|
||||
g_input.key = 0;
|
||||
} else if (g_input.key == KEY_DOWN && *selected < count - 1) {
|
||||
(*selected)++;
|
||||
changed = true;
|
||||
g_input.key = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > visible) {
|
||||
gui_gfx_draw_string(SX(x + w - 14), SY(y + 3), "^", GUI_THEME_TEXT_DIM);
|
||||
gui_gfx_draw_string(SX(x + w - 14), SY(y + h - 11), "v", GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool gui_textfield(int id, int x, int y, int w, char* buffer, size_t capacity) {
|
||||
if (!buffer || capacity == 0) return false;
|
||||
|
||||
int h = 20;
|
||||
bool hovered = hit_test(x, y, w, h);
|
||||
bool focused = (g_input.focus_id == id);
|
||||
bool changed = false;
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
g_input.focus_id = id;
|
||||
focused = true;
|
||||
}
|
||||
|
||||
if (focused && g_input.key) {
|
||||
uint16_t key = g_input.key;
|
||||
char c = (char)(key & 0xFF);
|
||||
size_t len = strlen(buffer);
|
||||
|
||||
if (!(key & KEY_SPECIAL_FLAG)) {
|
||||
if (c == '\b' || (uint8_t)c == 0x7F) {
|
||||
if (len > 0) {
|
||||
buffer[len - 1] = '\0';
|
||||
changed = true;
|
||||
}
|
||||
g_input.key = 0;
|
||||
} else if (c >= 32 && c <= 126 && len + 1 < capacity) {
|
||||
buffer[len] = c;
|
||||
buffer[len + 1] = '\0';
|
||||
changed = true;
|
||||
g_input.key = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h,
|
||||
focused ? GUI_THEME_PRIMARY : (hovered ? GUI_THEME_ACCENT : GUI_THEME_BORDER));
|
||||
|
||||
// Show the tail of the text when it overflows the field.
|
||||
int max_chars = (w - 10) / GUI_FONT_W;
|
||||
int len = (int)strlen(buffer);
|
||||
const char* view = (len > max_chars) ? buffer + (len - max_chars) : buffer;
|
||||
|
||||
gui_gfx_draw_string(SX(x + 5), SY(y + (h - GUI_FONT_H) / 2), view, GUI_THEME_TEXT_MAIN);
|
||||
|
||||
if (focused) {
|
||||
int cx = x + 5 + (int)strlen(view) * GUI_FONT_W;
|
||||
gui_gfx_fill_rect(SX(cx), SY(y + 4), 2, h - 8, GUI_THEME_SUCCESS);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
int gui_tabbar(int id, int x, int y, int w, const char* const* labels, int count, int* active) {
|
||||
if (!labels || count <= 0 || !active) return -1;
|
||||
|
||||
int tab_w = w / count;
|
||||
int tab_h = 22;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
int tx = x + i * tab_w;
|
||||
bool hovered = hit_test(tx, y, tab_w, tab_h);
|
||||
bool is_active = (*active == i);
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
*active = i;
|
||||
is_active = true;
|
||||
}
|
||||
|
||||
uint32_t bg = is_active ? GUI_THEME_BG_SURFACE
|
||||
: hovered ? GUI_THEME_BG_ELEVATED
|
||||
: GUI_THEME_BG_DARK;
|
||||
gui_gfx_fill_rect(SX(tx), SY(y), tab_w, tab_h, bg);
|
||||
gui_gfx_draw_rect(SX(tx), SY(y), tab_w, tab_h, GUI_THEME_BORDER);
|
||||
|
||||
if (is_active) {
|
||||
// Active tab gets an accent underline and merges with the body.
|
||||
gui_gfx_fill_rect(SX(tx + 1), SY(y), tab_w - 2, 2, GUI_THEME_PRIMARY);
|
||||
gui_gfx_draw_line(SX(tx + 1), SY(y + tab_h - 1), SX(tx + tab_w - 2),
|
||||
SY(y + tab_h - 1), GUI_THEME_BG_SURFACE);
|
||||
}
|
||||
|
||||
int tw = text_width(labels[i]);
|
||||
gui_gfx_draw_string(SX(tx + (tab_w - tw) / 2), SY(y + (tab_h - GUI_FONT_H) / 2),
|
||||
labels[i], is_active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_DIM);
|
||||
}
|
||||
|
||||
(void)id;
|
||||
return *active;
|
||||
}
|
||||
|
||||
bool gui_scrollbar(int id, int x, int y, int h, int* offset, int total, int visible) {
|
||||
if (!offset || total <= visible || visible <= 0) return false;
|
||||
|
||||
int w = 10;
|
||||
gui_gfx_fill_rect(SX(x), SY(y), w, h, GUI_THEME_BG_DARK);
|
||||
gui_gfx_draw_rect(SX(x), SY(y), w, h, GUI_THEME_BORDER);
|
||||
|
||||
int max_offset = total - visible;
|
||||
int thumb_h = (h * visible) / total;
|
||||
if (thumb_h < 12) thumb_h = 12;
|
||||
|
||||
bool hovered = hit_test(x, y, w, h);
|
||||
bool changed = false;
|
||||
|
||||
if (hovered && g_input.mouse_clicked) {
|
||||
g_input.mouse_clicked = false;
|
||||
g_input.active_id = id;
|
||||
}
|
||||
|
||||
if (g_input.active_id == id && g_input.mouse_down) {
|
||||
int span = h - thumb_h;
|
||||
if (span < 1) span = 1;
|
||||
int rel = g_input.mouse_y - y - thumb_h / 2;
|
||||
if (rel < 0) rel = 0;
|
||||
if (rel > span) rel = span;
|
||||
|
||||
int nv = (rel * max_offset) / span;
|
||||
if (nv != *offset) {
|
||||
*offset = nv;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (*offset < 0) *offset = 0;
|
||||
if (*offset > max_offset) *offset = max_offset;
|
||||
|
||||
int thumb_y = y + ((h - thumb_h) * (*offset)) / (max_offset ? max_offset : 1);
|
||||
gui_gfx_fill_rect(SX(x + 1), SY(thumb_y), w - 2, thumb_h,
|
||||
(g_input.active_id == id) ? GUI_THEME_PRIMARY : GUI_THEME_BG_ELEVATED);
|
||||
|
||||
return changed;
|
||||
}
|
||||
+658
@@ -0,0 +1,658 @@
|
||||
// SUB-WM: window manager and software compositor for SUB-OS
|
||||
//
|
||||
// Windows live in a fixed slot array; z-order is a separate array of ids with
|
||||
// the topmost window last. Interaction is edge-driven: a press picks a target
|
||||
// and latches it into a drag or resize gesture, and the gesture then owns the
|
||||
// pointer until the button comes back up.
|
||||
|
||||
#include <gui/gui_wm.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <drivers/keyboard.h>
|
||||
#include <lib/string.h>
|
||||
#include <mm/kmalloc.h>
|
||||
|
||||
#define BTN_SIZE 14
|
||||
#define BTN_GAP 4
|
||||
#define SNAP_MARGIN 8
|
||||
|
||||
static gui_window_t g_windows[GUI_MAX_WINDOWS];
|
||||
static int g_window_order[GUI_MAX_WINDOWS];
|
||||
static int g_num_windows = 0;
|
||||
static int g_next_win_id = 1;
|
||||
|
||||
static int g_drag_win_id = -1;
|
||||
static int g_resize_win_id = -1;
|
||||
static bool g_prev_btn_left = false;
|
||||
static int g_last_mouse_x = 0;
|
||||
static int g_last_mouse_y = 0;
|
||||
|
||||
static int g_work_w = GUI_DEFAULT_WIDTH;
|
||||
static int g_work_h = GUI_DEFAULT_HEIGHT;
|
||||
|
||||
// Anchor of the in-flight resize gesture, captured on the press edge.
|
||||
static int g_resize_start_w = 0;
|
||||
static int g_resize_start_h = 0;
|
||||
static int g_resize_start_mx = 0;
|
||||
static int g_resize_start_my = 0;
|
||||
|
||||
void gui_wm_init(void) {
|
||||
memset(g_windows, 0, sizeof(g_windows));
|
||||
memset(g_window_order, 0, sizeof(g_window_order));
|
||||
g_num_windows = 0;
|
||||
g_next_win_id = 1;
|
||||
g_drag_win_id = -1;
|
||||
g_resize_win_id = -1;
|
||||
g_prev_btn_left = false;
|
||||
g_work_w = gui_gfx_get_width();
|
||||
g_work_h = gui_gfx_get_height();
|
||||
}
|
||||
|
||||
void gui_wm_set_workarea(int width, int height) {
|
||||
if (width > 0) g_work_w = width;
|
||||
if (height > 0) g_work_h = height;
|
||||
}
|
||||
|
||||
gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h) {
|
||||
if (g_num_windows >= GUI_MAX_WINDOWS) return NULL;
|
||||
|
||||
int slot = -1;
|
||||
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
||||
if (g_windows[i].id == 0) { slot = i; break; }
|
||||
}
|
||||
if (slot == -1) return NULL;
|
||||
|
||||
gui_window_t* win = &g_windows[slot];
|
||||
memset(win, 0, sizeof(*win));
|
||||
|
||||
win->id = g_next_win_id++;
|
||||
strncpy(win->title, title ? title : "Window", sizeof(win->title) - 1);
|
||||
|
||||
win->width = (w > GUI_MIN_WINDOW_W) ? w : GUI_MIN_WINDOW_W;
|
||||
win->height = (h > GUI_MIN_WINDOW_H) ? h : GUI_MIN_WINDOW_H;
|
||||
|
||||
// Keep new windows fully on screen even if the caller asked otherwise.
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x + win->width > g_work_w) x = g_work_w - win->width;
|
||||
if (y + win->height > g_work_h) y = g_work_h - win->height;
|
||||
win->x = (x > 0) ? x : 0;
|
||||
win->y = (y > 0) ? y : 0;
|
||||
|
||||
win->visible = true;
|
||||
win->resizable = true;
|
||||
win->active = true;
|
||||
win->snap = GUI_SNAP_NONE;
|
||||
|
||||
g_window_order[g_num_windows++] = win->id;
|
||||
gui_wm_focus_window(win->id);
|
||||
return win;
|
||||
}
|
||||
|
||||
static int order_index_of(int win_id) {
|
||||
for (int i = 0; i < g_num_windows; i++) {
|
||||
if (g_window_order[i] == win_id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void gui_wm_destroy_window(int win_id) {
|
||||
int idx = order_index_of(win_id);
|
||||
if (idx != -1) {
|
||||
for (int i = idx; i < g_num_windows - 1; i++) {
|
||||
g_window_order[i] = g_window_order[i + 1];
|
||||
}
|
||||
g_num_windows--;
|
||||
}
|
||||
|
||||
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
||||
if (g_windows[i].id != win_id) continue;
|
||||
|
||||
if (g_windows[i].handle_event) {
|
||||
gui_event_t ev = { .type = GUI_EVENT_CLOSE };
|
||||
g_windows[i].handle_event(&g_windows[i], &ev);
|
||||
}
|
||||
memset(&g_windows[i], 0, sizeof(gui_window_t));
|
||||
break;
|
||||
}
|
||||
|
||||
if (g_drag_win_id == win_id) g_drag_win_id = -1;
|
||||
if (g_resize_win_id == win_id) g_resize_win_id = -1;
|
||||
|
||||
if (g_num_windows > 0) gui_wm_focus_window(g_window_order[g_num_windows - 1]);
|
||||
}
|
||||
|
||||
void gui_wm_focus_window(int win_id) {
|
||||
int idx = order_index_of(win_id);
|
||||
if (idx != -1 && idx != g_num_windows - 1) {
|
||||
int target = g_window_order[idx];
|
||||
for (int i = idx; i < g_num_windows - 1; i++) {
|
||||
g_window_order[i] = g_window_order[i + 1];
|
||||
}
|
||||
g_window_order[g_num_windows - 1] = target;
|
||||
}
|
||||
|
||||
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
||||
if (g_windows[i].id == 0) continue;
|
||||
|
||||
bool is_active = (g_windows[i].id == win_id);
|
||||
if (g_windows[i].active == is_active) continue;
|
||||
|
||||
g_windows[i].active = is_active;
|
||||
if (is_active && g_windows[i].handle_event) {
|
||||
gui_event_t ev = { .type = GUI_EVENT_FOCUS };
|
||||
g_windows[i].handle_event(&g_windows[i], &ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gui_window_t* gui_wm_get_window(int win_id) {
|
||||
for (int i = 0; i < GUI_MAX_WINDOWS; i++) {
|
||||
if (g_windows[i].id == win_id) return &g_windows[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
gui_window_t* gui_wm_get_window_at_index(int index) {
|
||||
if (index < 0 || index >= g_num_windows) return NULL;
|
||||
return gui_wm_get_window(g_window_order[index]);
|
||||
}
|
||||
|
||||
gui_window_t* gui_wm_get_active_window(void) {
|
||||
if (g_num_windows == 0) return NULL;
|
||||
return gui_wm_get_window(g_window_order[g_num_windows - 1]);
|
||||
}
|
||||
|
||||
int gui_wm_get_window_count(void) { return g_num_windows; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Window state transitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void save_geometry(gui_window_t* win) {
|
||||
if (win->snap != GUI_SNAP_NONE) return; // Already holding a saved rect
|
||||
win->saved_x = win->x;
|
||||
win->saved_y = win->y;
|
||||
win->saved_width = win->width;
|
||||
win->saved_height = win->height;
|
||||
}
|
||||
|
||||
static void emit_resize(gui_window_t* win) {
|
||||
if (!win->handle_event) return;
|
||||
gui_event_t ev = { .type = GUI_EVENT_RESIZE };
|
||||
win->handle_event(win, &ev);
|
||||
}
|
||||
|
||||
void gui_wm_snap_window(int win_id, gui_snap_t snap) {
|
||||
gui_window_t* win = gui_wm_get_window(win_id);
|
||||
if (!win) return;
|
||||
|
||||
if (snap == GUI_SNAP_NONE) {
|
||||
gui_wm_restore_window(win_id);
|
||||
return;
|
||||
}
|
||||
|
||||
save_geometry(win);
|
||||
win->snap = snap;
|
||||
|
||||
switch (snap) {
|
||||
case GUI_SNAP_LEFT:
|
||||
win->x = 0; win->y = 0;
|
||||
win->width = g_work_w / 2; win->height = g_work_h;
|
||||
break;
|
||||
case GUI_SNAP_RIGHT:
|
||||
win->x = g_work_w / 2; win->y = 0;
|
||||
win->width = g_work_w - g_work_w / 2; win->height = g_work_h;
|
||||
break;
|
||||
default: // GUI_SNAP_MAXIMIZED
|
||||
win->x = 0; win->y = 0;
|
||||
win->width = g_work_w; win->height = g_work_h;
|
||||
break;
|
||||
}
|
||||
|
||||
emit_resize(win);
|
||||
}
|
||||
|
||||
void gui_wm_restore_window(int win_id) {
|
||||
gui_window_t* win = gui_wm_get_window(win_id);
|
||||
if (!win) return;
|
||||
|
||||
win->minimized = false;
|
||||
win->visible = true;
|
||||
|
||||
if (win->snap != GUI_SNAP_NONE) {
|
||||
win->x = win->saved_x;
|
||||
win->y = win->saved_y;
|
||||
win->width = win->saved_width;
|
||||
win->height = win->saved_height;
|
||||
win->snap = GUI_SNAP_NONE;
|
||||
emit_resize(win);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_wm_toggle_maximize(int win_id) {
|
||||
gui_window_t* win = gui_wm_get_window(win_id);
|
||||
if (!win) return;
|
||||
|
||||
if (win->snap == GUI_SNAP_MAXIMIZED) gui_wm_restore_window(win_id);
|
||||
else gui_wm_snap_window(win_id, GUI_SNAP_MAXIMIZED);
|
||||
}
|
||||
|
||||
void gui_wm_minimize_window(int win_id) {
|
||||
gui_window_t* win = gui_wm_get_window(win_id);
|
||||
if (!win) return;
|
||||
|
||||
win->minimized = true;
|
||||
|
||||
// Focus drops to the next window that is still on screen.
|
||||
for (int i = g_num_windows - 1; i >= 0; i--) {
|
||||
gui_window_t* other = gui_wm_get_window(g_window_order[i]);
|
||||
if (other && other->id != win_id && !other->minimized && other->visible) {
|
||||
gui_wm_focus_window(other->id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
win->active = false;
|
||||
}
|
||||
|
||||
void gui_wm_close_active(void) {
|
||||
gui_window_t* active = gui_wm_get_active_window();
|
||||
if (active) gui_wm_destroy_window(active->id);
|
||||
}
|
||||
|
||||
void gui_wm_cycle_focus(void) {
|
||||
if (g_num_windows < 2) return;
|
||||
|
||||
// Walk down from the top and promote the first visible window below it.
|
||||
for (int i = g_num_windows - 2; i >= 0; i--) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (win && win->visible) {
|
||||
if (win->minimized) win->minimized = false;
|
||||
gui_wm_focus_window(win->id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After a bulk layout change nothing may hold focus (every window could have
|
||||
// been minimized). Promote the topmost visible window so the desktop always
|
||||
// has a keyboard target.
|
||||
static void ensure_focus(void) {
|
||||
if (gui_wm_get_active_window() && gui_wm_get_active_window()->active) return;
|
||||
|
||||
for (int i = g_num_windows - 1; i >= 0; i--) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (win && win->visible && !win->minimized) {
|
||||
gui_wm_focus_window(win->id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_wm_cascade_windows(void) {
|
||||
int step = 26;
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < g_num_windows; i++) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (!win || !win->visible) continue;
|
||||
|
||||
win->minimized = false;
|
||||
win->snap = GUI_SNAP_NONE;
|
||||
win->x = 20 + n * step;
|
||||
win->y = 20 + n * step;
|
||||
|
||||
if (win->x + win->width > g_work_w) win->x = g_work_w - win->width;
|
||||
if (win->y + win->height > g_work_h) win->y = g_work_h - win->height;
|
||||
if (win->x < 0) win->x = 0;
|
||||
if (win->y < 0) win->y = 0;
|
||||
|
||||
emit_resize(win);
|
||||
n++;
|
||||
}
|
||||
|
||||
ensure_focus();
|
||||
}
|
||||
|
||||
void gui_wm_tile_windows(void) {
|
||||
int visible = 0;
|
||||
for (int i = 0; i < g_num_windows; i++) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (win && win->visible && !win->minimized) visible++;
|
||||
}
|
||||
if (visible == 0) return;
|
||||
|
||||
// Lay out on the squarest grid that fits every window.
|
||||
int cols = 1;
|
||||
while (cols * cols < visible) cols++;
|
||||
int rows = (visible + cols - 1) / cols;
|
||||
|
||||
int cell_w = g_work_w / cols;
|
||||
int cell_h = g_work_h / rows;
|
||||
int n = 0;
|
||||
|
||||
for (int i = 0; i < g_num_windows; i++) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (!win || !win->visible || win->minimized) continue;
|
||||
|
||||
win->snap = GUI_SNAP_NONE;
|
||||
win->x = (n % cols) * cell_w;
|
||||
win->y = (n / cols) * cell_h;
|
||||
win->width = cell_w - 4;
|
||||
win->height = cell_h - 4;
|
||||
|
||||
if (win->width < GUI_MIN_WINDOW_W) win->width = GUI_MIN_WINDOW_W;
|
||||
if (win->height < GUI_MIN_WINDOW_H) win->height = GUI_MIN_WINDOW_H;
|
||||
|
||||
emit_resize(win);
|
||||
n++;
|
||||
}
|
||||
|
||||
ensure_focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hit testing and pointer gestures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Title bar buttons run right to left: 0 = close, 1 = maximize, 2 = minimize.
|
||||
static void button_rect(const gui_window_t* win, int which, int* bx, int* by) {
|
||||
*by = win->y + (GUI_TITLEBAR_HEIGHT - BTN_SIZE) / 2;
|
||||
*bx = win->x + win->width - (BTN_SIZE + BTN_GAP) * (which + 1) - 2;
|
||||
}
|
||||
|
||||
static bool point_in(int px, int py, int x, int y, int w, int h) {
|
||||
return px >= x && px < x + w && py >= y && py < y + h;
|
||||
}
|
||||
|
||||
static bool in_resize_grip(const gui_window_t* win, int mx, int my) {
|
||||
if (!win->resizable || win->snap != GUI_SNAP_NONE) return false;
|
||||
return point_in(mx, my,
|
||||
win->x + win->width - GUI_RESIZE_GRIP,
|
||||
win->y + win->height - GUI_RESIZE_GRIP,
|
||||
GUI_RESIZE_GRIP, GUI_RESIZE_GRIP);
|
||||
}
|
||||
|
||||
static void deliver(gui_window_t* win, gui_event_type_t type, int mx, int my,
|
||||
bool left, bool right) {
|
||||
if (!win->handle_event) return;
|
||||
|
||||
gui_event_t ev = {
|
||||
.type = type,
|
||||
.mouse_x = mx,
|
||||
.mouse_y = my,
|
||||
.rel_x = mx - win->x,
|
||||
.rel_y = my - (win->y + GUI_TITLEBAR_HEIGHT),
|
||||
.btn_left = left,
|
||||
.btn_right = right
|
||||
};
|
||||
win->handle_event(win, &ev);
|
||||
}
|
||||
|
||||
// A titlebar press: returns true if a button consumed it.
|
||||
static bool handle_titlebar_press(gui_window_t* win, int mx, int my) {
|
||||
int bx, by;
|
||||
|
||||
button_rect(win, 0, &bx, &by);
|
||||
if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) {
|
||||
gui_wm_destroy_window(win->id);
|
||||
return true;
|
||||
}
|
||||
|
||||
button_rect(win, 1, &bx, &by);
|
||||
if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) {
|
||||
gui_wm_toggle_maximize(win->id);
|
||||
return true;
|
||||
}
|
||||
|
||||
button_rect(win, 2, &bx, &by);
|
||||
if (point_in(mx, my, bx, by, BTN_SIZE, BTN_SIZE)) {
|
||||
gui_wm_minimize_window(win->id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Anywhere else on the bar starts a drag. Dragging a snapped window pops
|
||||
// it back to its saved size first, centred under the cursor.
|
||||
if (win->snap != GUI_SNAP_NONE) {
|
||||
int grab_ratio = (win->width > 0) ? ((mx - win->x) * 100) / win->width : 50;
|
||||
gui_wm_restore_window(win->id);
|
||||
win->x = mx - (win->width * grab_ratio) / 100;
|
||||
win->y = my - GUI_TITLEBAR_HEIGHT / 2;
|
||||
if (win->y < 0) win->y = 0;
|
||||
}
|
||||
|
||||
g_drag_win_id = win->id;
|
||||
win->is_dragging = true;
|
||||
win->drag_off_x = mx - win->x;
|
||||
win->drag_off_y = my - win->y;
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_wm_sync_button_state(bool btn_left) {
|
||||
g_prev_btn_left = btn_left;
|
||||
}
|
||||
|
||||
void gui_wm_handle_mouse(int mx, int my, bool btn_left, bool btn_right) {
|
||||
g_last_mouse_x = mx;
|
||||
g_last_mouse_y = my;
|
||||
|
||||
// --- An in-flight resize owns the pointer until the button is released --
|
||||
if (g_resize_win_id != -1) {
|
||||
gui_window_t* win = gui_wm_get_window(g_resize_win_id);
|
||||
if (!btn_left || !win) {
|
||||
if (win) { win->is_resizing = false; emit_resize(win); }
|
||||
g_resize_win_id = -1;
|
||||
} else {
|
||||
int nw = g_resize_start_w + (mx - g_resize_start_mx);
|
||||
int nh = g_resize_start_h + (my - g_resize_start_my);
|
||||
|
||||
if (nw < GUI_MIN_WINDOW_W) nw = GUI_MIN_WINDOW_W;
|
||||
if (nh < GUI_MIN_WINDOW_H) nh = GUI_MIN_WINDOW_H;
|
||||
if (win->x + nw > g_work_w) nw = g_work_w - win->x;
|
||||
if (win->y + nh > g_work_h) nh = g_work_h - win->y;
|
||||
|
||||
win->width = nw;
|
||||
win->height = nh;
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- An in-flight drag likewise owns the pointer ------------------------
|
||||
if (g_drag_win_id != -1) {
|
||||
gui_window_t* win = gui_wm_get_window(g_drag_win_id);
|
||||
if (!btn_left || !win) {
|
||||
if (win) {
|
||||
win->is_dragging = false;
|
||||
|
||||
// Releasing against a screen edge snaps the window there.
|
||||
if (mx <= SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_LEFT);
|
||||
else if (mx >= g_work_w - SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_RIGHT);
|
||||
else if (my <= SNAP_MARGIN) gui_wm_snap_window(win->id, GUI_SNAP_MAXIMIZED);
|
||||
}
|
||||
g_drag_win_id = -1;
|
||||
} else {
|
||||
win->x = mx - win->drag_off_x;
|
||||
win->y = my - win->drag_off_y;
|
||||
|
||||
// Keep at least a strip of the title bar reachable.
|
||||
if (win->y < 0) win->y = 0;
|
||||
if (win->y > g_work_h - GUI_TITLEBAR_HEIGHT) win->y = g_work_h - GUI_TITLEBAR_HEIGHT;
|
||||
if (win->x + win->width < 60) win->x = 60 - win->width;
|
||||
if (win->x > g_work_w - 60) win->x = g_work_w - 60;
|
||||
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Press edge: pick a target from the top of the z-order down ---------
|
||||
if (btn_left && !g_prev_btn_left) {
|
||||
for (int i = g_num_windows - 1; i >= 0; i--) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (!win || !win->visible || win->minimized) continue;
|
||||
if (!point_in(mx, my, win->x, win->y, win->width, win->height)) continue;
|
||||
|
||||
gui_wm_focus_window(win->id);
|
||||
|
||||
if (in_resize_grip(win, mx, my)) {
|
||||
g_resize_win_id = win->id;
|
||||
win->is_resizing = true;
|
||||
g_resize_start_w = win->width;
|
||||
g_resize_start_h = win->height;
|
||||
g_resize_start_mx = mx;
|
||||
g_resize_start_my = my;
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
|
||||
if (my < win->y + GUI_TITLEBAR_HEIGHT) {
|
||||
handle_titlebar_press(win, mx, my);
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
|
||||
deliver(win, GUI_EVENT_MOUSE_DOWN, mx, my, btn_left, btn_right);
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
|
||||
g_prev_btn_left = btn_left;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Release and move edges go to the focused window --------------------
|
||||
gui_window_t* active = gui_wm_get_active_window();
|
||||
if (active && !active->minimized) {
|
||||
if (!btn_left && g_prev_btn_left) {
|
||||
deliver(active, GUI_EVENT_MOUSE_UP, mx, my, btn_left, btn_right);
|
||||
} else {
|
||||
deliver(active, GUI_EVENT_MOUSE_MOVE, mx, my, btn_left, btn_right);
|
||||
}
|
||||
}
|
||||
|
||||
g_prev_btn_left = btn_left;
|
||||
}
|
||||
|
||||
void gui_wm_handle_key(uint16_t key) {
|
||||
// Function keys drive the window manager itself before the app sees them.
|
||||
switch (key) {
|
||||
case KEY_F5: gui_wm_cycle_focus(); return;
|
||||
case KEY_F6: gui_wm_cascade_windows(); return;
|
||||
case KEY_F7: gui_wm_tile_windows(); return;
|
||||
case KEY_F8: {
|
||||
gui_window_t* a = gui_wm_get_active_window();
|
||||
if (a) gui_wm_toggle_maximize(a->id);
|
||||
return;
|
||||
}
|
||||
case KEY_F9: {
|
||||
gui_window_t* a = gui_wm_get_active_window();
|
||||
if (a) gui_wm_minimize_window(a->id);
|
||||
return;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
|
||||
gui_window_t* active = gui_wm_get_active_window();
|
||||
if (active && active->handle_event && !active->minimized) {
|
||||
gui_event_t ev = { .type = GUI_EVENT_KEY_DOWN, .key = key };
|
||||
active->handle_event(active, &ev);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compositing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void draw_titlebar_buttons(const gui_window_t* win) {
|
||||
int bx, by;
|
||||
|
||||
// Close
|
||||
button_rect(win, 0, &bx, &by);
|
||||
bool hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE);
|
||||
gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFFFF6B6B : GUI_THEME_BTN_CLOSE);
|
||||
gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFF991B1B);
|
||||
gui_gfx_draw_line(bx + 4, by + 4, bx + BTN_SIZE - 5, by + BTN_SIZE - 5, GUI_COLOR_WHITE);
|
||||
gui_gfx_draw_line(bx + BTN_SIZE - 5, by + 4, bx + 4, by + BTN_SIZE - 5, GUI_COLOR_WHITE);
|
||||
|
||||
// Maximize / restore
|
||||
button_rect(win, 1, &bx, &by);
|
||||
hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE);
|
||||
gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFF34D399 : GUI_THEME_BTN_MAXIMIZE);
|
||||
gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFF047857);
|
||||
if (win->snap == GUI_SNAP_MAXIMIZED) {
|
||||
// Two offset outlines read as "restore down".
|
||||
gui_gfx_draw_rect(bx + 3, by + 5, 6, 5, GUI_COLOR_WHITE);
|
||||
gui_gfx_draw_rect(bx + 5, by + 3, 6, 5, GUI_COLOR_WHITE);
|
||||
} else {
|
||||
gui_gfx_draw_rect(bx + 4, by + 4, BTN_SIZE - 8, BTN_SIZE - 8, GUI_COLOR_WHITE);
|
||||
}
|
||||
|
||||
// Minimize
|
||||
button_rect(win, 2, &bx, &by);
|
||||
hot = point_in(g_last_mouse_x, g_last_mouse_y, bx, by, BTN_SIZE, BTN_SIZE);
|
||||
gui_gfx_fill_rect(bx, by, BTN_SIZE, BTN_SIZE, hot ? 0xFFFCD34D : GUI_THEME_BTN_MINIMIZE);
|
||||
gui_gfx_draw_rect(bx, by, BTN_SIZE, BTN_SIZE, 0xFFB45309);
|
||||
gui_gfx_fill_rect(bx + 4, by + BTN_SIZE - 6, BTN_SIZE - 8, 2, GUI_COLOR_WHITE);
|
||||
}
|
||||
|
||||
static void draw_resize_grip(const gui_window_t* win) {
|
||||
if (!win->resizable || win->snap != GUI_SNAP_NONE) return;
|
||||
|
||||
int gx = win->x + win->width - GUI_RESIZE_GRIP;
|
||||
int gy = win->y + win->height - GUI_RESIZE_GRIP;
|
||||
uint32_t col = win->is_resizing ? GUI_THEME_PRIMARY : GUI_THEME_BORDER;
|
||||
|
||||
// Three diagonal hatches, the conventional grip affordance.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int off = 3 + i * 3;
|
||||
gui_gfx_draw_line(gx + off, gy + GUI_RESIZE_GRIP - 2,
|
||||
gx + GUI_RESIZE_GRIP - 2, gy + off, col);
|
||||
}
|
||||
}
|
||||
|
||||
void gui_wm_render(void) {
|
||||
// Bottom-to-top painter's algorithm; the topmost window wins overlaps.
|
||||
for (int i = 0; i < g_num_windows; i++) {
|
||||
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
|
||||
if (!win || !win->visible || win->minimized) continue;
|
||||
|
||||
gui_gfx_draw_shadow(win->x, win->y, win->width, win->height, 6);
|
||||
|
||||
uint32_t border = win->active ? GUI_THEME_PRIMARY : GUI_THEME_BORDER;
|
||||
gui_gfx_draw_rect(win->x, win->y, win->width, win->height, border);
|
||||
|
||||
uint32_t top_bg = win->active ? 0xFF1E293B : 0xFF0F172A;
|
||||
uint32_t bot_bg = win->active ? 0xFF0F172A : 0xFF0A0E1A;
|
||||
gui_gfx_draw_gradient_v(win->x + 1, win->y + 1, win->width - 2,
|
||||
GUI_TITLEBAR_HEIGHT - 1, top_bg, bot_bg);
|
||||
gui_gfx_draw_line(win->x, win->y + GUI_TITLEBAR_HEIGHT,
|
||||
win->x + win->width - 1, win->y + GUI_TITLEBAR_HEIGHT, border);
|
||||
|
||||
// Truncate the title so it never runs under the buttons.
|
||||
int room = win->width - (BTN_SIZE + BTN_GAP) * 3 - 20;
|
||||
int max_chars = room / 8;
|
||||
if (max_chars < 1) max_chars = 1;
|
||||
if (max_chars > 62) max_chars = 62;
|
||||
|
||||
char shown[64];
|
||||
strncpy(shown, win->title, (size_t)max_chars);
|
||||
shown[max_chars] = '\0';
|
||||
if ((int)strlen(win->title) > max_chars && max_chars > 3) {
|
||||
shown[max_chars - 1] = '.';
|
||||
shown[max_chars - 2] = '.';
|
||||
}
|
||||
|
||||
uint32_t title_fg = win->active ? GUI_THEME_TEXT_MAIN : GUI_THEME_TEXT_MUTED;
|
||||
gui_gfx_draw_string_16_shadow(win->x + 8, win->y + 4, shown, title_fg, GUI_COLOR_BLACK);
|
||||
|
||||
draw_titlebar_buttons(win);
|
||||
|
||||
gui_gfx_fill_rect(win->x + 1, win->y + GUI_TITLEBAR_HEIGHT + 1,
|
||||
win->width - 2, win->height - GUI_TITLEBAR_HEIGHT - 2,
|
||||
GUI_THEME_BG_SURFACE);
|
||||
|
||||
if (win->paint) win->paint(win);
|
||||
|
||||
draw_resize_grip(win);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef _GUI_H
|
||||
#define _GUI_H
|
||||
|
||||
#include <gui/gui_theme.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_cursor.h>
|
||||
#include <gui/gui_wm.h>
|
||||
#include <gui/gui_desktop.h>
|
||||
#include <gui/gui_apps.h>
|
||||
#include <gui/gui_terminal.h>
|
||||
#include <gui/gui_apps_ext.h>
|
||||
#include <gui/gui_widgets.h>
|
||||
#include <gui/gui_icons.h>
|
||||
#include <gui/gui_dialog.h>
|
||||
|
||||
void gui_init(void);
|
||||
int gui_start_desktop(void);
|
||||
|
||||
#endif // _GUI_H
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef _GUI_APPS_H
|
||||
#define _GUI_APPS_H
|
||||
|
||||
#include <gui/gui_wm.h>
|
||||
#include <gui/gui_terminal.h>
|
||||
|
||||
void gui_app_sysmon_launch(int x, int y, int w, int h);
|
||||
void gui_app_fileman_launch(int x, int y, int w, int h);
|
||||
void gui_app_calc_launch(int x, int y, int w, int h);
|
||||
void gui_app_paint_launch(int x, int y, int w, int h);
|
||||
void gui_app_about_launch(int x, int y, int w, int h);
|
||||
|
||||
#endif // _GUI_APPS_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef _GUI_APPS_EXT_H
|
||||
#define _GUI_APPS_EXT_H
|
||||
|
||||
#include <gui/gui_wm.h>
|
||||
|
||||
// Second-generation desktop applications, built on the SUB-WT widget toolkit
|
||||
// rather than raw framebuffer calls.
|
||||
|
||||
void gui_app_settings_launch(int x, int y, int w, int h);
|
||||
void gui_app_taskmgr_launch(int x, int y, int w, int h);
|
||||
void gui_app_logviewer_launch(int x, int y, int w, int h);
|
||||
void gui_app_ktest_launch(int x, int y, int w, int h);
|
||||
void gui_app_clock_launch(int x, int y, int w, int h);
|
||||
void gui_app_editor_launch(int x, int y, int w, int h);
|
||||
|
||||
#endif // _GUI_APPS_EXT_H
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef _GUI_CURSOR_H
|
||||
#define _GUI_CURSOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
void gui_cursor_init(void);
|
||||
void gui_cursor_set_pos(int x, int y);
|
||||
int gui_cursor_get_x(void);
|
||||
int gui_cursor_get_y(void);
|
||||
void gui_cursor_draw(void);
|
||||
|
||||
#endif // _GUI_CURSOR_H
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef _GUI_DESKTOP_H
|
||||
#define _GUI_DESKTOP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define GUI_TASKBAR_HEIGHT 32
|
||||
#define GUI_DESKTOP_ICON_W 72
|
||||
#define GUI_DESKTOP_ICON_H 64
|
||||
|
||||
void gui_desktop_init(void);
|
||||
|
||||
void gui_desktop_render_background(void);
|
||||
|
||||
// Drop the cached wallpaper so the next background render repaints it. Needed
|
||||
// when the resolution changes or the framebuffer is handed to another owner.
|
||||
void gui_desktop_invalidate_background(void);
|
||||
void gui_desktop_render_icons(void);
|
||||
void gui_desktop_render_taskbar(void);
|
||||
void gui_desktop_render_start_menu(void);
|
||||
void gui_desktop_render_context_menu(void);
|
||||
|
||||
void gui_desktop_toggle_start_menu(void);
|
||||
bool gui_desktop_is_start_menu_open(void);
|
||||
void gui_desktop_open_context_menu(int mx, int my);
|
||||
void gui_desktop_close_menus(void);
|
||||
|
||||
// Returns true if the click was consumed by desktop chrome (taskbar, menus,
|
||||
// icons) rather than falling through to the window manager.
|
||||
bool gui_desktop_handle_click(int mx, int my);
|
||||
|
||||
// Height of the area windows may occupy (screen minus taskbar).
|
||||
int gui_desktop_workarea_height(void);
|
||||
|
||||
void gui_desktop_request_exit(void);
|
||||
int gui_desktop_run(void);
|
||||
|
||||
#endif // _GUI_DESKTOP_H
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef _GUI_DIALOG_H
|
||||
#define _GUI_DIALOG_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <gui/gui_icons.h>
|
||||
|
||||
// Modal dialogs. Only one is on screen at a time; while it is up the desktop
|
||||
// dims behind it and the compositor routes every click to the dialog, so no
|
||||
// window underneath can be interacted with.
|
||||
|
||||
typedef enum {
|
||||
GUI_DIALOG_INFO,
|
||||
GUI_DIALOG_WARNING,
|
||||
GUI_DIALOG_CONFIRM,
|
||||
GUI_DIALOG_ERROR
|
||||
} gui_dialog_kind_t;
|
||||
|
||||
typedef enum {
|
||||
GUI_DIALOG_RESULT_NONE = 0,
|
||||
GUI_DIALOG_RESULT_OK,
|
||||
GUI_DIALOG_RESULT_CANCEL
|
||||
} gui_dialog_result_t;
|
||||
|
||||
typedef void (*gui_dialog_cb_t)(gui_dialog_result_t result, void* ctx);
|
||||
|
||||
void gui_dialog_init(void);
|
||||
|
||||
void gui_dialog_show(gui_dialog_kind_t kind, const char* title, const char* message);
|
||||
void gui_dialog_confirm(const char* title, const char* message,
|
||||
gui_dialog_cb_t on_result, void* ctx);
|
||||
void gui_dialog_dismiss(void);
|
||||
|
||||
bool gui_dialog_is_open(void);
|
||||
|
||||
// Returns true when the dialog consumed the input.
|
||||
bool gui_dialog_handle_mouse(int mx, int my, bool clicked);
|
||||
bool gui_dialog_handle_key(uint16_t key);
|
||||
|
||||
void gui_dialog_render(void);
|
||||
|
||||
#endif // _GUI_DIALOG_H
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef _GUI_GFX_H
|
||||
#define _GUI_GFX_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <gui/gui_theme.h>
|
||||
|
||||
#define GUI_DEFAULT_WIDTH 800
|
||||
#define GUI_DEFAULT_HEIGHT 600
|
||||
|
||||
typedef struct {
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
} gui_rect_t;
|
||||
|
||||
void gui_gfx_init(uint32_t* fb, int width, int height);
|
||||
int gui_gfx_get_width(void);
|
||||
int gui_gfx_get_height(void);
|
||||
uint32_t* gui_gfx_get_backbuffer(void);
|
||||
|
||||
void gui_gfx_clear(uint32_t color);
|
||||
void gui_gfx_draw_pixel(int x, int y, uint32_t color);
|
||||
void gui_gfx_draw_pixel_blend(int x, int y, uint32_t color, uint8_t alpha);
|
||||
void gui_gfx_draw_line(int x0, int y0, int x1, int y1, uint32_t color);
|
||||
void gui_gfx_draw_rect(int x, int y, int w, int h, uint32_t color);
|
||||
void gui_gfx_fill_rect(int x, int y, int w, int h, uint32_t color);
|
||||
void gui_gfx_fill_rect_blend(int x, int y, int w, int h, uint32_t color, uint8_t alpha);
|
||||
void gui_gfx_draw_rounded_rect(int x, int y, int w, int h, int r, uint32_t color, bool filled);
|
||||
void gui_gfx_draw_gradient_v(int x, int y, int w, int h, uint32_t top_col, uint32_t bot_col);
|
||||
void gui_gfx_draw_gradient_h(int x, int y, int w, int h, uint32_t left_col, uint32_t right_col);
|
||||
|
||||
// Sharp Typography & Font Rendering
|
||||
void gui_gfx_draw_char(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg);
|
||||
void gui_gfx_draw_char_16(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg);
|
||||
void gui_gfx_draw_string(int x, int y, const char* str, uint32_t fg);
|
||||
void gui_gfx_draw_string_16(int x, int y, const char* str, uint32_t fg);
|
||||
void gui_gfx_draw_string_bg(int x, int y, const char* str, uint32_t fg, uint32_t bg);
|
||||
void gui_gfx_draw_string_shadow(int x, int y, const char* str, uint32_t fg, uint32_t shadow);
|
||||
void gui_gfx_draw_string_16_shadow(int x, int y, const char* str, uint32_t fg, uint32_t shadow);
|
||||
void gui_gfx_draw_shadow(int x, int y, int w, int h, int blur);
|
||||
void gui_gfx_blit(int dx, int dy, int w, int h, const uint32_t* src_buf);
|
||||
|
||||
// Push the composited frame to the display. Only the pixels that differ from
|
||||
// the previous frame are written, so an idle desktop costs almost no
|
||||
// framebuffer traffic.
|
||||
void gui_gfx_present(void);
|
||||
|
||||
// Force the next present to push the whole screen. Needed whenever something
|
||||
// outside the compositor has drawn over the framebuffer, such as the kernel
|
||||
// console before the desktop takes the display back.
|
||||
void gui_gfx_invalidate(void);
|
||||
|
||||
// Pixels written to the framebuffer by the last present. Diagnostic only.
|
||||
uint64_t gui_gfx_last_present_pixels(void);
|
||||
|
||||
#endif // _GUI_GFX_H
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef _GUI_ICONS_H
|
||||
#define _GUI_ICONS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 16x16 palette-indexed icon set. Each glyph stores one nibble per pixel
|
||||
// indexing a 16-entry ARGB palette, so the whole set costs 128 bytes an icon
|
||||
// and can be recoloured per theme without touching the bitmaps.
|
||||
|
||||
#define GUI_ICON_SIZE 16
|
||||
|
||||
typedef enum {
|
||||
GUI_ICON_TERMINAL = 0,
|
||||
GUI_ICON_MONITOR,
|
||||
GUI_ICON_FOLDER,
|
||||
GUI_ICON_CALC,
|
||||
GUI_ICON_PAINT,
|
||||
GUI_ICON_INFO,
|
||||
GUI_ICON_SETTINGS,
|
||||
GUI_ICON_TASKS,
|
||||
GUI_ICON_LOG,
|
||||
GUI_ICON_FLASK,
|
||||
GUI_ICON_CLOCK,
|
||||
GUI_ICON_FILE,
|
||||
GUI_ICON_POWER,
|
||||
GUI_ICON_WARNING,
|
||||
GUI_ICON_COUNT
|
||||
} gui_icon_id_t;
|
||||
|
||||
void gui_icons_init(void);
|
||||
|
||||
// Draw at native 16x16, or scaled by an integer factor (2 = 32x32).
|
||||
void gui_icon_draw(gui_icon_id_t id, int x, int y);
|
||||
void gui_icon_draw_scaled(gui_icon_id_t id, int x, int y, int scale);
|
||||
void gui_icon_draw_tinted(gui_icon_id_t id, int x, int y, int scale, uint32_t tint);
|
||||
|
||||
const char* gui_icon_name(gui_icon_id_t id);
|
||||
|
||||
#endif // _GUI_ICONS_H
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef _GUI_TERMINAL_H
|
||||
#define _GUI_TERMINAL_H
|
||||
|
||||
#include <gui/gui_wm.h>
|
||||
|
||||
// Desktop terminal emulator. Commands are dispatched to the real LazyBox
|
||||
// applet table, and the applet's printk output is captured and rendered in
|
||||
// the window, so the GUI terminal runs the same tools the kernel TTY does.
|
||||
|
||||
void gui_app_terminal_launch(int x, int y, int w, int h);
|
||||
|
||||
#endif // _GUI_TERMINAL_H
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef _GUI_THEME_H
|
||||
#define _GUI_THEME_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// 32-bit ARGB Color definitions (0xAARRGGBB)
|
||||
#define GUI_COLOR_TRANSPARENT 0x00000000
|
||||
#define GUI_COLOR_BLACK 0xFF000000
|
||||
#define GUI_COLOR_WHITE 0xFFFFFFFF
|
||||
|
||||
// Cyberpunk / Nord Dark Theme
|
||||
#define GUI_THEME_BG_DARK 0xFF0F172A // Slate 900
|
||||
#define GUI_THEME_BG_SURFACE 0xFF1E293B // Slate 800
|
||||
#define GUI_THEME_BG_ELEVATED 0xFF334155 // Slate 700
|
||||
#define GUI_THEME_BORDER 0xFF475569 // Slate 600
|
||||
|
||||
#define GUI_THEME_PRIMARY 0xFF38BDF8 // Sky 400
|
||||
#define GUI_THEME_PRIMARY_DARK 0xFF0284C7 // Sky 600
|
||||
#define GUI_THEME_ACCENT 0xFF818CF8 // Indigo 400
|
||||
#define GUI_THEME_SUCCESS 0xFF34D399 // Emerald 400
|
||||
#define GUI_THEME_WARNING 0xFFFBBF24 // Amber 400
|
||||
#define GUI_THEME_DANGER 0xFFF87171 // Red 400
|
||||
|
||||
#define GUI_THEME_TEXT_MAIN 0xFFF8FAFC // Slate 50
|
||||
#define GUI_THEME_TEXT_MUTED 0xFF94A3B8 // Slate 400
|
||||
#define GUI_THEME_TEXT_DIM 0xFF64748B // Slate 500
|
||||
|
||||
#define GUI_THEME_TITLEBAR_ACT 0xFF1E293B
|
||||
#define GUI_THEME_TITLEBAR_INACT 0xFF0F172A
|
||||
#define GUI_THEME_TASKBAR_BG 0xFF0B0F19
|
||||
|
||||
#define GUI_THEME_BTN_CLOSE 0xFFEF4444
|
||||
#define GUI_THEME_BTN_MINIMIZE 0xFFF59E0B
|
||||
#define GUI_THEME_BTN_MAXIMIZE 0xFF10B981
|
||||
|
||||
static inline uint32_t gui_color_alpha_blend(uint32_t bg, uint32_t fg, uint8_t alpha) {
|
||||
if (alpha == 0) return bg;
|
||||
if (alpha == 255) return fg;
|
||||
|
||||
uint32_t rb_bg = bg & 0x00FF00FF;
|
||||
uint32_t g_bg = bg & 0x0000FF00;
|
||||
uint32_t rb_fg = fg & 0x00FF00FF;
|
||||
uint32_t g_fg = fg & 0x0000FF00;
|
||||
|
||||
uint32_t rb = rb_bg + (((rb_fg - rb_bg) * alpha) >> 8);
|
||||
uint32_t g = g_bg + (((g_fg - g_bg) * alpha) >> 8);
|
||||
|
||||
return 0xFF000000 | (rb & 0x00FF00FF) | (g & 0x0000FF00);
|
||||
}
|
||||
|
||||
#endif // _GUI_THEME_H
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef _GUI_WIDGETS_H
|
||||
#define _GUI_WIDGETS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <gui/gui_wm.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
|
||||
// SUB-WT: immediate-mode widget toolkit for SUB-OS desktop applications.
|
||||
//
|
||||
// An app feeds the toolkit whatever event the window manager delivered
|
||||
// (gui_widget_feed_event) and then simply calls widget functions from inside
|
||||
// its paint routine. Each call both draws the control and reports what the
|
||||
// user did with it, so no retained widget tree has to be kept in sync with
|
||||
// the window's state.
|
||||
//
|
||||
// static void my_paint(gui_window_t* win) {
|
||||
// gui_widget_begin(win);
|
||||
// if (gui_button(1, 10, 10, 90, 24, "Refresh")) do_refresh();
|
||||
// gui_checkbox(2, 10, 44, "Verbose", &verbose);
|
||||
// gui_widget_end();
|
||||
// }
|
||||
|
||||
#define GUI_FONT_W 8
|
||||
#define GUI_FONT_H 8
|
||||
#define GUI_FONT16_H 16
|
||||
#define GUI_WIDGET_ROW_H 22
|
||||
|
||||
typedef enum {
|
||||
GUI_ALIGN_LEFT,
|
||||
GUI_ALIGN_CENTER,
|
||||
GUI_ALIGN_RIGHT
|
||||
} gui_align_t;
|
||||
|
||||
// Per-frame input snapshot the widgets consult.
|
||||
typedef struct {
|
||||
int mouse_x; // Window-relative, client area origin
|
||||
int mouse_y;
|
||||
bool mouse_down; // Button currently held
|
||||
bool mouse_clicked; // Press edge, consumed by the first widget hit
|
||||
bool mouse_released;
|
||||
uint16_t key; // Pending keystroke, consumed by the focused field
|
||||
int hot_id; // Widget under the cursor this frame
|
||||
int active_id; // Widget holding the press
|
||||
int focus_id; // Widget receiving keystrokes
|
||||
} gui_widget_input_t;
|
||||
|
||||
// Called from an app's handle_event to record what the WM delivered.
|
||||
void gui_widget_feed_event(gui_window_t* win, const gui_event_t* ev);
|
||||
|
||||
// Frame scope. Everything between these two is drawn in client-area
|
||||
// coordinates: (0,0) is the top-left pixel below the title bar.
|
||||
void gui_widget_begin(gui_window_t* win);
|
||||
void gui_widget_end(void);
|
||||
|
||||
const gui_widget_input_t* gui_widget_get_input(void);
|
||||
void gui_widget_set_focus(int id);
|
||||
int gui_widget_client_width(void);
|
||||
int gui_widget_client_height(void);
|
||||
|
||||
// --- Static content ---------------------------------------------------------
|
||||
void gui_label(int x, int y, const char* text, uint32_t color);
|
||||
void gui_label_bold(int x, int y, const char* text, uint32_t color);
|
||||
void gui_label_aligned(int x, int y, int w, const char* text, uint32_t color, gui_align_t align);
|
||||
void gui_panel(int x, int y, int w, int h, const char* title);
|
||||
void gui_separator(int x, int y, int w);
|
||||
void gui_progress_bar(int x, int y, int w, int h, int percent, uint32_t fill);
|
||||
void gui_sparkline(int x, int y, int w, int h, const uint8_t* samples, int count, uint32_t color);
|
||||
void gui_badge(int x, int y, const char* text, uint32_t bg, uint32_t fg);
|
||||
|
||||
// --- Interactive controls ---------------------------------------------------
|
||||
// Every control takes a caller-assigned id that must be unique and stable
|
||||
// within one window.
|
||||
bool gui_button(int id, int x, int y, int w, int h, const char* label);
|
||||
bool gui_button_colored(int id, int x, int y, int w, int h, const char* label, uint32_t accent);
|
||||
bool gui_checkbox(int id, int x, int y, const char* label, bool* value);
|
||||
bool gui_radio(int id, int x, int y, const char* label, int* selected, int this_value);
|
||||
bool gui_slider(int id, int x, int y, int w, int* value, int min_val, int max_val);
|
||||
bool gui_listbox(int id, int x, int y, int w, int h, const char* const* items, int count, int* selected);
|
||||
bool gui_textfield(int id, int x, int y, int w, char* buffer, size_t capacity);
|
||||
int gui_tabbar(int id, int x, int y, int w, const char* const* labels, int count, int* active);
|
||||
bool gui_scrollbar(int id, int x, int y, int h, int* offset, int total, int visible);
|
||||
|
||||
#endif // _GUI_WIDGETS_H
|
||||
@@ -0,0 +1,111 @@
|
||||
#ifndef _GUI_WM_H
|
||||
#define _GUI_WM_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <gui/gui_gfx.h>
|
||||
#include <gui/gui_theme.h>
|
||||
|
||||
#define GUI_MAX_WINDOWS 16
|
||||
#define GUI_TITLEBAR_HEIGHT 24
|
||||
#define GUI_RESIZE_GRIP 12
|
||||
#define GUI_MIN_WINDOW_W 160
|
||||
#define GUI_MIN_WINDOW_H 100
|
||||
|
||||
typedef enum {
|
||||
GUI_EVENT_MOUSE_MOVE,
|
||||
GUI_EVENT_MOUSE_DOWN,
|
||||
GUI_EVENT_MOUSE_UP,
|
||||
GUI_EVENT_KEY_DOWN,
|
||||
GUI_EVENT_FOCUS,
|
||||
GUI_EVENT_RESIZE,
|
||||
GUI_EVENT_CLOSE
|
||||
} gui_event_type_t;
|
||||
|
||||
typedef struct {
|
||||
gui_event_type_t type;
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
int rel_x;
|
||||
int rel_y;
|
||||
bool btn_left;
|
||||
bool btn_right;
|
||||
uint16_t key;
|
||||
} gui_event_t;
|
||||
|
||||
// Where a window sits relative to the screen edges.
|
||||
typedef enum {
|
||||
GUI_SNAP_NONE = 0,
|
||||
GUI_SNAP_LEFT,
|
||||
GUI_SNAP_RIGHT,
|
||||
GUI_SNAP_MAXIMIZED
|
||||
} gui_snap_t;
|
||||
|
||||
typedef struct gui_window {
|
||||
int id;
|
||||
char title[64];
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
bool active;
|
||||
bool minimized;
|
||||
bool visible;
|
||||
bool resizable;
|
||||
|
||||
// Drag / resize interaction state
|
||||
bool is_dragging;
|
||||
bool is_resizing;
|
||||
int drag_off_x;
|
||||
int drag_off_y;
|
||||
|
||||
// Geometry remembered across maximize / snap so restore is exact
|
||||
gui_snap_t snap;
|
||||
int saved_x;
|
||||
int saved_y;
|
||||
int saved_width;
|
||||
int saved_height;
|
||||
|
||||
void* user_data;
|
||||
|
||||
void (*paint)(struct gui_window* win);
|
||||
void (*handle_event)(struct gui_window* win, const gui_event_t* ev);
|
||||
} gui_window_t;
|
||||
|
||||
void gui_wm_init(void);
|
||||
|
||||
// The work area excludes the taskbar; windows maximize and snap inside it.
|
||||
void gui_wm_set_workarea(int width, int height);
|
||||
|
||||
gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h);
|
||||
void gui_wm_destroy_window(int win_id);
|
||||
void gui_wm_focus_window(int win_id);
|
||||
gui_window_t* gui_wm_get_window(int win_id);
|
||||
gui_window_t* gui_wm_get_active_window(void);
|
||||
int gui_wm_get_window_count(void);
|
||||
|
||||
// Enumerate in z-order, bottom-most first. Returns NULL past the end.
|
||||
gui_window_t* gui_wm_get_window_at_index(int index);
|
||||
|
||||
void gui_wm_minimize_window(int win_id);
|
||||
void gui_wm_restore_window(int win_id);
|
||||
void gui_wm_toggle_maximize(int win_id);
|
||||
void gui_wm_snap_window(int win_id, gui_snap_t snap);
|
||||
void gui_wm_close_active(void);
|
||||
void gui_wm_cycle_focus(void);
|
||||
void gui_wm_cascade_windows(void);
|
||||
void gui_wm_tile_windows(void);
|
||||
|
||||
// Keep the WM's press-edge detector in step when another layer (a menu, a
|
||||
// modal dialog) consumed the click instead. Without this the WM still holds
|
||||
// the pre-press state and reads the still-held button as a fresh press on the
|
||||
// following frame, stealing focus from whatever the click just opened.
|
||||
void gui_wm_sync_button_state(bool btn_left);
|
||||
|
||||
void gui_wm_handle_mouse(int x, int y, bool btn_left, bool btn_right);
|
||||
void gui_wm_handle_key(uint16_t key);
|
||||
void gui_wm_render(void);
|
||||
|
||||
#endif // _GUI_WM_H
|
||||
+27
-10
@@ -71,6 +71,8 @@
|
||||
#include <drivers/virtio_gpu.h>
|
||||
#include <drivers/cpufreq.h>
|
||||
#include <drivers/virtio_input.h>
|
||||
#include <gui/gui.h>
|
||||
#include <init/init.h>
|
||||
#include <kernel/rust.h>
|
||||
#include <kernel/sub_lang.h>
|
||||
#include <kernel/cpp_kernel.h>
|
||||
@@ -131,16 +133,17 @@ static void subos_modular_core_boot(void) {
|
||||
tsc_init();
|
||||
printk(ANSI_BRIGHT_GREEN "OK\n" ANSI_RESET);
|
||||
|
||||
// The framebuffer exists from here on. This kernel boots to a text console,
|
||||
// so the graphical splash is opt-in via `splash` on the command line --
|
||||
// raising it by default would hide the very console we are about to use.
|
||||
// The ASCII logo printed at the top of boot covers the default case.
|
||||
if (init_has_param("splash") && !init_has_param("nosplash")) {
|
||||
// The framebuffer exists from here on, so raise the graphical splash unless
|
||||
// a text console was requested. When it was, the adapter is still in graphics
|
||||
// mode and the VGA text buffer is no longer scanned out, so move the console
|
||||
// to the framebuffer or the rest of the boot log is visible only on the
|
||||
// serial line. Remaining messages keep flowing to the serial line and dmesg.
|
||||
bool want_splash = !(init_has_param("nogui") || init_has_param("text") ||
|
||||
init_has_param("emergency") || init_has_param("single") ||
|
||||
init_has_param("nosplash"));
|
||||
if (want_splash) {
|
||||
boot_logo_splash_begin();
|
||||
} else {
|
||||
// The adapter is in graphics mode from here on, so the VGA text buffer
|
||||
// is no longer scanned out. Move the console to the framebuffer or the
|
||||
// rest of the boot log is visible only on the serial line.
|
||||
fbcon_enable(true);
|
||||
}
|
||||
|
||||
@@ -268,15 +271,29 @@ static void subos_modular_core_boot(void) {
|
||||
weave_preempt_selftest();
|
||||
#endif
|
||||
|
||||
// Return the screen to the console before handing over to the shell.
|
||||
// Boot directly into the GUI Desktop unless emergency text mode is requested.
|
||||
bool emergency_text = init_has_param("nogui") || init_has_param("text") ||
|
||||
init_has_param("emergency") || init_has_param("single");
|
||||
|
||||
if (!emergency_text) {
|
||||
printk(KERN_INFO "INIT: Booting directly into SUB-OS Graphical Desktop Environment...\n");
|
||||
boot_logo_splash_end();
|
||||
gui_start_desktop();
|
||||
printk(KERN_INFO "INIT: GUI session closed. Entering Emergency Kernel TTY Mode.\n");
|
||||
} else {
|
||||
printk(KERN_INFO "INIT: Emergency text/kernel TTY mode requested via boot parameters.\n");
|
||||
boot_logo_splash_end();
|
||||
}
|
||||
|
||||
// The screen returns to the console before handing over to userland, whether
|
||||
// the GUI session just closed or a text boot was requested.
|
||||
fbcon_enable(true);
|
||||
|
||||
// Hand control to userland: /sbin/init is a separate static ELF that runs
|
||||
// in ring 3 with its own page tables and talks back only through INT 0x80.
|
||||
exec_run_builtin_init();
|
||||
|
||||
// Launch Shell
|
||||
// Launch Shell (Emergency TTY Mode)
|
||||
shell_run();
|
||||
|
||||
while (1) {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include <drivers/virtio_gpu.h>
|
||||
#include <drivers/e1000e.h>
|
||||
#include <drivers/virtio_input.h>
|
||||
#include <gui/gui.h>
|
||||
#include <arch/arch.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <lib/string.h>
|
||||
@@ -1033,6 +1034,11 @@ static int applet_e1000e(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int applet_startx(int argc, char** argv) {
|
||||
(void)argc; (void)argv;
|
||||
return gui_start_desktop();
|
||||
}
|
||||
|
||||
static int applet_readelf(int argc, char** argv) {
|
||||
if (argc >= 2) {
|
||||
int fd = vfs_open(argv[1], 0);
|
||||
@@ -2242,6 +2248,9 @@ static const lazybox_applet_t applets[] = {
|
||||
{"cpufreq", applet_cpufreq, "cpufreq [-g governor]", "CPU frequency & P-State gov", "System"},
|
||||
{"gpuinfo", applet_gpuinfo, "gpuinfo", "VirtIO-GPU hardware status", "Virtualization"},
|
||||
{"e1000e", applet_e1000e, "e1000e", "Intel e1000e PCIe NIC stats", "Network"},
|
||||
{"startx", applet_startx, "startx", "Start SUB-OS Desktop GUI", "Desktop"},
|
||||
{"gui", applet_startx, "gui", "Start SUB-OS Desktop GUI", "Desktop"},
|
||||
{"desktop", applet_startx, "desktop", "Start SUB-OS Desktop GUI", "Desktop"},
|
||||
|
||||
// Kernel Core Diagnostics
|
||||
{"ktest", applet_ktest, "ktest [suite|-l]", "Run in-kernel self-test suites", "Kernel"},
|
||||
|
||||
Reference in new issue
Block a user