std1.0.0-beta-7 SafeC docs

Source gui/gui_window.sc

View in the repository

// SafeC Standard Library — GUI: the portable window layer (see gui.h and
// gui_backend.h). GuiWindow's methods, the event queues, the clipboard and
// the clock, all on top of whichever struct GuiBackend is in use — this
// file contains no platform code.
#pragma once
#include <std/stddef.h>
#include <std/gui/gui.h>
#include <std/gui/gui_backend.h>
#include <std/gui/gui_gpu.h>
#include <std/mem.h>
#include <std/collections/vec.h>
#include <std/collections/string.h>

namespace std {

// ── backend selection ─────────────────────────────────────────────────────

static ?&GuiBackend gGuiInstalled = null;
static int gGuiInitState = 0;          // 0 = not tried, 1 = ready, -1 = init failed
static unsigned int gGuiNextId = 1U;
static struct Vec<&GuiWindowState> gGuiWindows;
static int gGuiWindowsInit = 0;

static &GuiBackend __gui_be_raw() {
    match (gGuiInstalled) {
        case some(b): return b;
        case null: { }
    }
    return gui_platform_backend();
}

&GuiBackend gui_backend() {
    gui_backend_ready();
    return __gui_be_raw();
}

int gui_backend_ready() {
    &GuiBackend b = __gui_be_raw();
    if (gGuiInitState == 0) {
        gGuiInitState = b.init() ? 1 : -1;
    }
    return gGuiInitState == 1 ? 1 : 0;
}

int gui_set_backend(&GuiBackend b) {
    if (gui_backend_window_count() > 0UL) { return 0; }
    gGuiInstalled = b;
    gGuiInitState = 0;
    return 1;
}

static void __gui_windows_init() {
    if (!gGuiWindowsInit) {
        gGuiWindows = Vec::new();
        gGuiWindowsInit = 1;
    }
}

unsigned long gui_backend_window_count() {
    if (!gGuiWindowsInit) { return 0UL; }
    return gGuiWindows.len;
}

?&GuiWindowState gui_backend_window_at(unsigned long i) {
    if (!gGuiWindowsInit || i >= gGuiWindows.len) { return null; }
    return *gGuiWindows.at(i);
}

?&GuiWindowState gui_backend_find(unsigned long long key) {
    if (!gGuiWindowsInit || key == 0ULL) { return null; }
    unsigned long i = 0UL;
    while (i < gGuiWindows.len) {
        &GuiWindowState st = *gGuiWindows.at(i);
        if (st.key == key) { return st; }
        i = i + 1UL;
    }
    return null;
}

static unsigned long __gui_queue_pending(&GuiWindowState st) {
    return st.queue.len - st.qhead;
}

int gui_backend_has_events() {
    if (!gGuiWindowsInit) { return 0; }
    unsigned long i = 0UL;
    while (i < gGuiWindows.len) {
        &GuiWindowState st = *gGuiWindows.at(i);
        if (__gui_queue_pending(st) > 0UL) { return 1; }
        i = i + 1UL;
    }
    return 0;
}

double gui_time_ms() {
    &GuiBackend b = __gui_be_raw();
    return (double)b.now_ns() / 1000000.0;
}

void gui_backend_post(&GuiWindowState st, struct GuiEvent ev) {
    ev.windowId = st.id;
    ev.defaultPrevented = 0;
    if (ev.timeMs == 0.0) { ev.timeMs = gui_time_ms(); }
    if (ev.kind == GUI_EVENT_MOUSE_DOWN) {
        // Two presses of the same button within 500 ms and 5 pixels of each
        // other make a double click, a third one a triple click.
        double dx = ev.x - st.lastDownX;
        double dy = ev.y - st.lastDownY;
        if (dx < 0.0) { dx = -dx; }
        if (dy < 0.0) { dy = -dy; }
        if (st.clickCount > 0 && ev.button == st.lastDownButton &&
            ev.timeMs - st.lastDownMs <= 500.0 && dx <= 5.0 * st.scale && dy <= 5.0 * st.scale) {
            st.clickCount = st.clickCount + 1;
            if (st.clickCount > 3) { st.clickCount = 1; }
        } else {
            st.clickCount = 1;
        }
        st.lastDownMs = ev.timeMs;
        st.lastDownX = ev.x;
        st.lastDownY = ev.y;
        st.lastDownButton = ev.button;
        ev.clicks = st.clickCount;
    }
    if (ev.kind == GUI_EVENT_MOUSE_ENTER) { st.hovered = 1; }
    if (ev.kind == GUI_EVENT_MOUSE_LEAVE) { st.hovered = 0; }
    st.queue.push(ev);
}

static int __gui_queue_pop(&GuiWindowState st, &GuiEvent out) {
    if (st.qhead >= st.queue.len) {
        st.queue.clear();
        st.qhead = 0UL;
        return 0;
    }
    *out = *st.queue.at(st.qhead);
    st.qhead = st.qhead + 1UL;
    if (st.qhead >= st.queue.len) {
        st.queue.clear();
        st.qhead = 0UL;
    }
    return 1;
}

?&GuiWindowState gui_window_state(const &GuiWindow win) {
    unsafe {
        if (win.platform.is_null()) { return null; }
        return (struct GuiWindowState*)win.platform;
    }
}

// ── events, options ───────────────────────────────────────────────────────

struct GuiEvent GuiEvent::new(int kind) {
    struct GuiEvent e;
    safe_memset_obj(&e, 0);
    e.kind = kind;
    return e;
}

void GuiEvent::prevent_default() {
    self.defaultPrevented = 1;
}

int GuiEvent::has_mod(int mod) const {
    return (self.modifiers & mod) != 0 ? 1 : 0;
}

int GuiEvent::primary_mod() const {
    return (self.modifiers & gui_primary_mod()) != 0 ? 1 : 0;
}

int gui_primary_mod() {
    &GuiBackend b = __gui_be_raw();
    return b.primaryMod;
}

double gui_display_scale() {
    if (!gui_backend_ready()) { return 1.0; }
    &GuiBackend b = gui_backend();
    double s = b.display_scale();
    return s > 0.0 ? s : 1.0;
}

struct GuiWindowOptions GuiWindowOptions::new(const &char title, int width, int height) {
    struct GuiWindowOptions o;
    o.title = title;
    o.x = GUI_POS_DEFAULT;
    o.y = GUI_POS_DEFAULT;
    o.width = width;
    o.height = height;
    o.minWidth = 0;
    o.minHeight = 0;
    o.maxWidth = 0;
    o.maxHeight = 0;
    o.resizable = 1;
    o.decorated = 1;
    o.visible = 1;
    o.hiDpi = 1;
    o.topmost = 0;
    o.present = GUI_PRESENT_DEFAULT;
    return o;
}

// ── creation / destruction ────────────────────────────────────────────────

static &heap unsigned char __gui_alloc_pixels(int w, int h) {
    unsigned long n = (unsigned long)(w > 0 ? w : 1) * (unsigned long)(h > 0 ? h : 1) * 4UL;
    unsafe { return (&heap unsigned char)alloc_zeroed_raw_(n); }
}

static struct GuiWindow __gui_window_blank() {
    struct GuiWindow win;
    win.platform = null;
    unsafe { win.pixels = (&heap unsigned char)0; }
    win.width = 0;
    win.height = 0;
    win.shouldClose = 0;
    win.scale = 1.0;
    win.id = 0U;
    return win;
}

struct GuiWindow GuiWindow::offscreen(int width, int height) {
    struct GuiWindow win = __gui_window_blank();
    if (width < 1) { width = 1; }
    if (height < 1) { height = 1; }
    win.pixels = __gui_alloc_pixels(width, height);
    win.width = width;
    win.height = height;
    return win;
}

struct GuiWindow GuiWindow::create(const &char title, int width, int height) {
    struct GuiWindowOptions o = GuiWindowOptions::new(title, width, height);
    o.hiDpi = 0;
    return GuiWindow::create_with(&o);
}

struct GuiWindow GuiWindow::create_with(const &GuiWindowOptions opts) {
    struct GuiWindow win = __gui_window_blank();
    if (!gui_backend_ready()) { return win; }
    &GuiBackend b = gui_backend();
    __gui_windows_init();

