360 lines
9.9 KiB
C++
360 lines
9.9 KiB
C++
/*
|
|
* 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 - system_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 - system_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 fh = system_font_height();
|
|
int ty = bounds.y + (bounds.h - fh) / 2;
|
|
draw_text(fb, tx, ty, text, fg);
|
|
|
|
// Draw cursor if focused
|
|
if (focused) {
|
|
// Measure text up to cursor position for proportional fonts
|
|
char prefix[256];
|
|
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
|
|
prefix[cursor < 255 ? cursor : 255] = '\0';
|
|
int cx = tx + text_width(prefix);
|
|
draw_vline(fb, cx, ty, fh, 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
|