Files
MontaukOS/programs/src/desktop/window.cpp
T

281 lines
9.8 KiB
C++

/*
* window.cpp
* Window management: create, close, raise, draw
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y, int w, int h) {
if (ds->window_count >= MAX_WINDOWS) return -1;
int idx = ds->window_count;
Window* win = &ds->windows[idx];
montauk::memset(win, 0, sizeof(Window));
montauk::strncpy(win->title, title, MAX_TITLE_LEN);
win->frame = {x, y, w, h};
win->state = WIN_NORMAL;
win->z_order = idx;
win->focused = true;
win->dirty = true;
win->dragging = false;
win->resizing = false;
win->saved_frame = win->frame;
// Allocate content buffer
Rect cr = win->content_rect();
win->content_w = cr.w;
win->content_h = cr.h;
int buf_size = cr.w * cr.h * 4;
win->content = (uint32_t*)montauk::alloc(buf_size);
montauk::memset(win->content, 0xFF, buf_size);
win->on_draw = nullptr;
win->on_mouse = nullptr;
win->on_key = nullptr;
win->on_close = nullptr;
win->on_poll = nullptr;
win->app_data = nullptr;
win->external = false;
win->ext_win_id = -1;
win->ext_cursor = 0;
win->ext_flags = 0;
// Unfocus previous window
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = false;
}
ds->focused_window = idx;
ds->window_count++;
return idx;
}
void gui::desktop_close_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
Window* win = &ds->windows[idx];
// For external windows, send a close event instead of freeing the buffer.
// Track the ID so the poll loop won't re-create it at the default position
// before the owning process has finished destroying it.
if (win->external) {
if (ds->closing_ext_count < DesktopState::MAX_CLOSING) {
ds->closing_ext_ids[ds->closing_ext_count++] = win->ext_win_id;
}
montauk::abi::WinEvent ev;
montauk::memset(&ev, 0, sizeof(ev));
ev.type = 3; // close
montauk::win_sendevent(win->ext_win_id, &ev);
}
if (win->on_close) win->on_close(win);
if (win->external) {
// Drop our mapping of the shared pixel buffer so the page table doesn't
// keep leaking mappings each time the user closes an external window.
if (win->content != nullptr) {
montauk::win_unmap(win->ext_win_id);
win->content = nullptr;
}
} else if (win->content) {
montauk::free(win->content);
win->content = nullptr;
}
// Shift remaining windows down
for (int i = idx; i < ds->window_count - 1; i++) {
ds->windows[i] = ds->windows[i + 1];
}
ds->window_count--;
// Fix focused window index
if (ds->focused_window == idx) {
ds->focused_window = ds->window_count > 0 ? ds->window_count - 1 : -1;
} else if (ds->focused_window > idx) {
ds->focused_window--;
}
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = true;
}
}
void gui::desktop_raise_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
if (idx == ds->window_count - 1) {
// Already on top, just focus
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count)
ds->windows[ds->focused_window].focused = false;
ds->focused_window = idx;
ds->windows[idx].focused = true;
return;
}
// Unfocus current
if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) {
ds->windows[ds->focused_window].focused = false;
}
// Move window to end (top of stack)
Window tmp = ds->windows[idx];
for (int i = idx; i < ds->window_count - 1; i++) {
ds->windows[i] = ds->windows[i + 1];
}
ds->windows[ds->window_count - 1] = tmp;
ds->focused_window = ds->window_count - 1;
ds->windows[ds->focused_window].focused = true;
}
void gui::desktop_draw_window(DesktopState* ds, int idx) {
if (idx < 0 || idx >= ds->window_count) return;
Window* win = &ds->windows[idx];
if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) return;
Framebuffer& fb = ds->fb;
int x = win->frame.x;
int y = win->frame.y;
int w = win->frame.w;
int h = win->frame.h;
bool fullscreen = desktop_window_fullscreen(win);
if (!fullscreen) {
// Draw shadow
if (ds->settings.show_shadows)
draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW);
// Draw window body
fb.fill_rect(x, y, w, h, colors::WINDOW_BG);
// Draw titlebar
Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8);
fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg);
// Draw border
draw_rect(fb, x, y, w, h, colors::BORDER);
// Titlebar bottom separator
draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER);
// Draw window buttons (macOS style: close, minimize, maximize)
Rect close_r = win->close_btn_rect();
Rect min_r = win->min_btn_rect();
Rect max_r = win->max_btn_rect();
fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN);
fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN);
fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN);
// Draw title text centered in titlebar (after buttons)
int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons
int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2;
int title_w = text_width(win->title);
// Center in remaining space
int remaining_w = w - (title_x - x) - 12;
if (remaining_w > title_w) {
title_x += (remaining_w - title_w) / 2;
}
draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR);
// Call app draw callback to render content (skip during resize — buffer is old size)
if (win->on_draw && !win->resizing) {
win->on_draw(win, fb);
}
}
// Blit content buffer to framebuffer (clip to actual buffer size during resize)
Rect cr = desktop_content_rect(win);
if (win->content) {
if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) {
// Nearest-neighbor scale for external windows (fixed-size shared buffer).
// Pre-clip the destination range so the inner loop avoids per-pixel
// framebuffer bounds checks. Avoid stack-allocated lookup tables —
// the desktop user stack is only 32 KiB and shared with TrueType.
int src_w = win->content_w;
int src_h = win->content_h;
int dst_w = cr.w;
int dst_h = cr.h;
if (src_w <= 0 || src_h <= 0 || dst_w <= 0 || dst_h <= 0) {
// nothing to do
} else {
int fbw = fb.width();
int fbh = fb.height();
uint32_t* buf = fb.buffer();
int pitch = fb.pitch();
int x_start = 0;
int x_end = dst_w;
if (cr.x < 0) x_start = -cr.x;
if (cr.x + dst_w > fbw) x_end = fbw - cr.x;
if (x_start < 0) x_start = 0;
if (x_end > dst_w) x_end = dst_w;
int y_start = 0;
int y_end = dst_h;
if (cr.y < 0) y_start = -cr.y;
if (cr.y + dst_h > fbh) y_end = fbh - cr.y;
if (y_start < 0) y_start = 0;
if (y_end > dst_h) y_end = dst_h;
for (int y = y_start; y < y_end; y++) {
int dy = cr.y + y;
int sy = y * src_h / dst_h;
if (sy >= src_h) sy = src_h - 1;
uint32_t* dst_row = (uint32_t*)((uint8_t*)buf + dy * pitch);
uint32_t* src_row = win->content + sy * src_w;
for (int x = x_start; x < x_end; x++) {
int sx = x * src_w / dst_w;
if (sx >= src_w) sx = src_w - 1;
dst_row[cr.x + x] = src_row[sx];
}
}
}
} else {
int blit_w = cr.w < win->content_w ? cr.w : win->content_w;
int blit_h = cr.h < win->content_h ? cr.h : win->content_h;
fb.blit(cr.x, cr.y, blit_w, blit_h, win->content);
}
}
}
gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my) {
using namespace gui;
int G = RESIZE_GRAB;
if (!f.contains(mx, my)) return RESIZE_NONE;
bool near_left = mx < f.x + G;
bool near_right = mx >= f.x + f.w - G;
bool near_top = my < f.y + G;
bool near_bottom = my >= f.y + f.h - G;
// Corners first
if (near_top && near_left) return RESIZE_TOP_LEFT;
if (near_top && near_right) return RESIZE_TOP_RIGHT;
if (near_bottom && near_left) return RESIZE_BOTTOM_LEFT;
if (near_bottom && near_right) return RESIZE_BOTTOM_RIGHT;
// Edges
if (near_top) return RESIZE_TOP;
if (near_bottom) return RESIZE_BOTTOM;
if (near_left) return RESIZE_LEFT;
if (near_right) return RESIZE_RIGHT;
return RESIZE_NONE;
}
gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge) {
using namespace gui;
switch (edge) {
case RESIZE_LEFT: case RESIZE_RIGHT:
return CURSOR_RESIZE_H;
case RESIZE_TOP: case RESIZE_BOTTOM:
return CURSOR_RESIZE_V;
case RESIZE_TOP_LEFT: case RESIZE_BOTTOM_RIGHT:
return CURSOR_RESIZE_NWSE;
case RESIZE_TOP_RIGHT: case RESIZE_BOTTOM_LEFT:
return CURSOR_RESIZE_NESW;
default:
return CURSOR_ARROW;
}
}