    &GuiWindowState st;
    unsafe {
        st = (struct GuiWindowState*)alloc_zeroed_raw_(sizeof(struct GuiWindowState));
    }
    st.id = gGuiNextId;
    st.key = 0ULL;
    st.native = null;
    st.presenterData = null;
    st.presenter = null;
    st.queue = Vec::new();
    st.qhead = 0UL;
    unsafe {
        match (opts.title) {
            case some(t): st.title = String::from(t);
            case null: st.title = String::new();
        }
    }
    st.x = opts.x;
    st.y = opts.y;
    st.logicalW = opts.width > 0 ? opts.width : 1;
    st.logicalH = opts.height > 0 ? opts.height : 1;
    st.scale = 1.0;
    st.pixelW = st.logicalW;
    st.pixelH = st.logicalH;
    st.hiDpi = opts.hiDpi;
    st.minW = opts.minWidth; st.minH = opts.minHeight;
    st.maxW = opts.maxWidth; st.maxH = opts.maxHeight;
    st.visible = opts.visible;
    st.focused = 0;
    st.hovered = 0;
    st.cursor = GUI_CURSOR_ARROW;
    st.resizable = opts.resizable;
    st.decorated = opts.decorated;
    st.topmost = opts.topmost;
    st.clickCount = 0;
    st.lastDownMs = -1000000.0;

