/* * widgets.hpp * MontaukOS 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 montauk::abi::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