feat: rudimentary userspace desktop environment

This commit is contained in:
2026-02-20 00:48:51 +01:00
parent 3753ebd4f4
commit 6eebc64863
27 changed files with 5027 additions and 9 deletions
+354
View File
@@ -0,0 +1,354 @@
/*
* widgets.hpp
* ZenithOS GUI widget toolkit (Label, Button, TextBox, Scrollbar)
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
#include "gui/font.hpp"
#include "gui/draw.hpp"
namespace gui {
// ---- Mouse event ----
struct MouseEvent {
int x, y;
uint8_t buttons;
uint8_t prev_buttons;
int32_t scroll;
bool left_held() const { return buttons & 0x01; }
bool right_held() const { return buttons & 0x02; }
bool middle_held() const { return buttons & 0x04; }
bool left_pressed() const { return (buttons & 0x01) && !(prev_buttons & 0x01); }
bool left_released() const { return !(buttons & 0x01) && (prev_buttons & 0x01); }
bool right_pressed() const { return (buttons & 0x02) && !(prev_buttons & 0x02); }
bool right_released() const { return !(buttons & 0x02) && (prev_buttons & 0x02); }
};
// ---- Callback types ----
using ClickCallback = void (*)(void* userdata);
// ---- Label ----
struct Label {
int x, y;
const char* text;
Color color;
void draw(Framebuffer& fb) const {
if (text) {
draw_text(fb, x, y, text, color);
}
}
};
// ---- Button ----
struct Button {
Rect bounds;
const char* text;
Color bg;
Color fg;
Color hover_bg;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h, const char* label) {
bounds = {x, y, w, h};
text = label;
bg = colors::ACCENT;
fg = colors::WHITE;
hover_bg = Color::from_rgb(0x2B, 0x6B, 0xE0);
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
Color bg_color = hovered ? hover_bg : bg;
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 4, bg_color);
// Center text
int tw = text_width(text);
int tx = bounds.x + (bounds.w - tw) / 2;
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, text, fg);
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- IconButton (for panel/menu items with SVG icon + optional text) ----
struct IconButton {
Rect bounds;
const char* text; // optional label (can be nullptr)
uint32_t* icon_pixels; // icon pixel data (ARGB)
int icon_w, icon_h;
Color bg;
Color hover_bg;
Color text_color;
bool hovered;
bool pressed;
ClickCallback on_click;
void* userdata;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text = nullptr;
icon_pixels = nullptr;
icon_w = 0;
icon_h = 0;
bg = {0, 0, 0, 0}; // transparent by default
hover_bg = colors::MENU_HOVER;
text_color = colors::TEXT_COLOR;
hovered = false;
pressed = false;
on_click = nullptr;
userdata = nullptr;
}
void draw(Framebuffer& fb) const {
if (hovered) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, hover_bg);
} else if (bg.a > 0) {
fill_rounded_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h, 3, bg);
}
int content_x = bounds.x + 6;
// Draw icon
if (icon_pixels && icon_w > 0 && icon_h > 0) {
int iy = bounds.y + (bounds.h - icon_h) / 2;
fb.blit_alpha(content_x, iy, icon_w, icon_h, icon_pixels);
content_x += icon_w + 6;
}
// Draw text
if (text) {
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, content_x, ty, text, text_color);
}
}
bool handle_mouse(const MouseEvent& ev) {
hovered = bounds.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
pressed = true;
}
if (pressed && ev.left_released()) {
pressed = false;
if (hovered && on_click) {
on_click(userdata);
return true;
}
}
if (!ev.left_held()) {
pressed = false;
}
return false;
}
};
// ---- TextBox ----
struct TextBox {
Rect bounds;
char text[256];
int cursor;
int text_len;
bool focused;
Color bg;
Color fg;
Color border_color;
Color cursor_color;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
text[0] = '\0';
cursor = 0;
text_len = 0;
focused = false;
bg = colors::WHITE;
fg = colors::TEXT_COLOR;
border_color = colors::BORDER;
cursor_color = colors::ACCENT;
}
void draw(Framebuffer& fb) const {
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
draw_rect(fb, bounds.x, bounds.y, bounds.w, bounds.h,
focused ? cursor_color : border_color);
// Draw text with 4px padding
int tx = bounds.x + 4;
int ty = bounds.y + (bounds.h - FONT_HEIGHT) / 2;
draw_text(fb, tx, ty, text, fg);
// Draw cursor if focused
if (focused) {
int cx = tx + cursor * FONT_WIDTH;
draw_vline(fb, cx, ty, FONT_HEIGHT, cursor_color);
}
}
void handle_mouse(const MouseEvent& ev) {
if (ev.left_pressed()) {
focused = bounds.contains(ev.x, ev.y);
}
}
void handle_key(const Zenith::KeyEvent& key) {
if (!focused || !key.pressed) return;
if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace
if (cursor > 0) {
for (int i = cursor - 1; i < text_len - 1; i++) {
text[i] = text[i + 1];
}
text_len--;
cursor--;
text[text_len] = '\0';
}
} else if (key.ascii >= 32 && key.ascii < 127) {
// Printable character
if (text_len < 254) {
for (int i = text_len; i > cursor; i--) {
text[i] = text[i - 1];
}
text[cursor] = key.ascii;
cursor++;
text_len++;
text[text_len] = '\0';
}
} else if (key.scancode == 0x4B) {
// Left arrow
if (cursor > 0) cursor--;
} else if (key.scancode == 0x4D) {
// Right arrow
if (cursor < text_len) cursor++;
}
}
};
// ---- Scrollbar ----
struct Scrollbar {
Rect bounds;
int content_height;
int view_height;
int scroll_offset;
bool dragging;
int drag_start_y;
int drag_start_offset;
Color bg;
Color fg;
Color hover_fg;
bool hovered;
void init(int x, int y, int w, int h) {
bounds = {x, y, w, h};
content_height = 0;
view_height = h;
scroll_offset = 0;
dragging = false;
drag_start_y = 0;
drag_start_offset = 0;
bg = colors::SCROLLBAR_BG;
fg = colors::SCROLLBAR_FG;
hover_fg = Color::from_rgb(0xA0, 0xA0, 0xA0);
hovered = false;
}
int thumb_height() const {
if (content_height <= view_height) return bounds.h;
int th = (view_height * bounds.h) / content_height;
return th < 20 ? 20 : th;
}
int thumb_y() const {
if (content_height <= view_height) return bounds.y;
int range = bounds.h - thumb_height();
int max_scroll = content_height - view_height;
if (max_scroll <= 0) return bounds.y;
return bounds.y + (scroll_offset * range) / max_scroll;
}
int max_scroll() const {
int ms = content_height - view_height;
return ms > 0 ? ms : 0;
}
void draw(Framebuffer& fb) const {
if (content_height <= view_height) return; // no scrollbar needed
fb.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h, bg);
int th = thumb_height();
int ty = thumb_y();
Color thumb_color = (hovered || dragging) ? hover_fg : fg;
fill_rounded_rect(fb, bounds.x + 1, ty, bounds.w - 2, th, 3, thumb_color);
}
void handle_mouse(const MouseEvent& ev) {
if (content_height <= view_height) return;
Rect thumb_rect = {bounds.x, thumb_y(), bounds.w, thumb_height()};
hovered = thumb_rect.contains(ev.x, ev.y);
if (hovered && ev.left_pressed()) {
dragging = true;
drag_start_y = ev.y;
drag_start_offset = scroll_offset;
}
if (dragging && ev.left_held()) {
int dy = ev.y - drag_start_y;
int range = bounds.h - thumb_height();
if (range > 0) {
int ms = max_scroll();
scroll_offset = drag_start_offset + (dy * ms) / range;
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
if (!ev.left_held()) {
dragging = false;
}
// Handle scroll wheel
if (bounds.contains(ev.x, ev.y) && ev.scroll != 0) {
scroll_offset += ev.scroll * 20;
int ms = max_scroll();
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > ms) scroll_offset = ms;
}
}
};
} // namespace gui