    if (!b.window_create(st, opts)) {
        unsafe {   // the state block (raw heap) is taken apart and freed
            st.queue.free();
            st.title.free();
            dealloc((?&heap GuiWindowState)st);
        }
        return win;
    }
    gGuiNextId = gGuiNextId + 1U;
    if (st.scale <= 0.0) { st.scale = 1.0; }
    if (st.pixelW < 1) { st.pixelW = 1; }
    if (st.pixelH < 1) { st.pixelH = 1; }
    gGuiWindows.push(st);

    win.id = st.id;
    win.scale = st.scale;
    win.width = st.pixelW;
    win.height = st.pixelH;
    win.pixels = __gui_alloc_pixels(win.width, win.height);
    unsafe { win.platform = (?&GuiWindowState)st; }
    if (gui_gpu_wanted(opts.present)) { gui_window_use_gpu(&win); }
    return win;
}

static void __gui_detach_presenter(&GuiWindowState st) {
    unsafe {
        if (!st.presenter.is_null()) {
            struct GuiPresenter* p = (struct GuiPresenter*)st.presenter;
            if (p->detach != (fn void(&GuiWindowState))0) { p->detach(st); }
            st.presenter = null;
            st.presenterData = null;
        }
    }
}

void GuiWindow::destroy() { self.drop(); }

void GuiWindow::drop() {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case some(st): {
            __gui_detach_presenter(st);
            &GuiBackend b = gui_backend();
            b.window_destroy(st);
            unsigned long i = 0UL;
            while (i < gGuiWindows.len) {
                &GuiWindowState other = *gGuiWindows.at(i);
                unsafe {
                    if ((const unsigned char*)other == (const unsigned char*)st) { gGuiWindows.remove(i); break; }
                }
                i = i + 1UL;
            }
            unsafe {   // the state block (raw heap) is taken apart and freed
                st.queue.free();
                st.title.free();
                dealloc((?&heap GuiWindowState)st);
            }
        }
        case null: { }
    }
    unsafe {
        if ((const unsigned char*)self.pixels != (const unsigned char*)0) { dealloc(self.pixels); }
        self.pixels = (&heap unsigned char)0;
    }
    self.platform = null;
    self.width = 0;
    self.height = 0;
}

// ── events ────────────────────────────────────────────────────────────────

// Brings the pixel buffer to the size the backend reported.
static void __gui_window_apply_size(&GuiWindow win, &GuiWindowState st, int w, int h) {
    win.scale = st.scale;
    if (w < 1 || h < 1) { return; }
    if (w == win.width && h == win.height) { return; }
    &heap unsigned char px = __gui_alloc_pixels(w, h);
    unsafe {
        if ((const unsigned char*)win.pixels != (const unsigned char*)0) { dealloc(win.pixels); }
    }
    win.pixels = px;
    win.width = w;
    win.height = h;
}

static int __gui_window_take(&GuiWindow win, &GuiWindowState st, &GuiEvent out) {
    while (__gui_queue_pop(st, out)) {
        int k = out.kind;
        if (k == GUI_EVENT_CLOSE) { win.shouldClose = 1; }
        if (k == GUI_EVENT_RESIZE || k == GUI_EVENT_SCALE) {
            if (out.width < 1 || out.height < 1) { continue; }   // minimized
            int same = (out.width == win.width && out.height == win.height) ? 1 : 0;
            __gui_window_apply_size(win, st, out.width, out.height);
            if (same && k == GUI_EVENT_RESIZE) { continue; }     // nothing changed
        }
        return 1;
    }
    return 0;
}

int GuiWindow::poll_event(&GuiEvent outEvent) {
    outEvent.kind = GUI_EVENT_NONE;
    outEvent.defaultPrevented = 0;
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return 0;
        case some(st): {
            if (__gui_queue_pending(st) == 0UL) {
                &GuiBackend b = gui_backend();
                b.pump(0);
            }
            return __gui_window_take(self, st, outEvent);
        }
    }
    return 0;
}

int GuiWindow::wait_event(&GuiEvent outEvent, int timeoutMs) {
    outEvent.kind = GUI_EVENT_NONE;
    outEvent.defaultPrevented = 0;
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return 0;
        case some(st): {
            &GuiBackend b = gui_backend();
            if (__gui_queue_pending(st) == 0UL) { b.pump(0); }
            if (__gui_queue_pending(st) == 0UL) {
                double start = gui_time_ms();
                while (__gui_queue_pending(st) == 0UL) {
                    int left = timeoutMs;
                    if (timeoutMs >= 0) {
                        left = timeoutMs - (int)(gui_time_ms() - start);
                        if (left <= 0) { break; }
                    }
                    b.pump(left);
                }
            }
            return __gui_window_take(self, st, outEvent);
        }
    }
    return 0;
}

// ── presentation ──────────────────────────────────────────────────────────

void GuiWindow::present() {
    self.present_rect(0, 0, self.width, self.height);
}

void GuiWindow::present_rect(int x, int y, int w, int h) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            if (x < 0) { w = w + x; x = 0; }
            if (y < 0) { h = h + y; y = 0; }
            if (x + w > self.width) { w = self.width - x; }
            if (y + h > self.height) { h = self.height - y; }
            if (w <= 0 || h <= 0) { return; }
            unsafe {
                const unsigned char* px = (const unsigned char*)self.pixels;
                if (!st.presenter.is_null()) {
                    struct GuiPresenter* p = (struct GuiPresenter*)st.presenter;
                    p->present(st, px, self.width, self.height, x, y, w, h);
                    return;
                }
            }
            &GuiBackend b = gui_backend();
            unsafe {
                b.window_present(st, (const unsigned char*)self.pixels, self.width, self.height, x, y, w, h);
            }
        }
    }
}

int gui_window_set_presenter(&GuiWindow win, ?&GuiPresenter p) {
    ?&GuiWindowState ost = gui_window_state(win);
    match (ost) {
        case null: return 0;
        case some(st): {
            __gui_detach_presenter(st);
            match (p) {
                case null: return 1;
                case some(pp): {
                    &GuiBackend b = gui_backend();
                    struct GuiNativeHandle h = b.native_handle(st);
                    unsafe { st.presenter = (?&GuiPresenter)pp; }
                    if (!pp.attach(st, h)) {
                        st.presenter = null;
                        st.presenterData = null;
                        return 0;
                    }
                    return 1;
                }
            }
        }
    }
    return 0;
}

// ── pixels ────────────────────────────────────────────────────────────────

void GuiWindow::set_pixel(int x, int y, unsigned int rgba) {
    if (x < 0 || y < 0 || x >= self.width || y >= self.height) { return; }
    unsafe {
        unsigned long i = ((unsigned long)y * (unsigned long)self.width + (unsigned long)x) * 4UL;
        self.pixels[i + 0UL] = (unsigned char)((rgba >> 24) & 0xFFU);
        self.pixels[i + 1UL] = (unsigned char)((rgba >> 16) & 0xFFU);
        self.pixels[i + 2UL] = (unsigned char)((rgba >> 8) & 0xFFU);
        self.pixels[i + 3UL] = (unsigned char)(rgba & 0xFFU);
    }
}

unsigned int GuiWindow::get_pixel(int x, int y) const {
    if (x < 0 || y < 0 || x >= self.width || y >= self.height) { return 0U; }
    unsafe {
        unsigned long i = ((unsigned long)y * (unsigned long)self.width + (unsigned long)x) * 4UL;
        return ((unsigned int)self.pixels[i] << 24) | ((unsigned int)self.pixels[i + 1UL] << 16) |
               ((unsigned int)self.pixels[i + 2UL] << 8) | (unsigned int)self.pixels[i + 3UL];
    }
}

void GuiWindow::clear(struct GuiColor c) {
    unsigned int a = (unsigned int)c.a;
    unsigned int r = ((unsigned int)c.r * a + 127U) / 255U;
    unsigned int g = ((unsigned int)c.g * a + 127U) / 255U;
    unsigned int b = ((unsigned int)c.b * a + 127U) / 255U;
    unsafe {
        unsigned int* p = (unsigned int*)self.pixels;
        unsigned long n = (unsigned long)self.width * (unsigned long)self.height;
        // Memory order R,G,B,A: as a little-endian word that's A<<24|B<<16|G<<8|R.
        unsigned char bytes[4];
        bytes[0] = (unsigned char)r; bytes[1] = (unsigned char)g; bytes[2] = (unsigned char)b; bytes[3] = (unsigned char)a;
        unsigned int word = *(unsigned int*)&bytes[0];
        unsigned long i = 0UL;
        while (i < n) { p[i] = word; i = i + 1UL; }
    }
}

// ── window management ────────────────────────────────────────────────────

void GuiWindow::set_title(const &char title) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            std::replace(&st.title, String::from(title)).drop();
            &GuiBackend b = gui_backend();
            b.window_set_title(st, title);
        }
    }
}

void GuiWindow::set_size(int width, int height) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            if (width < 1) { width = 1; }
            if (height < 1) { height = 1; }
            &GuiBackend b = gui_backend();
            b.window_set_size(st, width, height);
        }
    }
}

void GuiWindow::set_position(int x, int y) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            &GuiBackend b = gui_backend();
            b.window_set_position(st, x, y);
            // what was asked for until the platform reports where it went
            st.x = x;
            st.y = y;
        }
    }
}

int GuiWindow::pos_x() const {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) { case null: return GUI_POS_DEFAULT; case some(st): return st.x; }
    return GUI_POS_DEFAULT;
}

int GuiWindow::pos_y() const {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) { case null: return GUI_POS_DEFAULT; case some(st): return st.y; }
    return GUI_POS_DEFAULT;
}

void GuiWindow::set_min_size(int width, int height) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            st.minW = width; st.minH = height;
            &GuiBackend b = gui_backend();
            b.window_update_limits(st);
        }
    }
}

void GuiWindow::set_max_size(int width, int height) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            st.maxW = width; st.maxH = height;
            &GuiBackend b = gui_backend();
            b.window_update_limits(st);
        }
    }
}

static void __gui_window_visible(&GuiWindow win, int v) {
    ?&GuiWindowState ost = gui_window_state(win);
    match (ost) {
        case null: return;
        case some(st): {
            st.visible = v;
            &GuiBackend b = gui_backend();
            b.window_set_visible(st, v);
        }
    }
}

void GuiWindow::show() { __gui_window_visible(self, 1); }
void GuiWindow::hide() { __gui_window_visible(self, 0); }

int GuiWindow::is_visible() const {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) { case null: return 0; case some(st): return st.visible; }
    return 0;
}

void GuiWindow::focus() {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            &GuiBackend b = gui_backend();
            b.window_focus(st);
        }
    }
}

int GuiWindow::is_focused() const {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) { case null: return 0; case some(st): return st.focused; }
    return 0;
}

void GuiWindow::set_cursor(int cursor) {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): {
            if (cursor < 0 || cursor >= GUI_CURSOR_COUNT) { cursor = GUI_CURSOR_ARROW; }
            if (st.cursor == cursor) { return; }
            st.cursor = cursor;
            &GuiBackend b = gui_backend();
            b.window_set_cursor(st, cursor);
        }
    }
}

int GuiWindow::logical_width() const {
    double s = self.scale > 0.0 ? self.scale : 1.0;
    return (int)((double)self.width / s + 0.5);
}

int GuiWindow::logical_height() const {
    double s = self.scale > 0.0 ? self.scale : 1.0;
    return (int)((double)self.height / s + 0.5);
}

void GuiWindow::request_close() {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return;
        case some(st): { gui_backend_post(st, GuiEvent::new(GUI_EVENT_CLOSE)); }
    }
}

unsigned long long GuiWindow::native_handle() const {
    ?&GuiWindowState ost = gui_window_state(self);
    match (ost) {
        case null: return 0ULL;
        case some(st): {
            &GuiBackend b = gui_backend();
            struct GuiNativeHandle h = b.native_handle(st);
            return h.window;
        }
    }
    return 0ULL;
}

// ── clipboard ─────────────────────────────────────────────────────────────

int gui_clipboard_set_text(const &char utf8) {
    if (!gui_backend_ready()) { return 0; }
    &GuiBackend b = gui_backend();
    unsigned long n = 0UL;
    unsafe { while (utf8[n] != '\0') { n = n + 1UL; } }
    return b.clipboard_set(utf8, n);
}

int gui_clipboard_get_text(&String out) {
    out.clear();
    if (!gui_backend_ready()) { return 0; }
    &GuiBackend b = gui_backend();
    return b.clipboard_get(out);
}

} // namespace std