commit 8325266162b7297c7ae1b75d4f3d2871ce3ec62c
parent 1c3df920be5b9486b7d045cdacd8f07d954b202b
Author: bsandro <email@bsandro.tech>
Date: Sun, 21 Dec 2025 18:18:38 +0200
using luigi2 (from gf)
Diffstat:
5 files changed, 7255 insertions(+), 23 deletions(-)
diff --git a/TODO b/TODO
@@ -21,3 +21,14 @@
- 'System' menu emulation (File, Open, Save)
- Support saving spritesheet as .png file
- 'Help' and 'About' windows
+- Preview of animated .webp files in the 'Open' dialog
+
+ if (flags & UI_WINDOW_DIALOG) {
+ Atom atoms[] = {
+ XInternAtom(ui.display, "_NET_WM_WINDOW_TYPE", 0),
+ XInternAtom(ui.display, "_NET_WM_WINDOW_TYPE_DIALOG", 0),
+ XInternAtom(ui.display, "_MOTIF_WM_HINTS", 0)
+ };
+ XChangeProperty(ui.display, window->window, XInternAtom(ui.display, "_NET_WM_STATE", 0), XA_ATOM, 32, PropModeReplace, (unsigned char *)atoms, sizeof(atoms)/sizeof(*atoms));
+ XSetTransientForHint(ui.display, window->window, DefaultRootWindow(ui.display));
+ }
diff --git a/gui/Makefile b/gui/Makefile
@@ -22,6 +22,10 @@ else
CFLAGS+=-DUI_LINUX
endif
+ifeq ($(shell uname -m),x86_64)
+CFLAGS+=-DUI_SSE2
+endif
+
all: $(NAME)
.PHONY: clean run
diff --git a/gui/ui.c b/gui/ui.c
@@ -40,9 +40,9 @@ UIWindow * MainWindowCreate(const char *wname, int w, int h) {
//assert(font!=NULL);
UIWindow *win = UIWindowCreate(0, 0, wname, w, h);
win->e.messageUser = WinMainEvent;
- UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING);
+ UIPanel *panelv = UIPanelCreate(&win->e, UI_PANEL_COLOR_2|UI_PANEL_MEDIUM_SPACING);
// label with filename, buttons open/save/exit
- UIPanel *panelh_top = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL|UI_ELEMENT_H_FILL);
+ UIPanel *panelh_top = UIPanelCreate(&panelv->e, UI_PANEL_COLOR_2|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL|UI_ELEMENT_H_FILL);
UIButton *btnopen = UIButtonCreate(&panelh_top->e, 0, "Open", -1);
btnopen->e.messageUser = ButtonOpenEvent;
UIButton *btnsave = UIButtonCreate(&panelh_top->e, 0, "Save", -1);
@@ -53,7 +53,7 @@ UIWindow * MainWindowCreate(const char *wname, int w, int h) {
lbl_header = UILabelCreate(&panelh_top->e, 0, "webp to spritesheet converter", -1);
// scroll with dimensions label
- UIPanel *panelh_bottom = UIPanelCreate(&panelv->e, UI_PANEL_GRAY|UI_PANEL_SMALL_SPACING|UI_PANEL_HORIZONTAL|UI_PANEL_EXPAND|UI_ELEMENT_H_FILL);
+ UIPanel *panelh_bottom = UIPanelCreate(&panelv->e, UI_PANEL_COLOR_2|UI_PANEL_SMALL_SPACING|UI_PANEL_HORIZONTAL|UI_PANEL_EXPAND|UI_ELEMENT_H_FILL);
UILabel *label = UILabelCreate(&panelh_bottom->e, 0, "Size: 0x0px", -1);
UIButton *btnminus = UIButtonCreate(&panelh_bottom->e, 0, "-", -1);
UIButton *btnplus = UIButtonCreate(&panelh_bottom->e, 0, "+", -1);
@@ -72,10 +72,10 @@ UIWindow * MainWindowCreate(const char *wname, int w, int h) {
}
int WinModalEvent(UIElement *element, UIMessage msg, int di, void *dp) {
- if (msg==UI_MSG_WINDOW_CLOSE) {
- UIElementDestroy(element);
- return 1;
- }
+ if (msg==UI_MSG_WINDOW_CLOSE) {
+ UIElementDestroy(element);
+ return 1;
+ }
if (msg==UI_MSG_DESTROY) {
assert(element==(UIElement *)modal_win);
modal_win = NULL;
@@ -115,8 +115,8 @@ int ButtonPlusEvent(UIElement *element, UIMessage msg, int di, void *dp) {
int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
if (msg==UI_MSG_CLICKED) {
// get values of path and filename inputs
- UITextbox *path_input = (UITextbox *)element->parent->children;
- UITextbox *filename_input = (UITextbox *)path_input->e.next;
+ UITextbox *path_input = (UITextbox *)element->parent->children[0];
+ UITextbox *filename_input = (UITextbox *)element->parent->children[1];
// printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
// that printf might work incorrectly because path_input->string might not contain valid C string with \0 at the end
// printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, path_input->bytes, filename_input->string, filename_input->bytes);
@@ -140,8 +140,8 @@ int ButtonDialogSaveEvent(UIElement *element, UIMessage msg, int di, void *dp) {
int ButtonDialogOpenEvent(UIElement *element, UIMessage msg, int di, void *dp) {
if (msg==UI_MSG_CLICKED) {
// printf("open dialog window close\n");
- UITextbox *path_input = (UITextbox *)element->parent->children;
- UITextbox *filename_input = (UITextbox *)path_input->e.next;
+ UITextbox *path_input = (UITextbox *)element->parent->children[0];
+ UITextbox *filename_input = (UITextbox *)element->parent->children[1];
// printf("path_input: %p\nfilename_input: %p\n", path_input, filename_input);
// printf("path_input: %s(%d)\nfilename_input: %s(%d)\n", path_input->string, strlen(path_input->string), filename_input->string, strlen(filename_input->string));
@@ -199,9 +199,9 @@ int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) {
selected = -1;
//@todo duplicated code
UIPanel *panel_out = (UIPanel *)element->parent;
- UILabel *label = (UILabel *)panel_out->e.children;
- UIPanel *panel_top = (UIPanel *)label->e.next;
- UITextbox *path_input = (UITextbox *)panel_top->e.children;
+ UILabel *label = (UILabel *)panel_out->e.children[0];
+ UIPanel *panel_top = (UIPanel *)panel_out->e.children[1];
+ UITextbox *path_input = (UITextbox *)panel_top->e.children[0];
UITextboxClear(path_input, false);
UITextboxReplace(path_input, (char *)element->cp, -1, false);
UIElementRepaint(&path_input->e, NULL);
@@ -215,10 +215,10 @@ int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) {
//@todo remove copypasta
if (hit!=-1 && filelist[hit]->d_type!=DT_DIR) {
UIPanel *panel_out = (UIPanel *)element->parent;
- UILabel *label = (UILabel *)panel_out->e.children;
- UIPanel *panel_top = (UIPanel *)label->e.next;
- UITextbox *path_input = (UITextbox *)panel_top->e.children;
- UITextbox *file_input = (UITextbox *)path_input->e.next;
+ UILabel *label = (UILabel *)panel_out->e.children[0];
+ UIPanel *panel_top = (UIPanel *)panel_out->e.children[1];
+ UITextbox *path_input = (UITextbox *)panel_top->e.children[0];
+ UITextbox *file_input = (UITextbox *)panel_top->e.children[1];
UITextboxClear(path_input, false);
UITextboxClear(file_input, false);
UITextboxReplace(path_input, (char *)element->cp, -1, false);
@@ -231,9 +231,9 @@ int TableEvent(UIElement *element, UIMessage msg, int di, void *dp) {
free(filelist);
filelist = NULL;
selected = -1;
- UIElement *el = table->e.children;
+ UIElement *el = table->e.children[0];
printf("child %p\n", el);
- while (el!=NULL) {
+ if (el!=NULL) {
printf("child %p\n", el);
}
} else if (msg==UI_MSG_UPDATE) {
@@ -257,9 +257,9 @@ void ShowModalWindow(UIWindow *parent, char *def_dir, const char *def_file, Call
int h = UIElementMessage(&parent->e, UI_MSG_GET_HEIGHT, 0, 0);
modal_win = UIWindowCreate(parent, 0, def_file?"Save File":"Open File", w, h);
modal_win->e.messageUser = WinModalEvent;
- UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_GRAY|UI_PANEL_EXPAND);
+ UIPanel *panel_out = UIPanelCreate(&modal_win->e, UI_PANEL_COLOR_2|UI_PANEL_EXPAND);
UILabel *lbl_title = UILabelCreate(&panel_out->e, 0, def_file?"Save File":"Open File", -1);
- UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_GRAY|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
+ UIPanel *panel_top = UIPanelCreate(&panel_out->e, UI_PANEL_COLOR_2|UI_PANEL_MEDIUM_SPACING|UI_PANEL_HORIZONTAL);
UITextbox *path_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_DISABLED|UI_ELEMENT_H_FILL);
UITextboxReplace(path_input, def_dir, -1, false);
UITextbox *filename_input = UITextboxCreate(&panel_top->e, UI_ELEMENT_TAB_STOP|UI_ELEMENT_H_FILL);
@@ -327,6 +327,10 @@ int WinMainEvent(UIElement *element, UIMessage msg, int di, void *dp) {
free(img);
exit(0);
}
+ if (modal_win!=NULL) {
+ UIElementFocus((UIElement *)modal_win);
+ return 1;
+ }
return 0;
}
diff --git a/gui/ui.h b/gui/ui.h
@@ -7,7 +7,7 @@
#define DIR_SEPARATOR '/'
#endif
-#include "luigi.h"
+#include "luigi2.h"
#define UI_COLOR_FROM_RGBA(r, g, b, a) (((uint32_t) (r) << 16) | ((uint32_t) (g) << 8) | ((uint32_t) (b) << 0) | ((uint32_t) (a) << 24))
diff --git a/include/luigi2.h b/include/luigi2.h
@@ -0,0 +1,7213 @@
+// TODO UITextbox features - mouse input, undo, number dragging.
+// TODO New elements - list view, menu bar.
+// TODO Keyboard navigation in menus.
+// TODO Easier to use fonts.
+
+/////////////////////////////////////////
+// Header includes.
+/////////////////////////////////////////
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdarg.h>
+
+#ifdef UI_LINUX
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#include <X11/cursorfont.h>
+#endif
+
+#ifdef UI_SSE2
+#include <xmmintrin.h>
+#endif
+
+#ifdef UI_WINDOWS
+#undef _UNICODE
+#undef UNICODE
+#include <windows.h>
+
+#define UI_ASSERT(x) do { if (!(x)) { ui.assertionFailure = true; \
+ MessageBox(0, "Assertion failure on line " _UI_TO_STRING_2(__LINE__), 0, 0); \
+ ExitProcess(1); } } while (0)
+#define UI_CALLOC(x) HeapAlloc(ui.heap, HEAP_ZERO_MEMORY, (x))
+#define UI_FREE(x) HeapFree(ui.heap, 0, (x))
+#define UI_MALLOC(x) HeapAlloc(ui.heap, 0, (x))
+#define UI_REALLOC _UIHeapReAlloc
+#define UI_CLOCK GetTickCount
+#define UI_CLOCKS_PER_SECOND (1000)
+#define UI_CLOCK_T DWORD
+#define UI_MEMMOVE _UIMemmove
+#endif
+
+#ifdef UI_COCOA
+#import <Foundation/Foundation.h>
+#import <Cocoa/Cocoa.h>
+#import <Carbon/Carbon.h>
+#endif
+
+#if defined(UI_LINUX) || defined(UI_COCOA)
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <time.h>
+#include <math.h>
+
+#define UI_ASSERT assert
+#define UI_CALLOC(x) calloc(1, (x))
+#define UI_FREE free
+#define UI_MALLOC malloc
+#define UI_REALLOC realloc
+#define UI_CLOCK _UIClock
+#define UI_CLOCKS_PER_SECOND 1000
+#define UI_CLOCK_T clock_t
+#define UI_MEMMOVE(d, s, n) do { size_t _n = n; if (_n) { memmove(d, s, _n); } } while (0)
+#endif
+
+#if defined(UI_ESSENCE)
+#include <essence.h>
+
+#define UI_ASSERT EsAssert
+#define UI_CALLOC(x) EsHeapAllocate((x), true)
+#define UI_FREE EsHeapFree
+#define UI_MALLOC(x) EsHeapAllocate((x), false)
+#define UI_REALLOC(x, y) EsHeapReallocate((x), (y), false)
+#define UI_CLOCK EsTimeStampMs
+#define UI_CLOCKS_PER_SECOND 1000
+#define UI_CLOCK_T uint64_t
+#define UI_MEMMOVE EsCRTmemmove
+
+// Callback to allow the application to process messages.
+void _UIMessageProcess(EsMessage *message);
+#endif
+
+#ifdef UI_DEBUG
+#include <stdio.h>
+#endif
+
+#ifdef UI_FREETYPE
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include <freetype/ftbitmap.h>
+#endif
+
+/////////////////////////////////////////
+// Definitions.
+/////////////////////////////////////////
+
+#define _UI_TO_STRING_1(x) #x
+#define _UI_TO_STRING_2(x) _UI_TO_STRING_1(x)
+
+#define UI_SIZE_BUTTON_MINIMUM_WIDTH (100)
+#define UI_SIZE_BUTTON_PADDING (16)
+#define UI_SIZE_BUTTON_HEIGHT (27)
+#define UI_SIZE_BUTTON_CHECKED_AREA (4)
+
+#define UI_SIZE_CHECKBOX_BOX (14)
+#define UI_SIZE_CHECKBOX_GAP (8)
+
+#define UI_SIZE_MENU_ITEM_HEIGHT (24)
+#define UI_SIZE_MENU_ITEM_MINIMUM_WIDTH (160)
+#define UI_SIZE_MENU_ITEM_MARGIN (9)
+
+#define UI_SIZE_GAUGE_WIDTH (200)
+#define UI_SIZE_GAUGE_HEIGHT (22)
+
+#define UI_SIZE_SLIDER_WIDTH (200)
+#define UI_SIZE_SLIDER_HEIGHT (25)
+#define UI_SIZE_SLIDER_THUMB (15)
+#define UI_SIZE_SLIDER_TRACK (5)
+
+#define UI_SIZE_TEXTBOX_MARGIN (3)
+#define UI_SIZE_TEXTBOX_WIDTH (200)
+#define UI_SIZE_TEXTBOX_HEIGHT (27)
+
+#define UI_SIZE_TAB_PANE_SPACE_TOP (2)
+#define UI_SIZE_TAB_PANE_SPACE_LEFT (4)
+
+#define UI_SIZE_SPLITTER (8)
+
+#define UI_SIZE_SCROLL_BAR (16)
+#define UI_SIZE_SCROLL_MINIMUM_THUMB (20)
+
+#define UI_SIZE_CODE_MARGIN (ui.activeFont->glyphWidth * 5)
+#define UI_SIZE_CODE_MARGIN_GAP (ui.activeFont->glyphWidth * 1)
+
+#define UI_SIZE_TABLE_HEADER (26)
+#define UI_SIZE_TABLE_COLUMN_GAP (20)
+#define UI_SIZE_TABLE_ROW (20)
+
+#define UI_SIZE_PANE_LARGE_BORDER (20)
+#define UI_SIZE_PANE_LARGE_GAP (10)
+#define UI_SIZE_PANE_MEDIUM_BORDER (5)
+#define UI_SIZE_PANE_MEDIUM_GAP (5)
+#define UI_SIZE_PANE_SMALL_BORDER (3)
+#define UI_SIZE_PANE_SMALL_GAP (3)
+
+#define UI_SIZE_MDI_CHILD_BORDER (6)
+#define UI_SIZE_MDI_CHILD_TITLE (30)
+#define UI_SIZE_MDI_CHILD_CORNER (12)
+#define UI_SIZE_MDI_CHILD_MINIMUM_WIDTH (100)
+#define UI_SIZE_MDI_CHILD_MINIMUM_HEIGHT (50)
+#define UI_SIZE_MDI_CASCADE (30)
+
+#define UI_MDI_CHILD_CALCULATE_LAYOUT(bounds, scale) \
+ int titleSize = UI_SIZE_MDI_CHILD_TITLE * scale; \
+ int borderSize = UI_SIZE_MDI_CHILD_BORDER * scale; \
+ UIRectangle title = UIRectangleAdd(bounds, UI_RECT_4(borderSize, -borderSize, 0, 0)); \
+ title.b = title.t + titleSize; \
+ UIRectangle content = UIRectangleAdd(bounds, UI_RECT_4(borderSize, -borderSize, titleSize, -borderSize));
+
+#define UI_UPDATE_HOVERED (1)
+#define UI_UPDATE_PRESSED (2)
+#define UI_UPDATE_FOCUSED (3)
+#define UI_UPDATE_DISABLED (4)
+
+typedef enum UIMessage {
+ // General messages.
+ UI_MSG_PAINT, // dp = pointer to UIPainter
+ UI_MSG_PAINT_FOREGROUND, // after children have painted
+ UI_MSG_LAYOUT,
+ UI_MSG_DESTROY,
+ UI_MSG_DEALLOCATE,
+ UI_MSG_UPDATE, // di = UI_UPDATE_... constant
+ UI_MSG_ANIMATE,
+ UI_MSG_SCROLLED,
+ UI_MSG_GET_WIDTH, // di = height (if known); return width
+ UI_MSG_GET_HEIGHT, // di = width (if known); return height
+ UI_MSG_GET_CHILD_STABILITY, // dp = child element; return stable axes, 1 (width) | 2 (height)
+
+ // Input events.
+ UI_MSG_INPUT_EVENTS_START, // not sent to disabled elements
+ UI_MSG_LEFT_DOWN,
+ UI_MSG_LEFT_UP,
+ UI_MSG_MIDDLE_DOWN,
+ UI_MSG_MIDDLE_UP,
+ UI_MSG_RIGHT_DOWN,
+ UI_MSG_RIGHT_UP,
+ UI_MSG_KEY_TYPED, // dp = pointer to UIKeyTyped; return 1 if handled
+ UI_MSG_KEY_RELEASED, // dp = pointer to UIKeyTyped; return 1 if handled
+ UI_MSG_MOUSE_MOVE,
+ UI_MSG_MOUSE_DRAG,
+ UI_MSG_MOUSE_WHEEL, // di = delta; return 1 if handled
+ UI_MSG_CLICKED,
+ UI_MSG_GET_CURSOR, // return cursor code
+ UI_MSG_PRESSED_DESCENDENT, // dp = pointer to child that is/contains pressed element
+ UI_MSG_INPUT_EVENTS_END,
+
+ // Specific elements.
+ UI_MSG_VALUE_CHANGED, // sent to notify that the element's value has changed
+ UI_MSG_TABLE_GET_ITEM, // dp = pointer to UITableGetItem; return string length
+ UI_MSG_CODE_GET_MARGIN_COLOR, // di = line index (starts at 1); return color
+ UI_MSG_CODE_DECORATE_LINE, // dp = pointer to UICodeDecorateLine
+ UI_MSG_TAB_SELECTED, // sent to the tab that was selected (not the tab pane itself)
+
+ // Windows.
+ UI_MSG_WINDOW_DROP_FILES, // di = count, dp = char ** of paths
+ UI_MSG_WINDOW_ACTIVATE,
+ UI_MSG_WINDOW_CLOSE, // return 1 to prevent default (process exit for UIWindow; close for UIMDIChild)
+ UI_MSG_WINDOW_UPDATE_START,
+ UI_MSG_WINDOW_UPDATE_BEFORE_DESTROY,
+ UI_MSG_WINDOW_UPDATE_BEFORE_LAYOUT,
+ UI_MSG_WINDOW_UPDATE_BEFORE_PAINT,
+ UI_MSG_WINDOW_UPDATE_END,
+
+ // User-defined messages.
+ UI_MSG_USER,
+} UIMessage;
+
+#ifdef UI_ESSENCE
+#define UIRectangle EsRectangle
+#else
+typedef struct UIRectangle {
+ int l, r, t, b;
+} UIRectangle;
+#endif
+
+typedef struct UITheme {
+ uint32_t panel1, panel2, selected, border;
+ uint32_t text, textDisabled, textSelected;
+ uint32_t buttonNormal, buttonHovered, buttonPressed, buttonDisabled;
+ uint32_t textboxNormal, textboxFocused;
+ uint32_t codeFocused, codeBackground, codeDefault, codeComment, codeString, codeNumber, codeOperator, codePreprocessor;
+ uint32_t accent1, accent2;
+} UITheme;
+
+typedef struct UIPainter {
+ UIRectangle clip;
+ uint32_t *bits;
+ int width, height;
+#ifdef UI_DEBUG
+ int fillCount;
+#endif
+} UIPainter;
+
+typedef struct UIFont {
+ int glyphWidth, glyphHeight;
+
+#ifdef UI_FREETYPE
+ bool isFreeType;
+ FT_Face font;
+#ifdef UI_UNICODE
+ FT_Bitmap *glyphs;
+ bool *glyphsRendered;
+ int *glyphOffsetsX, *glyphOffsetsY;
+#else
+ FT_Bitmap glyphs[128];
+ bool glyphsRendered[128];
+ int glyphOffsetsX[128], glyphOffsetsY[128];
+#endif
+#endif
+} UIFont;
+
+typedef struct UIShortcut {
+ intptr_t code;
+ bool ctrl, shift, alt;
+ void (*invoke)(void *cp);
+ void *cp;
+} UIShortcut;
+
+typedef struct UIStringSelection {
+ int carets[2];
+ uint32_t colorText, colorBackground;
+} UIStringSelection;
+
+typedef struct UIKeyTyped {
+ char *text;
+ int textBytes;
+ intptr_t code;
+} UIKeyTyped;
+
+typedef struct UITableGetItem {
+ char *buffer;
+ size_t bufferBytes;
+ int index, column;
+ bool isSelected;
+} UITableGetItem;
+
+typedef struct UICodeDecorateLine {
+ UIRectangle bounds;
+ int index; // Starting at 1!
+ int x, y; // Position where additional text can be drawn.
+ UIPainter *painter;
+} UICodeDecorateLine;
+
+#define UI_RECT_1(x) ((UIRectangle) { (x), (x), (x), (x) })
+#define UI_RECT_1I(x) ((UIRectangle) { (x), -(x), (x), -(x) })
+#define UI_RECT_2(x, y) ((UIRectangle) { (x), (x), (y), (y) })
+#define UI_RECT_2I(x, y) ((UIRectangle) { (x), -(x), (y), -(y) })
+#define UI_RECT_2S(x, y) ((UIRectangle) { 0, (x), 0, (y) })
+#define UI_RECT_4(x, y, z, w) ((UIRectangle) { (x), (y), (z), (w) })
+#define UI_RECT_4PD(x, y, w, h) ((UIRectangle) { (x), ((x) + (w)), (y), ((y) + (h)) })
+#define UI_RECT_WIDTH(_r) ((_r).r - (_r).l)
+#define UI_RECT_HEIGHT(_r) ((_r).b - (_r).t)
+#define UI_RECT_TOTAL_H(_r) ((_r).r + (_r).l)
+#define UI_RECT_TOTAL_V(_r) ((_r).b + (_r).t)
+#define UI_RECT_SIZE(_r) UI_RECT_WIDTH(_r), UI_RECT_HEIGHT(_r)
+#define UI_RECT_TOP_LEFT(_r) (_r).l, (_r).t
+#define UI_RECT_BOTTOM_LEFT(_r) (_r).l, (_r).b
+#define UI_RECT_BOTTOM_RIGHT(_r) (_r).r, (_r).b
+#define UI_RECT_ALL(_r) (_r).l, (_r).r, (_r).t, (_r).b
+#define UI_RECT_VALID(_r) ((_r).l < (_r).r && (_r).t < (_r).b)
+
+#define UI_COLOR_ALPHA_F(x) ((((x) >> 24) & 0xFF) / 255.0f)
+#define UI_COLOR_RED_F(x) ((((x) >> 16) & 0xFF) / 255.0f)
+#define UI_COLOR_GREEN_F(x) ((((x) >> 8) & 0xFF) / 255.0f)
+#define UI_COLOR_BLUE_F(x) ((((x) >> 0) & 0xFF) / 255.0f)
+#define UI_COLOR_ALPHA(x) ((((x) >> 24) & 0xFF))
+#define UI_COLOR_RED(x) ((((x) >> 16) & 0xFF))
+#define UI_COLOR_GREEN(x) ((((x) >> 8) & 0xFF))
+#define UI_COLOR_BLUE(x) ((((x) >> 0) & 0xFF))
+#define UI_COLOR_FROM_FLOAT(r, g, b) (((uint32_t) ((r) * 255.0f) << 16) | ((uint32_t) ((g) * 255.0f) << 8) | ((uint32_t) ((b) * 255.0f) << 0))
+#define UI_COLOR_FROM_RGBA_F(r, g, b, a) (((uint32_t) ((r) * 255.0f) << 16) | ((uint32_t) ((g) * 255.0f) << 8) \
+ | ((uint32_t) ((b) * 255.0f) << 0) | ((uint32_t) ((a) * 255.0f) << 24))
+
+#define UI_SWAP(s, a, b) do { s t = (a); (a) = (b); (b) = t; } while (0)
+
+#ifndef UI_DRAW_CONTROL_CUSTOM
+#define UIDrawControl UIDrawControlDefault
+#endif
+#define UI_DRAW_CONTROL_PUSH_BUTTON (1)
+#define UI_DRAW_CONTROL_DROP_DOWN (2)
+#define UI_DRAW_CONTROL_MENU_ITEM (3)
+#define UI_DRAW_CONTROL_CHECKBOX (4)
+#define UI_DRAW_CONTROL_LABEL (5)
+#define UI_DRAW_CONTROL_SPLITTER (6)
+#define UI_DRAW_CONTROL_SCROLL_TRACK (7)
+#define UI_DRAW_CONTROL_SCROLL_UP (8)
+#define UI_DRAW_CONTROL_SCROLL_DOWN (9)
+#define UI_DRAW_CONTROL_SCROLL_THUMB (10)
+#define UI_DRAW_CONTROL_GAUGE (11)
+#define UI_DRAW_CONTROL_SLIDER (12)
+#define UI_DRAW_CONTROL_TEXTBOX (13)
+#define UI_DRAW_CONTROL_MODAL_POPUP (14)
+#define UI_DRAW_CONTROL_MENU (15)
+#define UI_DRAW_CONTROL_TABLE_ROW (16)
+#define UI_DRAW_CONTROL_TABLE_CELL (17)
+#define UI_DRAW_CONTROL_TABLE_BACKGROUND (18)
+#define UI_DRAW_CONTROL_TABLE_HEADER (19)
+#define UI_DRAW_CONTROL_MDI_CHILD (20)
+#define UI_DRAW_CONTROL_TAB (21)
+#define UI_DRAW_CONTROL_TAB_BAND (22)
+#define UI_DRAW_CONTROL_TYPE_MASK (0xFF)
+#define UI_DRAW_CONTROL_STATE_SELECTED (1 << 24)
+#define UI_DRAW_CONTROL_STATE_VERTICAL (1 << 25)
+#define UI_DRAW_CONTROL_STATE_INDETERMINATE (1 << 26)
+#define UI_DRAW_CONTROL_STATE_CHECKED (1 << 27)
+#define UI_DRAW_CONTROL_STATE_HOVERED (1 << 28)
+#define UI_DRAW_CONTROL_STATE_FOCUSED (1 << 29)
+#define UI_DRAW_CONTROL_STATE_PRESSED (1 << 30)
+#define UI_DRAW_CONTROL_STATE_DISABLED (1 << 31)
+#define UI_DRAW_CONTROL_STATE_FROM_ELEMENT(x) ((((x)->flags & UI_ELEMENT_DISABLED) ? UI_DRAW_CONTROL_STATE_DISABLED : 0) \
+ | (((x)->window->hovered == (x)) ? UI_DRAW_CONTROL_STATE_HOVERED : 0) \
+ | (((x)->window->focused == (x)) ? UI_DRAW_CONTROL_STATE_FOCUSED : 0) \
+ | (((x)->window->pressed == (x)) ? UI_DRAW_CONTROL_STATE_PRESSED : 0))
+
+#define UI_CURSOR_ARROW (0)
+#define UI_CURSOR_TEXT (1)
+#define UI_CURSOR_SPLIT_V (2)
+#define UI_CURSOR_SPLIT_H (3)
+#define UI_CURSOR_FLIPPED_ARROW (4)
+#define UI_CURSOR_CROSS_HAIR (5)
+#define UI_CURSOR_HAND (6)
+#define UI_CURSOR_RESIZE_UP (7)
+#define UI_CURSOR_RESIZE_LEFT (8)
+#define UI_CURSOR_RESIZE_UP_RIGHT (9)
+#define UI_CURSOR_RESIZE_UP_LEFT (10)
+#define UI_CURSOR_RESIZE_DOWN (11)
+#define UI_CURSOR_RESIZE_RIGHT (12)
+#define UI_CURSOR_RESIZE_DOWN_RIGHT (13)
+#define UI_CURSOR_RESIZE_DOWN_LEFT (14)
+#define UI_CURSOR_COUNT (15)
+
+#define UI_ALIGN_LEFT (1)
+#define UI_ALIGN_RIGHT (2)
+#define UI_ALIGN_CENTER (3)
+
+extern const int UI_KEYCODE_A;
+extern const int UI_KEYCODE_BACKSPACE;
+extern const int UI_KEYCODE_DELETE;
+extern const int UI_KEYCODE_DOWN;
+extern const int UI_KEYCODE_END;
+extern const int UI_KEYCODE_ENTER;
+extern const int UI_KEYCODE_ESCAPE;
+extern const int UI_KEYCODE_F1;
+extern const int UI_KEYCODE_HOME;
+extern const int UI_KEYCODE_LEFT;
+extern const int UI_KEYCODE_RIGHT;
+extern const int UI_KEYCODE_SPACE;
+extern const int UI_KEYCODE_TAB;
+extern const int UI_KEYCODE_UP;
+extern const int UI_KEYCODE_INSERT;
+extern const int UI_KEYCODE_0;
+extern const int UI_KEYCODE_BACKTICK;
+extern const int UI_KEYCODE_PAGE_UP;
+extern const int UI_KEYCODE_PAGE_DOWN;
+
+#define UI_KEYCODE_LETTER(x) (UI_KEYCODE_A + (x) - 'A')
+#define UI_KEYCODE_DIGIT(x) (UI_KEYCODE_0 + (x) - '0')
+#define UI_KEYCODE_FKEY(x) (UI_KEYCODE_F1 + (x) - 1)
+
+#define UI_ELEMENT_FILL (UI_ELEMENT_V_FILL | UI_ELEMENT_H_FILL)
+
+typedef struct UIElement {
+#define UI_ELEMENT_V_FILL (1 << 16)
+#define UI_ELEMENT_H_FILL (1 << 17)
+#define UI_ELEMENT_WINDOW (1 << 18)
+#define UI_ELEMENT_PARENT_PUSH (1 << 19)
+#define UI_ELEMENT_TAB_STOP (1 << 20)
+#define UI_ELEMENT_NON_CLIENT (1 << 21) // Don't destroy in UIElementDestroyDescendents, like scroll bars.
+#define UI_ELEMENT_DISABLED (1 << 22) // Don't receive input events.
+#define UI_ELEMENT_BORDER (1 << 23)
+
+#define UI_ELEMENT_HIDE (1 << 27)
+#define UI_ELEMENT_RELAYOUT (1 << 28)
+#define UI_ELEMENT_RELAYOUT_DESCENDENT (1 << 29)
+#define UI_ELEMENT_DESTROY (1 << 30)
+#define UI_ELEMENT_DESTROY_DESCENDENT (1 << 31)
+
+ uint32_t flags; // First 16 bits are element specific.
+ uint32_t id;
+ uint32_t childCount;
+ uint32_t _unused0;
+
+ struct UIElement *parent;
+ struct UIElement **children;
+ struct UIWindow *window;
+
+ UIRectangle bounds, clip;
+
+ void *cp; // Context pointer (for user).
+
+ int (*messageClass)(struct UIElement *element, UIMessage message, int di /* data integer */, void *dp /* data pointer */);
+ int (*messageUser)(struct UIElement *element, UIMessage message, int di, void *dp);
+
+ const char *cClassName;
+} UIElement;
+
+#define UI_SHORTCUT(code, ctrl, shift, alt, invoke, cp) ((UIShortcut) { (code), (ctrl), (shift), (alt), (invoke), (cp) })
+
+typedef struct UIWindow {
+#define UI_WINDOW_MENU (1 << 0)
+#define UI_WINDOW_INSPECTOR (1 << 1)
+#define UI_WINDOW_CENTER_IN_OWNER (1 << 2)
+#define UI_WINDOW_MAXIMIZE (1 << 3)
+
+ UIElement e;
+
+ UIElement *dialog;
+
+ UIShortcut *shortcuts;
+ size_t shortcutCount, shortcutAllocated;
+
+ float scale;
+
+ uint32_t *bits;
+ int width, height;
+ struct UIWindow *next;
+
+ UIElement *hovered, *pressed, *focused, *dialogOldFocus;
+ int pressedButton;
+
+ int cursorX, cursorY;
+ int cursorStyle;
+
+ // Set when a textbox is modified.
+ // Useful for tracking whether changes to the loaded document have been saved.
+ bool textboxModifiedFlag;
+
+ bool ctrl, shift, alt;
+
+ UIRectangle updateRegion;
+
+#ifdef UI_DEBUG
+ float lastFullFillCount;
+#endif
+
+#ifdef UI_LINUX
+ Window window;
+ XImage *image;
+ XIC xic;
+ unsigned ctrlCode, shiftCode, altCode;
+ Window dragSource;
+#endif
+
+#ifdef UI_WINDOWS
+ HWND hwnd;
+ bool trackingLeave;
+#endif
+
+#ifdef UI_ESSENCE
+ EsWindow *window;
+ EsElement *canvas;
+ int cursor;
+#endif
+
+#ifdef UI_COCOA
+ NSWindow *window;
+ void *view;
+#endif
+} UIWindow;
+
+typedef struct UIPanel {
+#define UI_PANEL_HORIZONTAL (1 << 0)
+#define UI_PANEL_COLOR_1 (1 << 2)
+#define UI_PANEL_COLOR_2 (1 << 3)
+#define UI_PANEL_SMALL_SPACING (1 << 5)
+#define UI_PANEL_MEDIUM_SPACING (1 << 6)
+#define UI_PANEL_LARGE_SPACING (1 << 7)
+#define UI_PANEL_SCROLL (1 << 8)
+#define UI_PANEL_EXPAND (1 << 9)
+ UIElement e;
+ struct UIScrollBar *scrollBar;
+ UIRectangle border;
+ int gap;
+} UIPanel;
+
+typedef struct UIButton {
+#define UI_BUTTON_SMALL (1 << 0)
+#define UI_BUTTON_MENU_ITEM (1 << 1)
+#define UI_BUTTON_CAN_FOCUS (1 << 2)
+#define UI_BUTTON_DROP_DOWN (1 << 3)
+#define UI_BUTTON_CHECKED (1 << 15)
+ UIElement e;
+ char *label;
+ ptrdiff_t labelBytes;
+ void (*invoke)(void *cp);
+} UIButton;
+
+typedef struct UICheckbox {
+#define UI_CHECKBOX_ALLOW_INDETERMINATE (1 << 0)
+ UIElement e;
+#define UI_CHECK_UNCHECKED (0)
+#define UI_CHECK_CHECKED (1)
+#define UI_CHECK_INDETERMINATE (2)
+ uint8_t check;
+ char *label;
+ ptrdiff_t labelBytes;
+ void (*invoke)(void *cp);
+} UICheckbox;
+
+typedef struct UILabel {
+ UIElement e;
+ char *label;
+ ptrdiff_t labelBytes;
+} UILabel;
+
+typedef struct UISpacer {
+ UIElement e;
+ int width, height;
+} UISpacer;
+
+typedef struct UISplitPane {
+#define UI_SPLIT_PANE_VERTICAL (1 << 0)
+ UIElement e;
+ float weight;
+} UISplitPane;
+
+typedef struct UITabPane {
+ UIElement e;
+ char *tabs;
+ uint32_t active;
+} UITabPane;
+
+typedef struct UIScrollBar {
+#define UI_SCROLL_BAR_HORIZONTAL (1 << 0)
+ UIElement e;
+ int64_t maximum, page;
+ int64_t dragOffset;
+ double position;
+ UI_CLOCK_T lastAnimateTime;
+ bool inDrag, horizontal;
+} UIScrollBar;
+
+#define _UI_LAYOUT_SCROLL_BAR_PAIR(element) \
+ element->vScroll->page = vSpace - (element->hScroll->page < element->hScroll->maximum ? scrollBarSize : 0); \
+ element->hScroll->page = hSpace - (element->vScroll->page < element->vScroll->maximum ? scrollBarSize : 0); \
+ element->vScroll->page = vSpace - (element->hScroll->page < element->hScroll->maximum ? scrollBarSize : 0); \
+ UIRectangle vScrollBarBounds = element->e.bounds, hScrollBarBounds = element->e.bounds; \
+ hScrollBarBounds.r = vScrollBarBounds.l = vScrollBarBounds.r - (element->vScroll->page < element->vScroll->maximum ? scrollBarSize : 0); \
+ vScrollBarBounds.b = hScrollBarBounds.t = hScrollBarBounds.b - (element->hScroll->page < element->hScroll->maximum ? scrollBarSize : 0); \
+ UIElementMove(&element->vScroll->e, vScrollBarBounds, true); \
+ UIElementMove(&element->hScroll->e, hScrollBarBounds, true);
+#define _UI_KEY_INPUT_VSCROLL(element, rowHeight, pageHeight) \
+ if (m->code == UI_KEYCODE_UP) element->vScroll->position -= (rowHeight); \
+ else if (m->code == UI_KEYCODE_DOWN) element->vScroll->position += (rowHeight); \
+ else if (m->code == UI_KEYCODE_PAGE_UP) element->vScroll->position += (pageHeight); \
+ else if (m->code == UI_KEYCODE_PAGE_DOWN) element->vScroll->position -= (pageHeight); \
+ else if (m->code == UI_KEYCODE_HOME) element->vScroll->position = 0; \
+ else if (m->code == UI_KEYCODE_END) element->vScroll->position = element->vScroll->maximum; \
+ UIElementRefresh(&element->e);
+
+typedef struct UICodeLine {
+ int offset, bytes;
+} UICodeLine;
+
+typedef struct UICode {
+#define UI_CODE_NO_MARGIN (1 << 0)
+#define UI_CODE_SELECTABLE (1 << 1)
+ UIElement e;
+ UIScrollBar *vScroll, *hScroll;
+ UICodeLine *lines;
+ UIFont *font;
+ int lineCount, focused;
+ bool moveScrollToFocusNextLayout;
+ bool leftDownInMargin;
+ char *content;
+ size_t contentBytes;
+ int tabSize;
+ int columns;
+ UI_CLOCK_T lastAnimateTime;
+ struct { int line, offset; } selection[4 /* start, end, anchor, caret */];
+ int verticalMotionColumn;
+ bool useVerticalMotionColumn;
+ bool moveScrollToCaretNextLayout;
+ bool centerExecutionPointer;
+} UICode;
+
+typedef struct UIGauge {
+ UIElement e;
+ double position;
+} UIGauge;
+
+typedef struct UITable {
+ UIElement e;
+ UIScrollBar *vScroll, *hScroll;
+ int itemCount;
+ char *columns;
+ int *columnWidths, columnCount, columnHighlight;
+} UITable;
+
+typedef struct UITextbox {
+ UIElement e;
+ char *string;
+ ptrdiff_t bytes;
+ int carets[2];
+ int scroll;
+ bool rejectNextKey;
+} UITextbox;
+
+#define UI_MENU_PLACE_ABOVE (1 << 0)
+#define UI_MENU_NO_SCROLL (1 << 1)
+#if defined(UI_COCOA)
+typedef NSMenu UIMenu;
+#elif defined(UI_ESSENCE)
+typedef EsMenu UIMenu;
+#else
+typedef struct UIMenu {
+ UIElement e;
+ int pointX, pointY;
+ UIScrollBar *vScroll;
+ UIWindow *parentWindow;
+} UIMenu;
+#endif
+
+typedef struct UISlider {
+ UIElement e;
+ double position;
+ int steps;
+} UISlider;
+
+typedef struct UIMDIClient {
+#define UI_MDI_CLIENT_TRANSPARENT (1 << 0)
+ UIElement e;
+ struct UIMDIChild *active;
+ int cascade;
+} UIMDIClient;
+
+typedef struct UIMDIChild {
+#define UI_MDI_CHILD_CLOSE_BUTTON (1 << 0)
+ UIElement e;
+ UIRectangle bounds;
+ char *title;
+ ptrdiff_t titleBytes;
+ int dragHitTest;
+ UIRectangle dragOffset;
+} UIMDIChild;
+
+typedef struct UIExpandPane {
+ UIElement e;
+ UIButton *button;
+ UIPanel *panel;
+ bool expanded;
+} UIExpandPane;
+
+typedef struct UIImageDisplay {
+#define UI_IMAGE_DISPLAY_INTERACTIVE (1 << 0)
+#define _UI_IMAGE_DISPLAY_ZOOM_FIT (1 << 1)
+
+ UIElement e;
+ uint32_t *bits;
+ int width, height;
+ float panX, panY, zoom;
+
+ // Internals:
+ int previousWidth, previousHeight;
+ int previousPanPointX, previousPanPointY;
+} UIImageDisplay;
+
+typedef struct UIWrapPanel {
+ UIElement e;
+} UIWrapPanel;
+
+typedef struct UISwitcher {
+ UIElement e;
+ UIElement *active;
+} UISwitcher;
+
+void UIInitialise();
+int UIMessageLoop();
+
+UIElement *UIElementCreate(size_t bytes, UIElement *parent, uint32_t flags,
+ int (*messageClass)(UIElement *, UIMessage, int, void *), const char *cClassName);
+
+UICheckbox *UICheckboxCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes);
+UIExpandPane *UIExpandPaneCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes, uint32_t panelFlags);
+UIMDIClient *UIMDIClientCreate(UIElement *parent, uint32_t flags);
+UIMDIChild *UIMDIChildCreate(UIElement *parent, uint32_t flags, UIRectangle initialBounds, const char *title, ptrdiff_t titleBytes);
+UIPanel *UIPanelCreate(UIElement *parent, uint32_t flags);
+UIScrollBar *UIScrollBarCreate(UIElement *parent, uint32_t flags);
+UISlider *UISliderCreate(UIElement *parent, uint32_t flags);
+UISpacer *UISpacerCreate(UIElement *parent, uint32_t flags, int width, int height);
+UISplitPane *UISplitPaneCreate(UIElement *parent, uint32_t flags, float weight);
+UITabPane *UITabPaneCreate(UIElement *parent, uint32_t flags, const char *tabs /* separate with \t, terminate with \0 */);
+UIWrapPanel *UIWrapPanelCreate(UIElement *parent, uint32_t flags);
+
+UIGauge *UIGaugeCreate(UIElement *parent, uint32_t flags);
+void UIGaugeSetPosition(UIGauge *gauge, float value);
+
+UIButton *UIButtonCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes);
+void UIButtonSetLabel(UIButton *button, const char *string, ptrdiff_t stringBytes);
+UILabel *UILabelCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes);
+void UILabelSetContent(UILabel *code, const char *content, ptrdiff_t byteCount);
+
+UIImageDisplay *UIImageDisplayCreate(UIElement *parent, uint32_t flags, uint32_t *bits, size_t width, size_t height, size_t stride);
+void UIImageDisplaySetContent(UIImageDisplay *display, uint32_t *bits, size_t width, size_t height, size_t stride);
+
+UISwitcher *UISwitcherCreate(UIElement *parent, uint32_t flags);
+void UISwitcherSwitchTo(UISwitcher *switcher, UIElement *child);
+
+UIWindow *UIWindowCreate(UIWindow *owner, uint32_t flags, const char *cTitle, int width, int height);
+void UIWindowRegisterShortcut(UIWindow *window, UIShortcut shortcut);
+void UIWindowPostMessage(UIWindow *window, UIMessage message, void *dp); // Thread-safe.
+void UIWindowPack(UIWindow *window, int width); // Change the size of the window to best match its contents.
+
+typedef void (*UIDialogUserCallback)(UIElement *);
+const char *UIDialogShow(UIWindow *window, uint32_t flags, const char *format, ...);
+
+UIMenu *UIMenuCreate(UIElement *parent, uint32_t flags);
+void UIMenuAddItem(UIMenu *menu, uint32_t flags, const char *label, ptrdiff_t labelBytes, void (*invoke)(void *cp), void *cp);
+void UIMenuShow(UIMenu *menu);
+bool UIMenusOpen();
+
+UITextbox *UITextboxCreate(UIElement *parent, uint32_t flags);
+void UITextboxReplace(UITextbox *textbox, const char *text, ptrdiff_t bytes, bool sendChangedMessage);
+void UITextboxClear(UITextbox *textbox, bool sendChangedMessage);
+void UITextboxMoveCaret(UITextbox *textbox, bool backward, bool word);
+char *UITextboxToCString(UITextbox *textbox); // Free with UI_FREE.
+
+UITable *UITableCreate(UIElement *parent, uint32_t flags, const char *columns /* separate with \t, terminate with \0 */);
+int UITableHitTest(UITable *table, int x, int y); // Returns item index. Returns -1 if not on an item.
+int UITableHeaderHitTest(UITable *table, int x, int y); // Returns column index or -1.
+bool UITableEnsureVisible(UITable *table, int index); // Returns false if the item was already visible.
+void UITableResizeColumns(UITable *table);
+
+UICode *UICodeCreate(UIElement *parent, uint32_t flags);
+void UICodeFocusLine(UICode *code, int index); // Line numbers are 1-indexed!!
+int UICodeHitTest(UICode *code, int x, int y); // Returns line number; negates if in margin. Returns 0 if not on a line.
+void UICodePositionToByte(UICode *code, int x, int y, int *line, int *byte);
+void UICodeInsertContent(UICode *code, const char *content, ptrdiff_t byteCount, bool replace);
+void UICodeMoveCaret(UICode *code, bool backward, bool word);
+
+void UIDrawBlock(UIPainter *painter, UIRectangle rectangle, uint32_t color);
+void UIDrawCircle(UIPainter *painter, int centerX, int centerY, int radius, uint32_t fillColor, uint32_t outlineColor, bool hollow);
+void UIDrawControl(UIPainter *painter, UIRectangle bounds, uint32_t mode /* UI_DRAW_CONTROL_* */, const char *label, ptrdiff_t labelBytes, double position, float scale);
+void UIDrawControlDefault(UIPainter *painter, UIRectangle bounds, uint32_t mode, const char *label, ptrdiff_t labelBytes, double position, float scale);
+void UIDrawInvert(UIPainter *painter, UIRectangle rectangle);
+bool UIDrawLine(UIPainter *painter, int x0, int y0, int x1, int y1, uint32_t color); // Returns false if the line was not visible.
+void UIDrawTriangle(UIPainter *painter, int x0, int y0, int x1, int y1, int x2, int y2, uint32_t color);
+void UIDrawTriangleOutline(UIPainter *painter, int x0, int y0, int x1, int y1, int x2, int y2, uint32_t color);
+void UIDrawGlyph(UIPainter *painter, int x, int y, int c, uint32_t color);
+void UIDrawRectangle(UIPainter *painter, UIRectangle r, uint32_t mainColor, uint32_t borderColor, UIRectangle borderSize);
+void UIDrawBorder(UIPainter *painter, UIRectangle r, uint32_t borderColor, UIRectangle borderSize);
+void UIDrawString(UIPainter *painter, UIRectangle r, const char *string, ptrdiff_t bytes, uint32_t color, int align, UIStringSelection *selection);
+int UIDrawStringHighlighted(UIPainter *painter, UIRectangle r, const char *string, ptrdiff_t bytes, int tabSize, UIStringSelection *selection); // Returns final x position.
+
+int UIMeasureStringWidth(const char *string, ptrdiff_t bytes);
+int UIMeasureStringHeight();
+
+uint64_t UIAnimateClock(); // In ms.
+
+bool UIElementAnimate(UIElement *element, bool stop);
+void UIElementDestroy(UIElement *element);
+void UIElementDestroyDescendents(UIElement *element);
+UIElement *UIElementFindByPoint(UIElement *element, int x, int y);
+void UIElementFocus(UIElement *element);
+UIRectangle UIElementScreenBounds(UIElement *element); // Returns bounds of element in same coordinate system as used by UIWindowCreate.
+void UIElementRefresh(UIElement *element);
+void UIElementRelayout(UIElement *element);
+void UIElementRepaint(UIElement *element, UIRectangle *region);
+void UIElementMeasurementsChanged(UIElement *element, int which);
+void UIElementMove(UIElement *element, UIRectangle bounds, bool alwaysLayout);
+int UIElementMessage(UIElement *element, UIMessage message, int di, void *dp);
+UIElement *UIElementChangeParent(UIElement *element, UIElement *newParent, UIElement *insertBefore); // Set insertBefore to null to insert at the end. Returns the element it was before in its previous parent, or NULL.
+
+UIElement *UIParentPush(UIElement *element);
+UIElement *UIParentPop();
+
+UIRectangle UIRectangleIntersection(UIRectangle a, UIRectangle b);
+UIRectangle UIRectangleBounding(UIRectangle a, UIRectangle b);
+UIRectangle UIRectangleAdd(UIRectangle a, UIRectangle b);
+UIRectangle UIRectangleTranslate(UIRectangle a, UIRectangle b);
+UIRectangle UIRectangleCenter(UIRectangle parent, UIRectangle child);
+UIRectangle UIRectangleFit(UIRectangle parent, UIRectangle child, bool allowScalingUp);
+bool UIRectangleEquals(UIRectangle a, UIRectangle b);
+bool UIRectangleContains(UIRectangle a, int x, int y);
+
+bool UIColorToHSV(uint32_t rgb, float *hue, float *saturation, float *value);
+void UIColorToRGB(float hue, float saturation, float value, uint32_t *rgb);
+
+char *UIStringCopy(const char *in, ptrdiff_t inBytes);
+
+UIFont *UIFontCreate(const char *cPath, uint32_t size);
+UIFont *UIFontActivate(UIFont *font); // Returns the previously active font.
+
+#ifdef UI_DEBUG
+void UIInspectorLog(const char *cFormat, ...);
+#endif
+
+static ptrdiff_t _UIStringLength(const char *cString) {
+ if (!cString) return 0;
+ ptrdiff_t length;
+ for (length = 0; cString[length]; length++);
+ return length;
+}
+
+#ifdef UI_UNICODE
+
+#ifndef UI_FREETYPE
+#error "Unicode support requires Freetype"
+#endif
+
+#define _UNICODE_MAX_CODEPOINT 0x10FFFF
+
+int Utf8GetCodePoint(const char *cString, ptrdiff_t bytesLength, ptrdiff_t *bytesConsumed) {
+ UI_ASSERT(bytesLength > 0 && "Attempted to get UTF-8 code point from an empty string");
+
+ if (bytesConsumed == NULL) {
+ ptrdiff_t bytesConsumed;
+ return Utf8GetCodePoint(cString, bytesLength, &bytesConsumed);
+ }
+
+ ptrdiff_t numExtraBytes;
+ uint8_t first = cString[0];
+
+ *bytesConsumed = 1;
+ if ((first & 0xF0) == 0xF0) {
+ numExtraBytes = 3;
+ } else if ((first & 0xE0) == 0xE0) {
+ numExtraBytes = 2;
+ } else if ((first & 0xC0) == 0xC0) {
+ numExtraBytes = 1;
+ } else if (first & 0x7F) {
+ return first & 0x80 ? -1 : first;
+ } else {
+ return -1;
+ }
+
+ if (bytesLength < numExtraBytes + 1) {
+ return -1;
+ }
+
+ int codePoint = ((int)first & (0x3F >> numExtraBytes)) << (6 * numExtraBytes);
+ for (ptrdiff_t idx = 1; idx < numExtraBytes + 1; idx++) {
+ char byte = cString[idx];
+ if ((byte & 0xC0) != 0x80) {
+ return -1;
+ }
+
+ codePoint |= (byte & 0x3F) << (6 * (numExtraBytes - idx));
+ (*bytesConsumed)++;
+ }
+
+ return codePoint > _UNICODE_MAX_CODEPOINT ? -1 : codePoint;
+}
+
+char * Utf8GetPreviousChar(char *string, char *offset) {
+ if (string == offset) {
+ return string;
+ }
+
+ char *prev = offset - 1;
+ while (prev > string) {
+ if ((*prev & 0xC0) == 0x80) prev--;
+ else break;
+ }
+
+ return prev;
+}
+
+ptrdiff_t Utf8GetCharBytes(const char *cString, ptrdiff_t bytes) {
+ if (!cString) {
+ return 0;
+ }
+ if (bytes == -1) {
+ bytes = _UIStringLength(cString);
+ }
+
+ ptrdiff_t bytesConsumed;
+ Utf8GetCodePoint(cString, bytes, &bytesConsumed);
+ return bytesConsumed;
+}
+
+ptrdiff_t Utf8StringLength(const char *cString, ptrdiff_t bytes) {
+ if (!cString) {
+ return 0;
+ }
+ if (bytes == -1) {
+ bytes = _UIStringLength(cString);
+ }
+
+ ptrdiff_t length = 0;
+ ptrdiff_t byteIndex = 0;
+ while (byteIndex < bytes) {
+ ptrdiff_t bytesConsumed;
+ Utf8GetCodePoint(cString+ byteIndex, bytes - byteIndex, &bytesConsumed);
+ byteIndex += bytesConsumed;
+ length++;
+
+ UI_ASSERT(byteIndex <= bytes && "Overran the end of the string while counting the number of UTF-8 code points");
+ }
+
+ return length;
+}
+
+#define _UI_ADVANCE_CHAR(index, text, count) \
+ index += Utf8GetCharBytes(text, count - index)
+
+#define _UI_SKIP_TAB(ti, text, bytesLeft, tabSize) do { \
+ int c = Utf8GetCodePoint(text, bytesLeft, NULL); \
+ if (c == '\t') while (ti % tabSize) ti++; \
+} while (0)
+
+#define _UI_MOVE_CARET_BACKWARD(caret, text, offset, offset2) do { \
+ char *prev = Utf8GetPreviousChar(text, text + offset); \
+ caret = prev - text - offset2; \
+} while (0)
+
+#define _UI_MOVE_CARET_FORWARD(caret, text, bytes, offset) do { \
+ caret += Utf8GetCharBytes(text + caret, bytes - offset); \
+} while (0)
+
+#define _UI_MOVE_CARET_BY_WORD(text, bytes, offset) { \
+ char *prev = Utf8GetPreviousChar(text, text + offset); \
+ int c1 = Utf8GetCodePoint(prev, bytes - (prev - text), NULL); \
+ int c2 = Utf8GetCodePoint(text + offset, bytes - offset, NULL); \
+ if (_UICharIsAlphaOrDigitOrUnderscore(c1) != _UICharIsAlphaOrDigitOrUnderscore(c2)) break; \
+}
+
+#else
+
+#define _UI_ADVANCE_CHAR(index, code, count) index++
+
+#define _UI_SKIP_TAB(ti, text, bytesLeft, tabSize) \
+ if (*(text) == '\t') while (ti % tabSize) ti++
+
+#define _UI_MOVE_CARET_BACKWARD(caret, text, offset, offset2) caret--
+#define _UI_MOVE_CARET_FORWARD(caret, text, bytes, offset) caret++
+
+#define _UI_MOVE_CARET_BY_WORD(text, bytes, offset) { \
+ char c1 = (text)[offset - 1]; \
+ char c2 = (text)[offset]; \
+ if (_UICharIsAlphaOrDigitOrUnderscore(c1) != _UICharIsAlphaOrDigitOrUnderscore(c2)) break; \
+}
+
+#endif // UI_UNICODE
+
+#ifdef UI_IMPLEMENTATION
+
+/////////////////////////////////////////
+// Global variables.
+/////////////////////////////////////////
+
+struct {
+ UIWindow *windows;
+ UITheme theme;
+
+ UIElement **animating;
+ uint32_t animatingCount;
+
+ UIElement *parentStack[16];
+ int parentStackCount;
+
+ bool quit;
+ const char *dialogResult;
+ UIElement *dialogOldFocus;
+ bool dialogCanExit;
+
+ UIFont *activeFont;
+
+#ifdef UI_DEBUG
+ UIWindow *inspector;
+ UITable *inspectorTable;
+ UIWindow *inspectorTarget;
+ UICode *inspectorLog;
+#endif
+
+#ifdef UI_LINUX
+ Display *display;
+ Visual *visual;
+ XIM xim;
+ Atom windowClosedID, primaryID, uriListID, plainTextID;
+ Atom dndEnterID, dndPositionID, dndStatusID, dndActionCopyID, dndDropID, dndSelectionID, dndFinishedID, dndAwareID;
+ Atom clipboardID, xSelectionDataID, textID, targetID, incrID;
+ Cursor cursors[UI_CURSOR_COUNT];
+ char *pasteText;
+ XEvent copyEvent;
+#endif
+
+#ifdef UI_WINDOWS
+ HCURSOR cursors[UI_CURSOR_COUNT];
+ HANDLE heap;
+ bool assertionFailure;
+#endif
+
+#ifdef UI_ESSENCE
+ EsInstance *instance;
+#endif
+
+#if defined(UI_ESSENCE) || defined(UI_COCOA)
+ void *menuData[256]; // HACK This limits the number of menu items to 128.
+ uintptr_t menuIndex;
+#endif
+
+#ifdef UI_COCOA
+ int menuX, menuY;
+ UIWindow *menuWindow;
+#endif
+
+#ifdef UI_FREETYPE
+ FT_Library ft;
+#endif
+} ui;
+
+/////////////////////////////////////////
+// Themes.
+/////////////////////////////////////////
+
+UITheme uiThemeClassic = {
+ .panel1 = 0xFFF0F0F0,
+ .panel2 = 0xFFFFFFFF,
+ .selected = 0xFF94BEFE,
+ .border = 0xFF404040,
+
+ .text = 0xFF000000,
+ .textDisabled = 0xFF404040,
+ .textSelected = 0xFF000000,
+
+ .buttonNormal = 0xFFE0E0E0,
+ .buttonHovered = 0xFFF0F0F0,
+ .buttonPressed = 0xFFA0A0A0,
+ .buttonDisabled = 0xFFF0F0F0,
+
+ .textboxNormal = 0xFFF8F8F8,
+ .textboxFocused = 0xFFFFFFFF,
+
+ .codeFocused = 0xFFE0E0E0,
+ .codeBackground = 0xFFFFFFFF,
+ .codeDefault = 0xFF000000,
+ .codeComment = 0xFFA11F20,
+ .codeString = 0xFF037E01,
+ .codeNumber = 0xFF213EF1,
+ .codeOperator = 0xFF7F0480,
+ .codePreprocessor = 0xFF545D70,
+
+ .accent1 = 0xFF0000,
+ .accent2 = 0x00FF00,
+};
+
+UITheme uiThemeDark = {
+ .panel1 = 0xFF252B31,
+ .panel2 = 0xFF14181E,
+ .selected = 0xFF94BEFE,
+ .border = 0xFF000000,
+
+ .text = 0xFFFFFFFF,
+ .textDisabled = 0xFF787D81,
+ .textSelected = 0xFF000000,
+
+ .buttonNormal = 0xFF383D41,
+ .buttonHovered = 0xFF4B5874,
+ .buttonPressed = 0xFF0D0D0F,
+ .buttonDisabled = 0xFF1B1F23,
+
+ .textboxNormal = 0xFF31353C,
+ .textboxFocused = 0xFF4D4D59,
+
+ .codeFocused = 0xFF505055,
+ .codeBackground = 0xFF212126,
+ .codeDefault = 0xFFFFFFFF,
+ .codeComment = 0xFFB4B4B4,
+ .codeString = 0xFFF5DDD1,
+ .codeNumber = 0xFFC3F5D3,
+ .codeOperator = 0xFFF5D499,
+ .codePreprocessor = 0xFFF5F3D1,
+
+ .accent1 = 0xF01231,
+ .accent2 = 0x45F94E,
+};
+
+/////////////////////////////////////////
+// Forward declarations.
+/////////////////////////////////////////
+
+void _UIWindowEndPaint(UIWindow *window, UIPainter *painter);
+void _UIWindowSetCursor(UIWindow *window, int cursor);
+void _UIWindowGetScreenPosition(UIWindow *window, int *x, int *y);
+void _UIWindowSetPressed(UIWindow *window, UIElement *element, int button);
+void _UIClipboardWriteText(UIWindow *window, char *text);
+char *_UIClipboardReadTextStart(UIWindow *window, size_t *bytes);
+void _UIClipboardReadTextEnd(UIWindow *window, char *text);
+bool _UIMessageLoopSingle(int *result);
+void _UIInspectorRefresh();
+void _UIUpdate();
+
+#if defined(UI_LINUX) || defined(UI_COCOA)
+UI_CLOCK_T _UIClock() {
+ struct timespec spec;
+ clock_gettime(CLOCK_REALTIME, &spec);
+ return spec.tv_sec * 1000 + spec.tv_nsec / 1000000;
+}
+#endif
+
+#ifdef UI_WINDOWS
+void *_UIHeapReAlloc(void *pointer, size_t size);
+void *_UIMemmove(void *dest, const void *src, size_t n);
+#endif
+
+/////////////////////////////////////////
+// Helper functions.
+/////////////////////////////////////////
+
+UIRectangle UIRectangleIntersection(UIRectangle a, UIRectangle b) {
+ if (a.l < b.l) a.l = b.l;
+ if (a.t < b.t) a.t = b.t;
+ if (a.r > b.r) a.r = b.r;
+ if (a.b > b.b) a.b = b.b;
+ return a;
+}
+
+UIRectangle UIRectangleBounding(UIRectangle a, UIRectangle b) {
+ if (a.l > b.l) a.l = b.l;
+ if (a.t > b.t) a.t = b.t;
+ if (a.r < b.r) a.r = b.r;
+ if (a.b < b.b) a.b = b.b;
+ return a;
+}
+
+UIRectangle UIRectangleAdd(UIRectangle a, UIRectangle b) {
+ a.l += b.l;
+ a.t += b.t;
+ a.r += b.r;
+ a.b += b.b;
+ return a;
+}
+
+UIRectangle UIRectangleTranslate(UIRectangle a, UIRectangle b) {
+ a.l += b.l;
+ a.t += b.t;
+ a.r += b.l;
+ a.b += b.t;
+ return a;
+}
+
+UIRectangle UIRectangleCenter(UIRectangle parent, UIRectangle child) {
+ int childWidth = UI_RECT_WIDTH(child), childHeight = UI_RECT_HEIGHT(child);
+ int parentWidth = UI_RECT_WIDTH(parent), parentHeight = UI_RECT_HEIGHT(parent);
+ child.l = parentWidth / 2 - childWidth / 2 + parent.l, child.r = child.l + childWidth;
+ child.t = parentHeight / 2 - childHeight / 2 + parent.t, child.b = child.t + childHeight;
+ return child;
+}
+
+UIRectangle UIRectangleFit(UIRectangle parent, UIRectangle child, bool allowScalingUp) {
+ int childWidth = UI_RECT_WIDTH(child), childHeight = UI_RECT_HEIGHT(child);
+ int parentWidth = UI_RECT_WIDTH(parent), parentHeight = UI_RECT_HEIGHT(parent);
+
+ if (childWidth < parentWidth && childHeight < parentHeight && !allowScalingUp) {
+ return UIRectangleCenter(parent, child);
+ }
+
+ float childAspectRatio = (float) childWidth / childHeight;
+ int childMaximumWidth = parentHeight * childAspectRatio;
+ int childMaximumHeight = parentWidth / childAspectRatio;
+
+ if (childMaximumWidth > parentWidth) {
+ return UIRectangleCenter(parent, UI_RECT_2S(parentWidth, childMaximumHeight));
+ } else {
+ return UIRectangleCenter(parent, UI_RECT_2S(childMaximumWidth, parentHeight));
+ }
+}
+
+bool UIRectangleEquals(UIRectangle a, UIRectangle b) {
+ return a.l == b.l && a.r == b.r && a.t == b.t && a.b == b.b;
+}
+
+bool UIRectangleContains(UIRectangle a, int x, int y) {
+ return a.l <= x && a.r > x && a.t <= y && a.b > y;
+}
+
+typedef union _UIConvertFloatInteger {
+ float f;
+ uint32_t i;
+} _UIConvertFloatInteger;
+
+float _UIFloorFloat(float x) {
+ _UIConvertFloatInteger convert = {x};
+ uint32_t sign = convert.i & 0x80000000;
+ int exponent = (int) ((convert.i >> 23) & 0xFF) - 0x7F;
+
+ if (exponent >= 23) {
+ // There aren't any bits representing a fractional part.
+ } else if (exponent >= 0) {
+ // Positive exponent.
+ uint32_t mask = 0x7FFFFF >> exponent;
+ if (!(mask & convert.i)) return x; // Already an integer.
+ if (sign) convert.i += mask;
+ convert.i &= ~mask; // Mask out the fractional bits.
+ } else if (exponent < 0) {
+ // Negative exponent.
+ return sign ? -1.0 : 0.0;
+ }
+
+ return convert.f;
+}
+
+float _UILinearMap(float value, float inFrom, float inTo, float outFrom, float outTo) {
+ float inRange = inTo - inFrom, outRange = outTo - outFrom;
+ float normalisedValue = (value - inFrom) / inRange;
+ return normalisedValue * outRange + outFrom;
+}
+
+bool UIColorToHSV(uint32_t rgb, float *hue, float *saturation, float *value) {
+ float r = UI_COLOR_RED_F(rgb);
+ float g = UI_COLOR_GREEN_F(rgb);
+ float b = UI_COLOR_BLUE_F(rgb);
+
+ float maximum = (r > g && r > b) ? r : (g > b ? g : b),
+ minimum = (r < g && r < b) ? r : (g < b ? g : b),
+ difference = maximum - minimum;
+ *value = maximum;
+
+ if (!difference) {
+ *saturation = 0;
+ return false;
+ } else {
+ if (r == maximum) *hue = (g - b) / difference + 0;
+ if (g == maximum) *hue = (b - r) / difference + 2;
+ if (b == maximum) *hue = (r - g) / difference + 4;
+ if (*hue < 0) *hue += 6;
+ *saturation = difference / maximum;
+ return true;
+ }
+}
+
+void UIColorToRGB(float h, float s, float v, uint32_t *rgb) {
+ float r, g, b;
+
+ if (!s) {
+ r = g = b = v;
+ } else {
+ int h0 = ((int) h) % 6;
+ float f = h - _UIFloorFloat(h);
+ float x = v * (1 - s), y = v * (1 - s * f), z = v * (1 - s * (1 - f));
+
+ switch (h0) {
+ case 0: r = v, g = z, b = x; break;
+ case 1: r = y, g = v, b = x; break;
+ case 2: r = x, g = v, b = z; break;
+ case 3: r = x, g = y, b = v; break;
+ case 4: r = z, g = x, b = v; break;
+ default: r = v, g = x, b = y; break;
+ }
+ }
+
+ *rgb = UI_COLOR_FROM_FLOAT(r, g, b);
+}
+
+char *UIStringCopy(const char *in, ptrdiff_t inBytes) {
+ if (inBytes == -1) {
+ inBytes = _UIStringLength(in);
+ }
+
+ char *buffer = (char *) UI_MALLOC(inBytes + 1);
+
+ for (intptr_t i = 0; i < inBytes; i++) {
+ buffer[i] = in[i];
+ }
+
+ buffer[inBytes] = 0;
+ return buffer;
+}
+
+int _UIByteToColumn(const char *string, int byte, int bytes, int tabSize) {
+ int ti = 0, i = 0;
+
+ while (i < byte && i < bytes) {
+ ti++;
+ _UI_SKIP_TAB(ti, string + i, bytes - i, tabSize);
+ _UI_ADVANCE_CHAR(i, string + i, byte);
+ }
+
+ return ti;
+}
+
+int _UIColumnToByte(const char *string, int column, int bytes, int tabSize) {
+ int byte = 0, ti = 0;
+
+ while (byte < bytes) {
+ ti++;
+ _UI_SKIP_TAB(ti, string + byte, bytes - byte, tabSize);
+ if (column < ti) break;
+
+ _UI_ADVANCE_CHAR(byte, string + byte, bytes);
+ }
+
+ return byte;
+}
+
+/////////////////////////////////////////
+// Animations.
+/////////////////////////////////////////
+
+bool UIElementAnimate(UIElement *element, bool stop) {
+ if (stop) {
+ for (uint32_t i = 0; i < ui.animatingCount; i++) {
+ if (ui.animating[i] == element) {
+ ui.animating[i] = ui.animating[ui.animatingCount - 1];
+ ui.animatingCount--;
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ for (uint32_t i = 0; i < ui.animatingCount; i++) {
+ if (ui.animating[i] == element) {
+ return true;
+ }
+ }
+
+ ui.animating = (UIElement **) UI_REALLOC(ui.animating, sizeof(UIElement *) * (ui.animatingCount + 1));
+ ui.animating[ui.animatingCount] = element;
+ ui.animatingCount++;
+ UI_ASSERT(~element->flags & UI_ELEMENT_DESTROY);
+ return true;
+ }
+}
+
+uint64_t UIAnimateClock() {
+ return (uint64_t) UI_CLOCK() * 1000 / UI_CLOCKS_PER_SECOND;
+}
+
+void _UIProcessAnimations() {
+ bool update = ui.animatingCount;
+
+ for (uint32_t i = 0; i < ui.animatingCount; i++) {
+ UIElementMessage(ui.animating[i], UI_MSG_ANIMATE, 0, 0);
+ }
+
+ if (update) {
+ _UIUpdate();
+ }
+}
+
+/////////////////////////////////////////
+// Rendering.
+/////////////////////////////////////////
+
+void UIDrawBlock(UIPainter *painter, UIRectangle rectangle, uint32_t color) {
+ rectangle = UIRectangleIntersection(painter->clip, rectangle);
+
+ if (!UI_RECT_VALID(rectangle)) {
+ return;
+ }
+
+#ifdef UI_SSE2
+ __m128i color4 = _mm_set_epi32(color, color, color, color);
+#endif
+
+ for (int line = rectangle.t; line < rectangle.b; line++) {
+ uint32_t *bits = painter->bits + line * painter->width + rectangle.l;
+ int count = UI_RECT_WIDTH(rectangle);
+
+#ifdef UI_SSE2
+ while (count >= 4) {
+ _mm_storeu_si128((__m128i *) bits, color4);
+ bits += 4;
+ count -= 4;
+ }
+#endif
+
+ while (count--) {
+ *bits++ = color;
+ }
+ }
+
+#ifdef UI_DEBUG
+ painter->fillCount += UI_RECT_WIDTH(rectangle) * UI_RECT_HEIGHT(rectangle);
+#endif
+}
+
+bool UIDrawLine(UIPainter *painter, int x0, int y0, int x1, int y1, uint32_t color) {
+ // Apply the clip.
+
+ UIRectangle c = painter->clip;
+ if (!UI_RECT_VALID(c)) return false;
+ int dx = x1 - x0, dy = y1 - y0;
+ const int p[4] = { -dx, dx, -dy, dy };
+ const int q[4] = { x0 - c.l, c.r - 1 - x0, y0 - c.t, c.b - 1 - y0 };
+ float t0 = 0.0f, t1 = 1.0f; // How far along the line the points end up.
+
+ for (int i = 0; i < 4; i++) {
+ if (!p[i] && q[i] < 0) return false;
+ float r = (float) q[i] / p[i];
+ if (p[i] < 0 && r > t1) return false;
+ if (p[i] > 0 && r < t0) return false;
+ if (p[i] < 0 && r > t0) t0 = r;
+ if (p[i] > 0 && r < t1) t1 = r;
+ }
+
+ x1 = x0 + t1 * dx, y1 = y0 + t1 * dy;
+ x0 += t0 * dx, y0 += t0 * dy;
+
+ // Calculate the delta X and delta Y.
+
+ if (y1 < y0) {
+ int t;
+ t = x0, x0 = x1, x1 = t;
+ t = y0, y0 = y1, y1 = t;
+ }
+
+ dx = x1 - x0, dy = y1 - y0;
+ int dxs = dx < 0 ? -1 : 1;
+ if (dx < 0) dx = -dx;
+
+ // Draw the line using Bresenham's line algorithm.
+
+ uint32_t *bits = painter->bits + y0 * painter->width + x0;
+
+ if (dy * dy < dx * dx) {
+ int m = 2 * dy - dx;
+
+ for (int i = 0; i < dx; i++, bits += dxs) {
+ *bits = color;
+ if (m > 0) bits += painter->width, m -= 2 * dx;
+ m += 2 * dy;
+ }
+ } else {
+ int m = 2 * dx - dy;
+
+ for (int i = 0; i < dy; i++, bits += painter->width) {
+ *bits = color;
+ if (m > 0) bits += dxs, m -= 2 * dy;
+ m += 2 * dx;
+ }
+ }
+
+ return true;
+}
+
+void UIDrawCircle(UIPainter *painter, int cx, int cy, int radius, uint32_t fillColor, uint32_t outlineColor, bool hollow) {
+ // TODO There's a hole missing at the bottom of the circle!
+ // TODO This looks bad at small radii (< 20).
+
+ float x = 0, y = -radius;
+ float dx = radius, dy = 0;
+ float step = 0.2f / radius;
+ int px = 0, py = cy + y;
+
+ while (x >= 0) {
+ x += dx * step;
+ y += dy * step;
+ dx += -x * step;
+ dy += -y * step;
+
+ int ix = x, iy = cy + y;
+
+ while (py <= iy) {
+ if (py >= painter->clip.t && py < painter->clip.b) {
+ for (int s = 0; s <= ix || s <= px; s++) {
+ bool inOutline = ((s <= ix) != (s <= px)) || ((ix == px) && (s == ix));
+ if (hollow && !inOutline) continue;
+ bool clip0 = cx + s >= painter->clip.l && cx + s < painter->clip.r;
+ bool clip1 = cx - s >= painter->clip.l && cx - s < painter->clip.r;
+ if (clip0) painter->bits[painter->width * py + cx + s] = inOutline ? outlineColor : fillColor;
+ if (clip1) painter->bits[painter->width * py + cx - s] = inOutline ? outlineColor : fillColor;
+ }
+ }
+
+ px = ix, py++;
+ }
+ }
+}
+
+void UIDrawTriangle(UIPainter *painter, int x0, int y0, int x1, int y1, int x2, int y2, uint32_t color) {
+ // Step 1: Sort the points by their y-coordinate.
+ if (y1 < y0) { int xt = x0; x0 = x1, x1 = xt; int yt = y0; y0 = y1, y1 = yt; }
+ if (y2 < y1) { int xt = x1; x1 = x2, x2 = xt; int yt = y1; y1 = y2, y2 = yt; }
+ if (y1 < y0) { int xt = x0; x0 = x1, x1 = xt; int yt = y0; y0 = y1, y1 = yt; }
+ if (y2 == y0) return;
+
+ // Step 2: Clip the triangle.
+ if (x0 < painter->clip.l && x1 < painter->clip.l && x2 < painter->clip.l) return;
+ if (x0 >= painter->clip.r && x1 >= painter->clip.r && x2 >= painter->clip.r) return;
+ if (y2 < painter->clip.t || y0 >= painter->clip.b) return;
+ bool needsXClip = x0 < painter->clip.l + 1 || x0 >= painter->clip.r - 1
+ || x1 < painter->clip.l + 1 || x1 >= painter->clip.r - 1
+ || x2 < painter->clip.l + 1 || x2 >= painter->clip.r - 1;
+ bool needsYClip = y0 < painter->clip.t + 1 || y2 >= painter->clip.b - 1;
+#define _UI_DRAW_TRIANGLE_APPLY_CLIP(xo, yo) \
+ if (needsYClip && (yi + yo < painter->clip.t || yi + yo >= painter->clip.b)) continue; \
+ if (needsXClip && xf + xo < painter->clip.l) xf = painter->clip.l - xo; \
+ if (needsXClip && xt + xo > painter->clip.r) xt = painter->clip.r - xo;
+
+ // Step 3: Split into 2 triangles with bases aligned with the x-axis.
+ float xm0 = (x2 - x0) * (y1 - y0) / (y2 - y0), xm1 = x1 - x0;
+ if (xm1 < xm0) { float xmt = xm0; xm0 = xm1, xm1 = xmt; }
+ float xe0 = xm0 + x0 - x2, xe1 = xm1 + x0 - x2;
+ int ym = y1 - y0, ye = y2 - y1;
+ float ymr = 1.0f / ym, yer = 1.0f / ye;
+
+ // Step 4: Draw the top part.
+ for (float y = 0; y < ym; y++) {
+ int xf = xm0 * y * ymr, xt = xm1 * y * ymr, yi = (int) y;
+ _UI_DRAW_TRIANGLE_APPLY_CLIP(x0, y0);
+ uint32_t *b = &painter->bits[(yi + y0) * painter->width + x0];
+ for (int x = xf; x < xt; x++) b[x] = color;
+ }
+
+ // Step 5: Draw the bottom part.
+ for (float y = 0; y < ye; y++) {
+ int xf = xe0 * (ye - y) * yer, xt = xe1 * (ye - y) * yer, yi = (int) y;
+ _UI_DRAW_TRIANGLE_APPLY_CLIP(x2, y1);
+ uint32_t *b = &painter->bits[(yi + y1) * painter->width + x2];
+ for (int x = xf; x < xt; x++) b[x] = color;
+ }
+}
+
+void UIDrawTriangleOutline(UIPainter *painter, int x0, int y0, int x1, int y1, int x2, int y2, uint32_t color) {
+ UIDrawLine(painter, x0, y0, x1, y1, color);
+ UIDrawLine(painter, x1, y1, x2, y2, color);
+ UIDrawLine(painter, x2, y2, x0, y0, color);
+}
+
+void UIDrawInvert(UIPainter *painter, UIRectangle rectangle) {
+ rectangle = UIRectangleIntersection(painter->clip, rectangle);
+
+ if (!UI_RECT_VALID(rectangle)) {
+ return;
+ }
+
+ for (int line = rectangle.t; line < rectangle.b; line++) {
+ uint32_t *bits = painter->bits + line * painter->width + rectangle.l;
+ int count = UI_RECT_WIDTH(rectangle);
+
+ while (count--) {
+ uint32_t in = *bits;
+ *bits = in ^ 0xFFFFFF;
+ bits++;
+ }
+ }
+}
+
+int UIMeasureStringWidth(const char *string, ptrdiff_t bytes) {
+#ifdef UI_UNICODE
+ return Utf8StringLength(string, bytes) * ui.activeFont->glyphWidth;
+#else
+ if (bytes == -1) {
+ bytes = _UIStringLength(string);
+ }
+
+ return bytes * ui.activeFont->glyphWidth;
+#endif
+}
+
+int UIMeasureStringHeight() {
+ return ui.activeFont->glyphHeight;
+}
+
+void UIDrawString(UIPainter *painter, UIRectangle r, const char *string, ptrdiff_t bytes, uint32_t color, int align, UIStringSelection *selection) {
+ UIRectangle oldClip = painter->clip;
+ painter->clip = UIRectangleIntersection(r, oldClip);
+
+ if (!UI_RECT_VALID(painter->clip)) {
+ painter->clip = oldClip;
+ return;
+ }
+
+ if (bytes == -1) {
+ bytes = _UIStringLength(string);
+ }
+
+ int width = UIMeasureStringWidth(string, bytes);
+ int height = UIMeasureStringHeight();
+ int x = align == UI_ALIGN_CENTER ? ((r.l + r.r - width) / 2) : align == UI_ALIGN_RIGHT ? (r.r - width) : r.l;
+ int y = (r.t + r.b - height) / 2;
+ int i = 0, j = 0;
+
+ int selectFrom = -1, selectTo = -1;
+
+ if (selection) {
+ selectFrom = selection->carets[0];
+ selectTo = selection->carets[1];
+
+ if (selectFrom > selectTo) {
+ UI_SWAP(int, selectFrom, selectTo);
+ }
+ }
+
+ while (j < bytes) {
+ ptrdiff_t bytesConsumed = 1;
+#ifdef UI_UNICODE
+ int c = Utf8GetCodePoint(string, bytes - j, &bytesConsumed);
+ UI_ASSERT(bytesConsumed > 0);
+ string += bytesConsumed;
+#else
+ char c = *string++;
+#endif
+ uint32_t colorText = color;
+
+ if (i >= selectFrom && i < selectTo) {
+ int w = ui.activeFont->glyphWidth;
+ if (c == '\t') {
+ int ii = i;
+ while (++ii & 3) w += ui.activeFont->glyphWidth;
+ }
+ UIDrawBlock(painter, UI_RECT_4(x, x + w, y, y + height), selection->colorBackground);
+ colorText = selection->colorText;
+ }
+
+ if (c != '\t') {
+ UIDrawGlyph(painter, x, y, c, colorText);
+ }
+
+ if (selection && selection->carets[0] == i) {
+ UIDrawInvert(painter, UI_RECT_4(x, x + 1, y, y + height));
+ }
+
+ x += ui.activeFont->glyphWidth, i++;
+
+ if (c == '\t') {
+ while (i & 3) x += ui.activeFont->glyphWidth, i++;
+ }
+
+ j += bytesConsumed;
+ }
+
+ if (selection && selection->carets[0] == i) {
+ UIDrawInvert(painter, UI_RECT_4(x, x + 1, y, y + height));
+ }
+
+ painter->clip = oldClip;
+}
+
+void UIDrawBorder(UIPainter *painter, UIRectangle r, uint32_t borderColor, UIRectangle borderSize) {
+ UIDrawBlock(painter, UI_RECT_4(r.l, r.r, r.t, r.t + borderSize.t), borderColor);
+ UIDrawBlock(painter, UI_RECT_4(r.l, r.l + borderSize.l, r.t + borderSize.t, r.b - borderSize.b), borderColor);
+ UIDrawBlock(painter, UI_RECT_4(r.r - borderSize.r, r.r, r.t + borderSize.t, r.b - borderSize.b), borderColor);
+ UIDrawBlock(painter, UI_RECT_4(r.l, r.r, r.b - borderSize.b, r.b), borderColor);
+}
+
+void UIDrawRectangle(UIPainter *painter, UIRectangle r, uint32_t mainColor, uint32_t borderColor, UIRectangle borderSize) {
+ UIDrawBorder(painter, r, borderColor, borderSize);
+ UIDrawBlock(painter, UI_RECT_4(r.l + borderSize.l, r.r - borderSize.r, r.t + borderSize.t, r.b - borderSize.b), mainColor);
+}
+
+void UIDrawControlDefault(UIPainter *painter, UIRectangle bounds, uint32_t mode, const char *label, ptrdiff_t labelBytes, double position, float scale) {
+ bool checked = mode & UI_DRAW_CONTROL_STATE_CHECKED;
+ bool disabled = mode & UI_DRAW_CONTROL_STATE_DISABLED;
+ bool focused = mode & UI_DRAW_CONTROL_STATE_FOCUSED;
+ bool hovered = mode & UI_DRAW_CONTROL_STATE_HOVERED;
+ bool indeterminate = mode & UI_DRAW_CONTROL_STATE_INDETERMINATE;
+ bool pressed = mode & UI_DRAW_CONTROL_STATE_PRESSED;
+ bool selected = mode & UI_DRAW_CONTROL_STATE_SELECTED;
+ uint32_t which = mode & UI_DRAW_CONTROL_TYPE_MASK;
+
+ uint32_t buttonColor = disabled ? ui.theme.buttonDisabled
+ : (pressed && hovered) ? ui.theme.buttonPressed
+ : (pressed || hovered) ? ui.theme.buttonHovered
+ : focused ? ui.theme.selected : ui.theme.buttonNormal;
+ uint32_t buttonTextColor = disabled ? ui.theme.textDisabled
+ : buttonColor == ui.theme.selected ? ui.theme.textSelected : ui.theme.text;
+
+ if (which == UI_DRAW_CONTROL_CHECKBOX) {
+ uint32_t color = buttonColor, textColor = buttonTextColor;
+ int midY = (bounds.t + bounds.b) / 2;
+ UIRectangle boxBounds = UI_RECT_4(bounds.l, bounds.l + UI_SIZE_CHECKBOX_BOX,
+ midY - UI_SIZE_CHECKBOX_BOX / 2, midY + UI_SIZE_CHECKBOX_BOX / 2);
+ UIDrawRectangle(painter, boxBounds, color, ui.theme.border, UI_RECT_1(1));
+ UIDrawString(painter, UIRectangleAdd(boxBounds, UI_RECT_4(1, 0, 0, 0)),
+ checked ? "*" : indeterminate ? "-" : " ", -1,
+ textColor, UI_ALIGN_CENTER, NULL);
+ UIDrawString(painter, UIRectangleAdd(bounds, UI_RECT_4(UI_SIZE_CHECKBOX_BOX + UI_SIZE_CHECKBOX_GAP, 0, 0, 0)),
+ label, labelBytes, disabled ? ui.theme.textDisabled : ui.theme.text, UI_ALIGN_LEFT, NULL);
+ } else if (which == UI_DRAW_CONTROL_MENU_ITEM || which == UI_DRAW_CONTROL_DROP_DOWN || which == UI_DRAW_CONTROL_PUSH_BUTTON) {
+ uint32_t color = buttonColor, textColor = buttonTextColor;
+ int borderSize = which == UI_DRAW_CONTROL_MENU_ITEM ? 0 : scale;
+ UIDrawRectangle(painter, bounds, color, ui.theme.border, UI_RECT_1(borderSize));
+
+ if (checked && !focused) {
+ UIDrawBlock(painter, UIRectangleAdd(bounds, UI_RECT_1I((int) (UI_SIZE_BUTTON_CHECKED_AREA * scale))), ui.theme.buttonPressed);
+ }
+
+ UIRectangle innerBounds = UIRectangleAdd(bounds, UI_RECT_2I((int) (UI_SIZE_MENU_ITEM_MARGIN * scale), 0));
+
+ if (which == UI_DRAW_CONTROL_MENU_ITEM) {
+ if (labelBytes == -1) {
+ labelBytes = _UIStringLength(label);
+ }
+
+ int tab = 0;
+ for (; tab < labelBytes && label[tab] != '\t'; tab++);
+
+ UIDrawString(painter, innerBounds, label, tab, textColor, UI_ALIGN_LEFT, NULL);
+
+ if (labelBytes > tab) {
+ UIDrawString(painter, innerBounds, label + tab + 1, labelBytes - tab - 1, textColor, UI_ALIGN_RIGHT, NULL);
+ }
+ } else if (which == UI_DRAW_CONTROL_DROP_DOWN) {
+ UIDrawString(painter, innerBounds, label, labelBytes, textColor, UI_ALIGN_LEFT, NULL);
+ UIDrawString(painter, innerBounds, "\x19", 1, textColor, UI_ALIGN_RIGHT, NULL);
+ } else {
+ UIDrawString(painter, bounds, label, labelBytes, textColor, UI_ALIGN_CENTER, NULL);
+ }
+ } else if (which == UI_DRAW_CONTROL_LABEL) {
+ UIDrawString(painter, bounds, label, labelBytes, ui.theme.text, UI_ALIGN_LEFT, NULL);
+ } else if (which == UI_DRAW_CONTROL_SPLITTER) {
+ UIRectangle borders = (mode & UI_DRAW_CONTROL_STATE_VERTICAL) ? UI_RECT_2(0, 1) : UI_RECT_2(1, 0);
+ UIDrawRectangle(painter, bounds, ui.theme.buttonNormal, ui.theme.border, borders);
+ } else if (which == UI_DRAW_CONTROL_SCROLL_TRACK) {
+ if (disabled) UIDrawBlock(painter, bounds, ui.theme.panel1);
+ } else if (which == UI_DRAW_CONTROL_SCROLL_DOWN || which == UI_DRAW_CONTROL_SCROLL_UP) {
+ bool isDown = which == UI_DRAW_CONTROL_SCROLL_DOWN;
+ uint32_t color = pressed ? ui.theme.buttonPressed : hovered ? ui.theme.buttonHovered : ui.theme.panel2;
+ UIDrawRectangle(painter, bounds, color, ui.theme.border, UI_RECT_1(0));
+
+ if (mode & UI_DRAW_CONTROL_STATE_VERTICAL) {
+ UIDrawGlyph(painter, (bounds.l + bounds.r - ui.activeFont->glyphWidth) / 2 + 1,
+ isDown ? (bounds.b - ui.activeFont->glyphHeight - 2 * scale)
+ : (bounds.t + 2 * scale),
+ isDown ? 25 : 24, ui.theme.text);
+ } else {
+ UIDrawGlyph(painter, isDown ? (bounds.r - ui.activeFont->glyphWidth - 2 * scale)
+ : (bounds.l + 2 * scale),
+ (bounds.t + bounds.b - ui.activeFont->glyphHeight) / 2,
+ isDown ? 26 : 27, ui.theme.text);
+ }
+ } else if (which == UI_DRAW_CONTROL_SCROLL_THUMB) {
+ uint32_t color = pressed ? ui.theme.buttonPressed : hovered ? ui.theme.buttonHovered : ui.theme.buttonNormal;
+ UIDrawRectangle(painter, bounds, color, ui.theme.border, UI_RECT_1(2));
+ } else if (which == UI_DRAW_CONTROL_GAUGE) {
+ UIDrawRectangle(painter, bounds, ui.theme.buttonNormal, ui.theme.border, UI_RECT_1(1));
+ UIRectangle filled = UIRectangleAdd(bounds, UI_RECT_1I(1));
+ filled.r = filled.l + UI_RECT_WIDTH(filled) * position;
+ UIDrawBlock(painter, filled, ui.theme.selected);
+ } else if (which == UI_DRAW_CONTROL_SLIDER) {
+ int centerY = (bounds.t + bounds.b) / 2;
+ int trackSize = UI_SIZE_SLIDER_TRACK * scale;
+ int thumbSize = UI_SIZE_SLIDER_THUMB * scale;
+ int thumbPosition = (UI_RECT_WIDTH(bounds) - thumbSize) * position;
+ UIRectangle track = UI_RECT_4(bounds.l, bounds.r, centerY - (trackSize + 1) / 2, centerY + trackSize / 2);
+ UIDrawRectangle(painter, track, disabled ? ui.theme.buttonDisabled : ui.theme.buttonNormal, ui.theme.border, UI_RECT_1(1));
+ uint32_t color = disabled ? ui.theme.buttonDisabled : pressed ? ui.theme.buttonPressed : hovered ? ui.theme.buttonHovered : ui.theme.buttonNormal;
+ UIRectangle thumb = UI_RECT_4(bounds.l + thumbPosition, bounds.l + thumbPosition + thumbSize, centerY - (thumbSize + 1) / 2, centerY + thumbSize / 2);
+ UIDrawRectangle(painter, thumb, color, ui.theme.border, UI_RECT_1(1));
+ } else if (which == UI_DRAW_CONTROL_TEXTBOX) {
+ UIDrawRectangle(painter, bounds,
+ disabled ? ui.theme.buttonDisabled : focused ? ui.theme.textboxFocused : ui.theme.textboxNormal,
+ ui.theme.border, UI_RECT_1(1));
+ } else if (which == UI_DRAW_CONTROL_MODAL_POPUP) {
+ UIRectangle bounds2 = UIRectangleAdd(bounds, UI_RECT_1I(-1));
+ UIDrawBorder(painter, bounds2, ui.theme.border, UI_RECT_1(1));
+ UIDrawBorder(painter, UIRectangleAdd(bounds2, UI_RECT_1(1)), ui.theme.border, UI_RECT_1(1));
+ } else if (which == UI_DRAW_CONTROL_MENU) {
+ UIDrawBlock(painter, bounds, ui.theme.border);
+ } else if (which == UI_DRAW_CONTROL_TABLE_ROW) {
+ if (selected) UIDrawBlock(painter, bounds, ui.theme.selected);
+ else if (hovered) UIDrawBlock(painter, bounds, ui.theme.buttonHovered);
+ } else if (which == UI_DRAW_CONTROL_TABLE_CELL) {
+ uint32_t textColor = selected ? ui.theme.textSelected : ui.theme.text;
+ UIDrawString(painter, bounds, label, labelBytes, textColor, UI_ALIGN_LEFT, NULL);
+ } else if (which == UI_DRAW_CONTROL_TABLE_BACKGROUND) {
+ UIDrawBlock(painter, bounds, ui.theme.panel2);
+ UIDrawRectangle(painter, UI_RECT_4(bounds.l, bounds.r, bounds.t, bounds.t + (int) (UI_SIZE_TABLE_HEADER * scale)),
+ ui.theme.panel1, ui.theme.border, UI_RECT_4(0, 0, 0, 1));
+ } else if (which == UI_DRAW_CONTROL_TABLE_HEADER) {
+ UIDrawString(painter, bounds, label, labelBytes, ui.theme.text, UI_ALIGN_LEFT, NULL);
+ if (selected) UIDrawInvert(painter, bounds);
+ } else if (which == UI_DRAW_CONTROL_MDI_CHILD) {
+ UI_MDI_CHILD_CALCULATE_LAYOUT(bounds, scale);
+ UIRectangle borders = UI_RECT_4(borderSize, borderSize, titleSize, borderSize);
+ UIDrawBorder(painter, bounds, ui.theme.buttonNormal, borders);
+ UIDrawBorder(painter, bounds, ui.theme.border, UI_RECT_1((int) scale));
+ UIDrawBorder(painter, UIRectangleAdd(content, UI_RECT_1I(-1)), ui.theme.border, UI_RECT_1((int) scale));
+ UIDrawString(painter, title, label, labelBytes, ui.theme.text, UI_ALIGN_LEFT, NULL);
+ } else if (which == UI_DRAW_CONTROL_TAB) {
+ uint32_t color = selected ? ui.theme.buttonPressed : ui.theme.buttonNormal;
+ UIRectangle t = bounds;
+ if (selected) t.b++, t.t--;
+ else t.t++;
+ UIDrawRectangle(painter, t, color, ui.theme.border, UI_RECT_1(1));
+ UIDrawString(painter, bounds, label, labelBytes, ui.theme.text, UI_ALIGN_CENTER, NULL);
+ } else if (which == UI_DRAW_CONTROL_TAB_BAND) {
+ UIDrawRectangle(painter, bounds, ui.theme.panel1, ui.theme.border, UI_RECT_4(0, 0, 0, 1));
+ }
+}
+
+/////////////////////////////////////////
+// Element hierarchy.
+/////////////////////////////////////////
+
+void _UIElementDestroyDescendents(UIElement *element, bool topLevel) {
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (!topLevel || (~child->flags & UI_ELEMENT_NON_CLIENT)) {
+ UIElementDestroy(child);
+ }
+ }
+
+#ifdef UI_DEBUG
+ _UIInspectorRefresh();
+#endif
+}
+
+void UIElementDestroyDescendents(UIElement *element) {
+ _UIElementDestroyDescendents(element, true);
+}
+
+void UIElementDestroy(UIElement *element) {
+ if (element->flags & UI_ELEMENT_DESTROY) {
+ return;
+ }
+
+ UIElementMessage(element, UI_MSG_DESTROY, 0, 0);
+ element->flags |= UI_ELEMENT_DESTROY | UI_ELEMENT_HIDE;
+
+ UIElement *ancestor = element->parent;
+
+ while (ancestor) {
+ if (ancestor->flags & UI_ELEMENT_DESTROY_DESCENDENT) break;
+ ancestor->flags |= UI_ELEMENT_DESTROY_DESCENDENT;
+ ancestor = ancestor->parent;
+ }
+
+ _UIElementDestroyDescendents(element, false);
+
+ if (element->parent) {
+ UIElementRelayout(element->parent);
+ UIElementRepaint(element->parent, &element->bounds);
+ UIElementMeasurementsChanged(element->parent, 3);
+ }
+}
+
+int UIElementMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message != UI_MSG_DEALLOCATE && (element->flags & UI_ELEMENT_DESTROY)) {
+ return 0;
+ }
+
+ if (message >= UI_MSG_INPUT_EVENTS_START && message <= UI_MSG_INPUT_EVENTS_END && (element->flags & UI_ELEMENT_DISABLED)) {
+ return 0;
+ }
+
+ if (element->messageUser) {
+ int result = element->messageUser(element, message, di, dp);
+
+ if (result) {
+ return result;
+ }
+ }
+
+ if (element->messageClass) {
+ return element->messageClass(element, message, di, dp);
+ } else {
+ return 0;
+ }
+}
+
+UIElement *UIElementChangeParent(UIElement *element, UIElement *newParent, UIElement *insertBefore) {
+ bool found = false;
+ UIElement *oldBefore = NULL;
+
+ for (uint32_t i = 0; i < element->parent->childCount; i++) {
+ if (element->parent->children[i] == element) {
+ UI_MEMMOVE(&element->parent->children[i], &element->parent->children[i + 1], sizeof(UIElement *) * (element->parent->childCount - i - 1));
+ element->parent->childCount--;
+ oldBefore = i == element->parent->childCount ? NULL : element->parent->children[i];
+ found = true;
+ break;
+ }
+ }
+
+ UI_ASSERT(found && (~element->flags & UI_ELEMENT_DESTROY));
+
+ for (uint32_t i = 0; i <= newParent->childCount; i++) {
+ if (i == newParent->childCount || newParent->children[i] == insertBefore) {
+ newParent->children = (UIElement **) UI_REALLOC(newParent->children, sizeof(UIElement *) * (newParent->childCount + 1));
+ UI_MEMMOVE(&newParent->children[i + 1], &newParent->children[i], sizeof(UIElement *) * (newParent->childCount - i));
+ newParent->childCount++;
+ newParent->children[i] = element;
+ found = true;
+ break;
+ }
+ }
+
+ UIElement *oldParent = element->parent;
+ element->parent = newParent;
+ element->window = newParent->window;
+
+ UIElementMeasurementsChanged(oldParent, 3);
+ UIElementMeasurementsChanged(newParent, 3);
+
+ return oldBefore;
+}
+
+UIElement *UIElementCreate(size_t bytes, UIElement *parent, uint32_t flags, int (*message)(UIElement *, UIMessage, int, void *), const char *cClassName) {
+ UI_ASSERT(bytes >= sizeof(UIElement));
+ UIElement *element = (UIElement *) UI_CALLOC(bytes);
+ element->flags = flags;
+ element->messageClass = message;
+
+ if (!parent && (~flags & UI_ELEMENT_WINDOW)) {
+ UI_ASSERT(ui.parentStackCount);
+ parent = ui.parentStack[ui.parentStackCount - 1];
+ }
+
+ if (parent) {
+ UI_ASSERT(~parent->flags & UI_ELEMENT_DESTROY);
+ element->window = parent->window;
+ element->parent = parent;
+ parent->children = (UIElement **) UI_REALLOC(parent->children, sizeof(UIElement *) * (parent->childCount + 1));
+ parent->children[parent->childCount] = element;
+ parent->childCount++;
+ UIElementRelayout(parent);
+ UIElementMeasurementsChanged(parent, 3);
+ }
+
+ element->cClassName = cClassName;
+ static uint32_t id = 0;
+ element->id = ++id;
+
+#ifdef UI_DEBUG
+ _UIInspectorRefresh();
+#endif
+
+ if (flags & UI_ELEMENT_PARENT_PUSH) {
+ UIParentPush(element);
+ }
+
+ return element;
+}
+
+UIElement *UIParentPush(UIElement *element) {
+ UI_ASSERT(ui.parentStackCount != sizeof(ui.parentStack) / sizeof(ui.parentStack[0]));
+ ui.parentStack[ui.parentStackCount++] = element;
+ return element;
+}
+
+UIElement *UIParentPop() {
+ UI_ASSERT(ui.parentStackCount);
+ ui.parentStackCount--;
+ return ui.parentStack[ui.parentStackCount];
+}
+
+/////////////////////////////////////////
+// Panels.
+/////////////////////////////////////////
+
+int _UIPanelCalculatePerFill(UIPanel *panel, int *_count, int hSpace, int vSpace, float scale) {
+ bool horizontal = panel->e.flags & UI_PANEL_HORIZONTAL;
+ int available = horizontal ? hSpace : vSpace;
+ int count = 0, fill = 0, perFill = 0;
+
+ for (uint32_t i = 0; i < panel->e.childCount; i++) {
+ UIElement *child = panel->e.children[i];
+
+ if (child->flags & (UI_ELEMENT_HIDE | UI_ELEMENT_NON_CLIENT)) {
+ continue;
+ }
+
+ count++;
+
+ if (horizontal) {
+ if (child->flags & UI_ELEMENT_H_FILL) {
+ fill++;
+ } else if (available > 0) {
+ available -= UIElementMessage(child, UI_MSG_GET_WIDTH, vSpace, 0);
+ }
+ } else {
+ if (child->flags & UI_ELEMENT_V_FILL) {
+ fill++;
+ } else if (available > 0) {
+ available -= UIElementMessage(child, UI_MSG_GET_HEIGHT, hSpace, 0);
+ }
+ }
+ }
+
+ if (count) {
+ available -= (count - 1) * (int) (panel->gap * scale);
+ }
+
+ if (available > 0 && fill) {
+ perFill = available / fill;
+ }
+
+ if (_count) {
+ *_count = count;
+ }
+
+ return perFill;
+}
+
+int _UIPanelMeasure(UIPanel *panel, int di) {
+ bool horizontal = panel->e.flags & UI_PANEL_HORIZONTAL;
+ int perFill = _UIPanelCalculatePerFill(panel, NULL, horizontal ? di : 0, horizontal ? 0 : di, panel->e.window->scale);
+ int size = 0;
+
+ for (uint32_t i = 0; i < panel->e.childCount; i++) {
+ UIElement *child = panel->e.children[i];
+ if (child->flags & (UI_ELEMENT_HIDE | UI_ELEMENT_NON_CLIENT)) continue;
+ int childSize = UIElementMessage(child, horizontal ? UI_MSG_GET_HEIGHT : UI_MSG_GET_WIDTH,
+ (child->flags & (horizontal ? UI_ELEMENT_H_FILL : UI_ELEMENT_V_FILL)) ? perFill : 0, 0);
+ if (childSize > size) size = childSize;
+ }
+
+ int border = horizontal ? (panel->border.t + panel->border.b) : (panel->border.l + panel->border.r);
+ return size + border * panel->e.window->scale;
+}
+
+int _UIPanelLayout(UIPanel *panel, UIRectangle bounds, bool measure) {
+ bool horizontal = panel->e.flags & UI_PANEL_HORIZONTAL;
+ float scale = panel->e.window->scale;
+ int position = (horizontal ? panel->border.l : panel->border.t) * scale;
+ if (panel->scrollBar && !measure) position -= panel->scrollBar->position;
+ int hSpace = UI_RECT_WIDTH(bounds) - UI_RECT_TOTAL_H(panel->border) * scale;
+ int vSpace = UI_RECT_HEIGHT(bounds) - UI_RECT_TOTAL_V(panel->border) * scale;
+ int count = 0;
+ int perFill = _UIPanelCalculatePerFill(panel, &count, hSpace, vSpace, scale);
+ int scaledBorder2 = (horizontal ? panel->border.t : panel->border.l) * panel->e.window->scale;
+ bool expand = panel->e.flags & UI_PANEL_EXPAND;
+
+ for (uint32_t i = 0; i < panel->e.childCount; i++) {
+ UIElement *child = panel->e.children[i];
+
+ if (child->flags & (UI_ELEMENT_HIDE | UI_ELEMENT_NON_CLIENT)) {
+ continue;
+ }
+
+ if (horizontal) {
+ int height = ((child->flags & UI_ELEMENT_V_FILL) || expand) ? vSpace
+ : UIElementMessage(child, UI_MSG_GET_HEIGHT, (child->flags & UI_ELEMENT_H_FILL) ? perFill : 0, 0);
+ int width = (child->flags & UI_ELEMENT_H_FILL) ? perFill : UIElementMessage(child, UI_MSG_GET_WIDTH, height, 0);
+ UIRectangle relative = UI_RECT_4(position, position + width,
+ scaledBorder2 + (vSpace - height) / 2,
+ scaledBorder2 + (vSpace + height) / 2);
+ if (!measure) UIElementMove(child, UIRectangleTranslate(relative, bounds), false);
+ position += width + panel->gap * scale;
+ } else {
+ int width = ((child->flags & UI_ELEMENT_H_FILL) || expand) ? hSpace
+ : UIElementMessage(child, UI_MSG_GET_WIDTH, (child->flags & UI_ELEMENT_V_FILL) ? perFill : 0, 0);
+ int height = (child->flags & UI_ELEMENT_V_FILL) ? perFill : UIElementMessage(child, UI_MSG_GET_HEIGHT, width, 0);
+ UIRectangle relative = UI_RECT_4(scaledBorder2 + (hSpace - width) / 2,
+ scaledBorder2 + (hSpace + width) / 2, position, position + height);
+ if (!measure) UIElementMove(child, UIRectangleTranslate(relative, bounds), false);
+ position += height + panel->gap * scale;
+ }
+ }
+
+ return position - (count ? panel->gap : 0) * scale + (horizontal ? panel->border.r : panel->border.b) * scale;
+}
+
+int _UIPanelMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIPanel *panel = (UIPanel *) element;
+ bool horizontal = element->flags & UI_PANEL_HORIZONTAL;
+
+ if (message == UI_MSG_LAYOUT) {
+ int scrollBarWidth = panel->scrollBar ? (UI_SIZE_SCROLL_BAR * element->window->scale) : 0;
+ UIRectangle bounds = element->bounds;
+ bounds.r -= scrollBarWidth;
+
+ if (panel->scrollBar) {
+ UIRectangle scrollBarBounds = element->bounds;
+ scrollBarBounds.l = scrollBarBounds.r - scrollBarWidth;
+ panel->scrollBar->maximum = _UIPanelLayout(panel, bounds, true);
+ panel->scrollBar->page = UI_RECT_HEIGHT(element->bounds);
+ UIElementMove(&panel->scrollBar->e, scrollBarBounds, true);
+ }
+
+ _UIPanelLayout(panel, bounds, false);
+ } else if (message == UI_MSG_GET_WIDTH) {
+ if (horizontal) {
+ return _UIPanelLayout(panel, UI_RECT_4(0, 0, 0, di), true);
+ } else {
+ return _UIPanelMeasure(panel, di);
+ }
+ } else if (message == UI_MSG_GET_HEIGHT) {
+ if (horizontal) {
+ return _UIPanelMeasure(panel, di);
+ } else {
+ int width = di && panel->scrollBar ? (di - UI_SIZE_SCROLL_BAR * element->window->scale) : di;
+ return _UIPanelLayout(panel, UI_RECT_4(0, width, 0, 0), true);
+ }
+ } else if (message == UI_MSG_PAINT) {
+ if (element->flags & UI_PANEL_COLOR_1) {
+ UIDrawBlock((UIPainter *) dp, element->bounds, ui.theme.panel1);
+ } else if (element->flags & UI_PANEL_COLOR_2) {
+ UIDrawBlock((UIPainter *) dp, element->bounds, ui.theme.panel2);
+ }
+ } else if (message == UI_MSG_MOUSE_WHEEL && panel->scrollBar) {
+ return UIElementMessage(&panel->scrollBar->e, message, di, dp);
+ } else if (message == UI_MSG_SCROLLED) {
+ UIElementRefresh(element);
+ } else if (message == UI_MSG_GET_CHILD_STABILITY) {
+ UIElement *child = (UIElement *) dp;
+ return ((element->flags & UI_PANEL_EXPAND) ? (horizontal ? 2 : 1) : 0)
+ | ((child->flags & UI_ELEMENT_H_FILL) ? 1 : 0) | ((child->flags & UI_ELEMENT_V_FILL) ? 2 : 0);
+ }
+
+ return 0;
+}
+
+UIPanel *UIPanelCreate(UIElement *parent, uint32_t flags) {
+ UIPanel *panel = (UIPanel *) UIElementCreate(sizeof(UIPanel), parent, flags, _UIPanelMessage, "Panel");
+
+ if (flags & UI_PANEL_LARGE_SPACING) {
+ panel->border = UI_RECT_1(UI_SIZE_PANE_LARGE_BORDER);
+ panel->gap = UI_SIZE_PANE_LARGE_GAP;
+ } else if (flags & UI_PANEL_MEDIUM_SPACING) {
+ panel->border = UI_RECT_1(UI_SIZE_PANE_MEDIUM_BORDER);
+ panel->gap = UI_SIZE_PANE_MEDIUM_GAP;
+ } else if (flags & UI_PANEL_SMALL_SPACING) {
+ panel->border = UI_RECT_1(UI_SIZE_PANE_SMALL_BORDER);
+ panel->gap = UI_SIZE_PANE_SMALL_GAP;
+ }
+
+ if (flags & UI_PANEL_SCROLL) {
+ panel->scrollBar = UIScrollBarCreate(&panel->e, UI_ELEMENT_NON_CLIENT);
+ }
+
+ return panel;
+}
+
+void _UIWrapPanelLayoutRow(UIWrapPanel *panel, uint32_t rowStart, uint32_t rowEnd, int rowY, int rowHeight) {
+ int rowPosition = 0;
+
+ for (uint32_t i = rowStart; i < rowEnd; i++) {
+ UIElement *child = panel->e.children[i];
+ if (child->flags & UI_ELEMENT_HIDE) continue;
+ int height = UIElementMessage(child, UI_MSG_GET_HEIGHT, 0, 0);
+ int width = UIElementMessage(child, UI_MSG_GET_WIDTH, 0, 0);
+ UIRectangle relative = UI_RECT_4(rowPosition, rowPosition + width, rowY + rowHeight / 2 - height / 2, rowY + rowHeight / 2 + height / 2);
+ UIElementMove(child, UIRectangleTranslate(relative, panel->e.bounds), false);
+ rowPosition += width;
+ }
+}
+
+int _UIWrapPanelMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIWrapPanel *panel = (UIWrapPanel *) element;
+
+ if (message == UI_MSG_LAYOUT || message == UI_MSG_GET_HEIGHT) {
+ int totalHeight = 0;
+ int rowPosition = 0;
+ int rowHeight = 0;
+ int rowLimit = message == UI_MSG_LAYOUT ? UI_RECT_WIDTH(element->bounds) : di;
+
+ uint32_t rowStart = 0;
+
+ for (uint32_t i = 0; i < panel->e.childCount; i++) {
+ UIElement *child = panel->e.children[i];
+ if (child->flags & UI_ELEMENT_HIDE) continue;
+
+ int height = UIElementMessage(child, UI_MSG_GET_HEIGHT, 0, 0);
+ int width = UIElementMessage(child, UI_MSG_GET_WIDTH, 0, 0);
+
+ if (rowLimit && rowPosition + width > rowLimit) {
+ _UIWrapPanelLayoutRow(panel, rowStart, i, totalHeight, rowHeight);
+ totalHeight += rowHeight;
+ rowPosition = rowHeight = 0;
+ rowStart = i;
+ }
+
+ if (height > rowHeight) {
+ rowHeight = height;
+ }
+
+ rowPosition += width;
+ }
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return totalHeight + rowHeight;
+ } else {
+ _UIWrapPanelLayoutRow(panel, rowStart, panel->e.childCount, totalHeight, rowHeight);
+ }
+ }
+
+ return 0;
+}
+
+UIWrapPanel *UIWrapPanelCreate(UIElement *parent, uint32_t flags) {
+ return (UIWrapPanel *) UIElementCreate(sizeof(UIWrapPanel), parent, flags, _UIWrapPanelMessage, "Wrap Panel");
+}
+
+int _UISwitcherMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UISwitcher *switcher = (UISwitcher *) element;
+
+ if (!switcher->active) {
+ } else if (message == UI_MSG_GET_WIDTH || message == UI_MSG_GET_HEIGHT) {
+ return UIElementMessage(switcher->active, message, di, dp);
+ } else if (message == UI_MSG_LAYOUT) {
+ UIElementMove(switcher->active, element->bounds, false);
+ }
+
+ return 0;
+}
+
+void UISwitcherSwitchTo(UISwitcher *switcher, UIElement *child) {
+ for (uint32_t i = 0; i < switcher->e.childCount; i++) {
+ switcher->e.children[i]->flags |= UI_ELEMENT_HIDE;
+ }
+
+ UI_ASSERT(child->parent == &switcher->e);
+ child->flags &= ~UI_ELEMENT_HIDE;
+ switcher->active = child;
+ UIElementMeasurementsChanged(&switcher->e, 3);
+ UIElementRefresh(&switcher->e);
+}
+
+UISwitcher *UISwitcherCreate(UIElement *parent, uint32_t flags) {
+ return (UISwitcher *) UIElementCreate(sizeof(UISwitcher), parent, flags, _UISwitcherMessage, "Switcher");
+}
+
+/////////////////////////////////////////
+// Checkboxes and buttons.
+/////////////////////////////////////////
+
+int _UIButtonMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIButton *button = (UIButton *) element;
+ bool isMenuItem = element->flags & UI_BUTTON_MENU_ITEM;
+ bool isDropDown = element->flags & UI_BUTTON_DROP_DOWN;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ if (isMenuItem) {
+ return UI_SIZE_MENU_ITEM_HEIGHT * element->window->scale;
+ } else {
+ return UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+ }
+ } else if (message == UI_MSG_GET_WIDTH) {
+ int labelSize = UIMeasureStringWidth(button->label, button->labelBytes);
+ int paddedSize = labelSize + UI_SIZE_BUTTON_PADDING * element->window->scale;
+ if (isDropDown) paddedSize += ui.activeFont->glyphWidth * 2;
+ int minimumSize = ((element->flags & UI_BUTTON_SMALL) ? 0
+ : isMenuItem ? UI_SIZE_MENU_ITEM_MINIMUM_WIDTH
+ : UI_SIZE_BUTTON_MINIMUM_WIDTH)
+ * element->window->scale;
+ return paddedSize > minimumSize ? paddedSize : minimumSize;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds,
+ (isMenuItem ? UI_DRAW_CONTROL_MENU_ITEM : isDropDown ? UI_DRAW_CONTROL_DROP_DOWN : UI_DRAW_CONTROL_PUSH_BUTTON)
+ | ((element->flags & UI_BUTTON_CHECKED) ? UI_DRAW_CONTROL_STATE_CHECKED : 0) | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ button->label, button->labelBytes, 0, element->window->scale);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(button->label);
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ if (element->flags & UI_BUTTON_CAN_FOCUS) {
+ UIElementFocus(element);
+ }
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if ((m->textBytes == 1 && m->text[0] == ' ') || m->code == UI_KEYCODE_ENTER) {
+ UIElementMessage(element, UI_MSG_CLICKED, 0, 0);
+ UIElementRepaint(element, NULL);
+ return 1;
+ }
+ } else if (message == UI_MSG_CLICKED) {
+ if (button->invoke) {
+ button->invoke(element->cp);
+ }
+ }
+
+ return 0;
+}
+
+void UIButtonSetLabel(UIButton *button, const char *string, ptrdiff_t stringBytes) {
+ UI_FREE(button->label);
+ button->label = UIStringCopy(string, (button->labelBytes = stringBytes));
+ UIElementMeasurementsChanged(&button->e, 1);
+ UIElementRepaint(&button->e, NULL);
+}
+
+UIButton *UIButtonCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes) {
+ UIButton *button = (UIButton *) UIElementCreate(sizeof(UIButton), parent, flags | UI_ELEMENT_TAB_STOP, _UIButtonMessage, "Button");
+ button->label = UIStringCopy(label, (button->labelBytes = labelBytes));
+ return button;
+}
+
+int _UICheckboxMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UICheckbox *box = (UICheckbox *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ int labelSize = UIMeasureStringWidth(box->label, box->labelBytes);
+ return (labelSize + UI_SIZE_CHECKBOX_BOX + UI_SIZE_CHECKBOX_GAP) * element->window->scale;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds,
+ UI_DRAW_CONTROL_CHECKBOX | (box->check == UI_CHECK_INDETERMINATE ? UI_DRAW_CONTROL_STATE_INDETERMINATE
+ : box->check == UI_CHECK_CHECKED ? UI_DRAW_CONTROL_STATE_CHECKED : 0)
+ | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ box->label, box->labelBytes, 0, element->window->scale);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(box->label);
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if (m->textBytes == 1 && m->text[0] == ' ') {
+ UIElementMessage(element, UI_MSG_CLICKED, 0, 0);
+ UIElementRepaint(element, NULL);
+ }
+ } else if (message == UI_MSG_CLICKED) {
+ box->check = (box->check + 1) % ((element->flags & UI_CHECKBOX_ALLOW_INDETERMINATE) ? 3 : 2);
+ UIElementRepaint(element, NULL);
+ if (box->invoke) box->invoke(element->cp);
+ }
+
+ return 0;
+}
+
+UICheckbox *UICheckboxCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes) {
+ UICheckbox *box = (UICheckbox *) UIElementCreate(sizeof(UICheckbox), parent, flags | UI_ELEMENT_TAB_STOP, _UICheckboxMessage, "Checkbox");
+ box->label = UIStringCopy(label, (box->labelBytes = labelBytes));
+ return box;
+}
+
+/////////////////////////////////////////
+// Labels.
+/////////////////////////////////////////
+
+int _UILabelMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UILabel *label = (UILabel *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return UIMeasureStringHeight();
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return UIMeasureStringWidth(label->label, label->labelBytes);
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_LABEL | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ label->label, label->labelBytes, 0, element->window->scale);
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(label->label);
+ }
+
+ return 0;
+}
+
+void UILabelSetContent(UILabel *label, const char *string, ptrdiff_t stringBytes) {
+ UI_FREE(label->label);
+ label->label = UIStringCopy(string, (label->labelBytes = stringBytes));
+ UIElementMeasurementsChanged(&label->e, 1);
+ UIElementRepaint(&label->e, NULL);
+}
+
+UILabel *UILabelCreate(UIElement *parent, uint32_t flags, const char *string, ptrdiff_t stringBytes) {
+ UILabel *label = (UILabel *) UIElementCreate(sizeof(UILabel), parent, flags, _UILabelMessage, "Label");
+ label->label = UIStringCopy(string, (label->labelBytes = stringBytes));
+ return label;
+}
+
+/////////////////////////////////////////
+// Split panes.
+/////////////////////////////////////////
+
+int _UISplitPaneMessage(UIElement *element, UIMessage message, int di, void *dp);
+
+int _UISplitterMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UISplitPane *splitPane = (UISplitPane *) element->parent;
+ bool vertical = splitPane->e.flags & UI_SPLIT_PANE_VERTICAL;
+
+ if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_SPLITTER | (vertical ? UI_DRAW_CONTROL_STATE_VERTICAL : 0)
+ | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element), NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_GET_CURSOR) {
+ return vertical ? UI_CURSOR_SPLIT_V : UI_CURSOR_SPLIT_H;
+ } else if (message == UI_MSG_MOUSE_DRAG) {
+ int cursor = vertical ? element->window->cursorY : element->window->cursorX;
+ int splitterSize = UI_SIZE_SPLITTER * element->window->scale;
+ int space = (vertical ? UI_RECT_HEIGHT(splitPane->e.bounds) : UI_RECT_WIDTH(splitPane->e.bounds)) - splitterSize;
+ float oldWeight = splitPane->weight;
+ splitPane->weight = (float) (cursor - splitterSize / 2 - (vertical ? splitPane->e.bounds.t : splitPane->e.bounds.l)) / space;
+ if (splitPane->weight < 0.05f) splitPane->weight = 0.05f;
+ if (splitPane->weight > 0.95f) splitPane->weight = 0.95f;
+
+ if (splitPane->e.children[2]->messageClass == _UISplitPaneMessage
+ && (splitPane->e.children[2]->flags & UI_SPLIT_PANE_VERTICAL) == (splitPane->e.flags & UI_SPLIT_PANE_VERTICAL)) {
+ UISplitPane *subSplitPane = (UISplitPane *) splitPane->e.children[2];
+ subSplitPane->weight = (splitPane->weight - oldWeight - subSplitPane->weight + oldWeight * subSplitPane->weight) / (-1 + splitPane->weight);
+ if (subSplitPane->weight < 0.05f) subSplitPane->weight = 0.05f;
+ if (subSplitPane->weight > 0.95f) subSplitPane->weight = 0.95f;
+ }
+
+ UIElementRefresh(&splitPane->e);
+ }
+
+ return 0;
+}
+
+int _UISplitPaneMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UISplitPane *splitPane = (UISplitPane *) element;
+ bool vertical = splitPane->e.flags & UI_SPLIT_PANE_VERTICAL;
+
+ if (message == UI_MSG_LAYOUT) {
+ UIElement *splitter = element->children[0];
+ UIElement *left = element->children[1];
+ UIElement *right = element->children[2];
+
+ int splitterSize = UI_SIZE_SPLITTER * element->window->scale;
+ int space = (vertical ? UI_RECT_HEIGHT(element->bounds) : UI_RECT_WIDTH(element->bounds)) - splitterSize;
+ int leftSize = space * splitPane->weight;
+ int rightSize = space - leftSize;
+
+ if (vertical) {
+ UIElementMove(left, UI_RECT_4(element->bounds.l, element->bounds.r, element->bounds.t, element->bounds.t + leftSize), false);
+ UIElementMove(splitter, UI_RECT_4(element->bounds.l, element->bounds.r, element->bounds.t + leftSize, element->bounds.t + leftSize + splitterSize), false);
+ UIElementMove(right, UI_RECT_4(element->bounds.l, element->bounds.r, element->bounds.b - rightSize, element->bounds.b), false);
+ } else {
+ UIElementMove(left, UI_RECT_4(element->bounds.l, element->bounds.l + leftSize, element->bounds.t, element->bounds.b), false);
+ UIElementMove(splitter, UI_RECT_4(element->bounds.l + leftSize, element->bounds.l + leftSize + splitterSize, element->bounds.t, element->bounds.b), false);
+ UIElementMove(right, UI_RECT_4(element->bounds.r - rightSize, element->bounds.r, element->bounds.t, element->bounds.b), false);
+ }
+ }
+
+ return 0;
+}
+
+UISplitPane *UISplitPaneCreate(UIElement *parent, uint32_t flags, float weight) {
+ UISplitPane *splitPane = (UISplitPane *) UIElementCreate(sizeof(UISplitPane), parent, flags, _UISplitPaneMessage, "Split Pane");
+ splitPane->weight = weight;
+ UIElementCreate(sizeof(UIElement), &splitPane->e, 0, _UISplitterMessage, "Splitter");
+ return splitPane;
+}
+
+/////////////////////////////////////////
+// Tab panes.
+/////////////////////////////////////////
+
+int _UITabPaneMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UITabPane *tabPane = (UITabPane *) element;
+
+ if (message == UI_MSG_PAINT) {
+ UIPainter *painter = (UIPainter *) dp;
+ UIRectangle top = element->bounds;
+ top.b = top.t + UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+ UIDrawControl(painter, top, UI_DRAW_CONTROL_TAB_BAND, NULL, 0, 0, element->window->scale);
+
+ UIRectangle tab = top;
+ tab.l += UI_SIZE_TAB_PANE_SPACE_LEFT * element->window->scale;
+ tab.t += UI_SIZE_TAB_PANE_SPACE_TOP * element->window->scale;
+
+ int position = 0;
+ uint32_t index = 0;
+
+ while (true) {
+ int end = position;
+ for (; tabPane->tabs[end] != '\t' && tabPane->tabs[end]; end++);
+
+ int width = UIMeasureStringWidth(tabPane->tabs, end - position);
+ tab.r = tab.l + width + UI_SIZE_BUTTON_PADDING;
+
+ UIDrawControl(painter, tab, UI_DRAW_CONTROL_TAB | (tabPane->active == index ? UI_DRAW_CONTROL_STATE_SELECTED : 0),
+ tabPane->tabs + position, end - position, 0, element->window->scale);
+ tab.l = tab.r - 1;
+
+ if (tabPane->tabs[end] == '\t') {
+ position = end + 1;
+ index++;
+ } else {
+ break;
+ }
+ }
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ UIRectangle tab = element->bounds;
+ tab.b = tab.t + UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+ tab.l += UI_SIZE_TAB_PANE_SPACE_LEFT * element->window->scale;
+ tab.t += UI_SIZE_TAB_PANE_SPACE_TOP * element->window->scale;
+
+ int position = 0;
+ int index = 0;
+
+ while (true) {
+ int end = position;
+ for (; tabPane->tabs[end] != '\t' && tabPane->tabs[end]; end++);
+
+ int width = UIMeasureStringWidth(tabPane->tabs, end - position);
+ tab.r = tab.l + width + UI_SIZE_BUTTON_PADDING;
+
+ if (UIRectangleContains(tab, element->window->cursorX, element->window->cursorY)) {
+ tabPane->active = index;
+ UIElementRelayout(element);
+ UIElementRepaint(element, NULL);
+ break;
+ }
+
+ tab.l = tab.r - 1;
+
+ if (tabPane->tabs[end] == '\t') {
+ position = end + 1;
+ index++;
+ } else {
+ break;
+ }
+ }
+ } else if (message == UI_MSG_LAYOUT) {
+ UIRectangle content = element->bounds;
+ content.t += UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+
+ for (uint32_t index = 0; index < element->childCount; index++) {
+ UIElement *child = element->children[index];
+
+ if (tabPane->active == index) {
+ child->flags &= ~UI_ELEMENT_HIDE;
+ UIElementMove(child, content, false);
+ UIElementMessage(child, UI_MSG_TAB_SELECTED, 0, 0);
+ } else {
+ child->flags |= UI_ELEMENT_HIDE;
+ }
+ }
+ } else if (message == UI_MSG_GET_HEIGHT) {
+ int baseHeight = UI_SIZE_BUTTON_HEIGHT * element->window->scale;
+
+ for (uint32_t index = 0; index < element->childCount; index++) {
+ UIElement *child = element->children[index];
+
+ if (tabPane->active == index) {
+ return baseHeight + UIElementMessage(child, UI_MSG_GET_HEIGHT, di, dp);
+ }
+ }
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(tabPane->tabs);
+ }
+
+ return 0;
+}
+
+UITabPane *UITabPaneCreate(UIElement *parent, uint32_t flags, const char *tabs) {
+ UITabPane *tabPane = (UITabPane *) UIElementCreate(sizeof(UITabPane), parent, flags, _UITabPaneMessage, "Tab Pane");
+ tabPane->tabs = UIStringCopy(tabs, -1);
+ return tabPane;
+}
+
+/////////////////////////////////////////
+// Spacers.
+/////////////////////////////////////////
+
+int _UISpacerMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UISpacer *spacer = (UISpacer *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return spacer->height * element->window->scale;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return spacer->width * element->window->scale;
+ }
+
+ return 0;
+}
+
+UISpacer *UISpacerCreate(UIElement *parent, uint32_t flags, int width, int height) {
+ UISpacer *spacer = (UISpacer *) UIElementCreate(sizeof(UISpacer), parent, flags, _UISpacerMessage, "Spacer");
+ spacer->width = width;
+ spacer->height = height;
+ return spacer;
+}
+
+/////////////////////////////////////////
+// Scroll bars.
+/////////////////////////////////////////
+
+int _UIScrollBarMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIScrollBar *scrollBar = (UIScrollBar *) element;
+
+ if (message == UI_MSG_GET_WIDTH || message == UI_MSG_GET_HEIGHT) {
+ return UI_SIZE_SCROLL_BAR * element->window->scale;
+ } else if (message == UI_MSG_LAYOUT) {
+ UIElement *up = element->children[0];
+ UIElement *thumb = element->children[1];
+ UIElement *down = element->children[2];
+
+ if (scrollBar->page >= scrollBar->maximum || scrollBar->maximum <= 0 || scrollBar->page <= 0) {
+ up->flags |= UI_ELEMENT_HIDE;
+ thumb->flags |= UI_ELEMENT_HIDE;
+ down->flags |= UI_ELEMENT_HIDE;
+
+ scrollBar->position = 0;
+ } else {
+ up->flags &= ~UI_ELEMENT_HIDE;
+ thumb->flags &= ~UI_ELEMENT_HIDE;
+ down->flags &= ~UI_ELEMENT_HIDE;
+
+ int size = scrollBar->horizontal ? UI_RECT_WIDTH(element->bounds) : UI_RECT_HEIGHT(element->bounds);
+ int thumbSize = size * scrollBar->page / scrollBar->maximum;
+
+ if (thumbSize < UI_SIZE_SCROLL_MINIMUM_THUMB * element->window->scale) {
+ thumbSize = UI_SIZE_SCROLL_MINIMUM_THUMB * element->window->scale;
+ }
+
+ if (scrollBar->position < 0) {
+ scrollBar->position = 0;
+ } else if (scrollBar->position > scrollBar->maximum - scrollBar->page) {
+ scrollBar->position = scrollBar->maximum - scrollBar->page;
+ }
+
+ int thumbPosition = scrollBar->position / (scrollBar->maximum - scrollBar->page) * (size - thumbSize);
+
+ if (scrollBar->position == scrollBar->maximum - scrollBar->page) {
+ thumbPosition = size - thumbSize;
+ }
+
+ if (scrollBar->horizontal) {
+ UIRectangle r = element->bounds;
+ r.r = r.l + thumbPosition;
+ UIElementMove(up, r, false);
+ r.l = r.r, r.r = r.l + thumbSize;
+ UIElementMove(thumb, r, false);
+ r.l = r.r, r.r = element->bounds.r;
+ UIElementMove(down, r, false);
+ } else {
+ UIRectangle r = element->bounds;
+ r.b = r.t + thumbPosition;
+ UIElementMove(up, r, false);
+ r.t = r.b, r.b = r.t + thumbSize;
+ UIElementMove(thumb, r, false);
+ r.t = r.b, r.b = element->bounds.b;
+ UIElementMove(down, r, false);
+ }
+ }
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_SCROLL_TRACK
+ | ((scrollBar->page >= scrollBar->maximum || scrollBar->maximum <= 0 || scrollBar->page <= 0) ? UI_DRAW_CONTROL_STATE_DISABLED : 0),
+ NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_MOUSE_WHEEL) {
+ scrollBar->position += di;
+ UIElementRefresh(element);
+ UIElementMessage(element->parent, UI_MSG_SCROLLED, 0, 0);
+ return 1;
+ }
+
+ return 0;
+}
+
+int _UIScrollUpDownMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIScrollBar *scrollBar = (UIScrollBar *) element->parent;
+ bool isDown = element->cp;
+
+ if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, (isDown ? UI_DRAW_CONTROL_SCROLL_DOWN : UI_DRAW_CONTROL_SCROLL_UP)
+ | (scrollBar->horizontal ? 0 : UI_DRAW_CONTROL_STATE_VERTICAL) | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ UIElementAnimate(element, false);
+ scrollBar->lastAnimateTime = UI_CLOCK();
+ } else if (message == UI_MSG_LEFT_UP) {
+ UIElementAnimate(element, true);
+ } else if (message == UI_MSG_ANIMATE) {
+ UI_CLOCK_T previous = scrollBar->lastAnimateTime;
+ UI_CLOCK_T current = UI_CLOCK();
+ UI_CLOCK_T delta = current - previous;
+ double deltaSeconds = (double) delta / UI_CLOCKS_PER_SECOND;
+ if (deltaSeconds > 0.1) deltaSeconds = 0.1;
+ double deltaPixels = deltaSeconds * scrollBar->page * 3;
+ scrollBar->lastAnimateTime = current;
+ if (isDown) scrollBar->position += deltaPixels;
+ else scrollBar->position -= deltaPixels;
+ UIElementRefresh(&scrollBar->e);
+ UIElementMessage(scrollBar->e.parent, UI_MSG_SCROLLED, 0, 0);
+ }
+
+ return 0;
+}
+
+int _UIScrollThumbMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIScrollBar *scrollBar = (UIScrollBar *) element->parent;
+
+ if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_SCROLL_THUMB
+ | (scrollBar->horizontal ? 0 : UI_DRAW_CONTROL_STATE_VERTICAL)
+ | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element), NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_MOUSE_DRAG && element->window->pressedButton == 1) {
+ if (!scrollBar->inDrag) {
+ scrollBar->inDrag = true;
+
+ if (scrollBar->horizontal) {
+ scrollBar->dragOffset = element->bounds.l - scrollBar->e.bounds.l - element->window->cursorX;
+ } else {
+ scrollBar->dragOffset = element->bounds.t - scrollBar->e.bounds.t - element->window->cursorY;
+ }
+ }
+
+ int thumbPosition = (scrollBar->horizontal ? element->window->cursorX : element->window->cursorY) + scrollBar->dragOffset;
+ int size = scrollBar->horizontal ? (UI_RECT_WIDTH(scrollBar->e.bounds) - UI_RECT_WIDTH(element->bounds))
+ : (UI_RECT_HEIGHT(scrollBar->e.bounds) - UI_RECT_HEIGHT(element->bounds));
+ scrollBar->position = (double) thumbPosition / size * (scrollBar->maximum - scrollBar->page);
+ UIElementRefresh(&scrollBar->e);
+ UIElementMessage(scrollBar->e.parent, UI_MSG_SCROLLED, 0, 0);
+ } else if (message == UI_MSG_LEFT_UP) {
+ scrollBar->inDrag = false;
+ }
+
+ return 0;
+}
+
+UIScrollBar *UIScrollBarCreate(UIElement *parent, uint32_t flags) {
+ UIScrollBar *scrollBar = (UIScrollBar *) UIElementCreate(sizeof(UIScrollBar), parent, flags, _UIScrollBarMessage, "Scroll Bar");
+ bool horizontal = scrollBar->horizontal = flags & UI_SCROLL_BAR_HORIZONTAL;
+ UIElementCreate(sizeof(UIElement), &scrollBar->e, flags, _UIScrollUpDownMessage, !horizontal ? "Scroll Up" : "Scroll Left")->cp = (void *) (uintptr_t) 0;
+ UIElementCreate(sizeof(UIElement), &scrollBar->e, flags, _UIScrollThumbMessage, "Scroll Thumb");
+ UIElementCreate(sizeof(UIElement), &scrollBar->e, flags, _UIScrollUpDownMessage, !horizontal ? "Scroll Down" : "Scroll Right")->cp = (void *) (uintptr_t) 1;
+ return scrollBar;
+}
+
+/////////////////////////////////////////
+// Code views.
+/////////////////////////////////////////
+
+bool _UICharIsDigit(int c) {
+ return c >= '0' && c <= '9';
+}
+
+bool _UICharIsAlpha(int c) {
+ return (
+ ('A' <= c && c <= 'Z') ||
+ ('a' <= c && c <= 'z') ||
+ c > 127
+ );
+}
+
+bool _UICharIsAlphaOrDigitOrUnderscore(int c) {
+ return _UICharIsAlpha(c) || _UICharIsDigit(c) || c == '_';
+}
+
+int _UICodeByteToColumn(UICode *code, int line, int byte) {
+ return _UIByteToColumn(&code->content[code->lines[line].offset], byte, code->lines[line].bytes, code->tabSize);
+}
+
+int _UICodeColumnToByte(UICode *code, int line, int column) {
+ return _UIColumnToByte(&code->content[code->lines[line].offset], column, code->lines[line].bytes, code->tabSize);
+}
+
+void UICodePositionToByte(UICode *code, int x, int y, int *line, int *byte) {
+ UIFont *previousFont = UIFontActivate(code->font);
+ int lineHeight = UIMeasureStringHeight();
+ *line = (y - code->e.bounds.t + code->vScroll->position) / lineHeight;
+ if (*line < 0) *line = 0;
+ else if (*line >= code->lineCount) *line = code->lineCount - 1;
+ int column = (x - code->e.bounds.l + code->hScroll->position + ui.activeFont->glyphWidth / 2) / ui.activeFont->glyphWidth;
+ if (~code->e.flags & UI_CODE_NO_MARGIN) column -= (UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP) / ui.activeFont->glyphWidth;
+ UIFontActivate(previousFont);
+ *byte = _UICodeColumnToByte(code, *line, column);
+}
+
+int UICodeHitTest(UICode *code, int x, int y) {
+ x -= code->e.bounds.l;
+
+ if (x < 0 || x >= code->vScroll->e.bounds.l) {
+ return 0;
+ }
+
+ y -= code->e.bounds.t - code->vScroll->position;
+
+ UIFont *previousFont = UIFontActivate(code->font);
+ int lineHeight = UIMeasureStringHeight();
+ bool inMargin = x < UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP / 2 && (~code->e.flags & UI_CODE_NO_MARGIN);
+ UIFontActivate(previousFont);
+
+ if (y < 0 || y >= lineHeight * code->lineCount) {
+ return 0;
+ }
+
+ int line = y / lineHeight + 1;
+ return inMargin ? -line : line;
+}
+
+int UIDrawStringHighlighted(UIPainter *painter, UIRectangle lineBounds, const char *string, ptrdiff_t bytes, int tabSize, UIStringSelection *selection) {
+ if (bytes == -1) bytes = _UIStringLength(string);
+ if (bytes > 10000) bytes = 10000;
+
+ typedef enum _UICodeTokenType {
+ UI_CODE_TOKEN_TYPE_DEFAULT,
+ UI_CODE_TOKEN_TYPE_COMMENT,
+ UI_CODE_TOKEN_TYPE_STRING,
+ UI_CODE_TOKEN_TYPE_NUMBER,
+ UI_CODE_TOKEN_TYPE_OPERATOR,
+ UI_CODE_TOKEN_TYPE_PREPROCESSOR,
+ } _UICodeTokenType;
+
+ uint32_t colors[] = {
+ ui.theme.codeDefault,
+ ui.theme.codeComment,
+ ui.theme.codeString,
+ ui.theme.codeNumber,
+ ui.theme.codeOperator,
+ ui.theme.codePreprocessor,
+ };
+
+ int lineHeight = UIMeasureStringHeight();
+ int x = lineBounds.l;
+ int y = (lineBounds.t + lineBounds.b - lineHeight) / 2;
+ int ti = 0;
+ _UICodeTokenType tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ bool inComment = false, inIdentifier = false, inChar = false, startedString = false, startedPreprocessor = false;
+ uint32_t last = 0;
+ int j = 0;
+
+ while (bytes) {
+#ifdef UI_UNICODE
+ ptrdiff_t bytesConsumed;
+ int c = Utf8GetCodePoint(string, bytes, &bytesConsumed);
+ UI_ASSERT(bytesConsumed > 0);
+ string += bytesConsumed;
+ bytes -= bytesConsumed;
+#else
+ char c = *string++;
+ bytes--;
+#endif
+
+ last <<= 8;
+ last |= c & 0xFF;
+
+ if (tokenType == UI_CODE_TOKEN_TYPE_PREPROCESSOR) {
+ if (bytes && c == '/' && (*string == '/' || *string == '*')) {
+ tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ }
+ } else if (tokenType == UI_CODE_TOKEN_TYPE_OPERATOR) {
+ tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ } else if (tokenType == UI_CODE_TOKEN_TYPE_COMMENT) {
+ if ((last & 0xFF0000) == ('*' << 16) && (last & 0xFF00) == ('/' << 8) && inComment) {
+ tokenType = startedPreprocessor ? UI_CODE_TOKEN_TYPE_PREPROCESSOR : UI_CODE_TOKEN_TYPE_DEFAULT;
+ inComment = false;
+ }
+ } else if (tokenType == UI_CODE_TOKEN_TYPE_NUMBER) {
+ if (!_UICharIsAlpha(c) && !_UICharIsDigit(c)) {
+ tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ }
+ } else if (tokenType == UI_CODE_TOKEN_TYPE_STRING) {
+ if (!startedString) {
+ if (!inChar && ((last >> 8) & 0xFF) == '"' && ((last >> 16) & 0xFF) != '\\') {
+ tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ } else if (inChar && ((last >> 8) & 0xFF) == '\'' && ((last >> 16) & 0xFF) != '\\') {
+ tokenType = UI_CODE_TOKEN_TYPE_DEFAULT;
+ }
+ }
+
+ startedString = false;
+ }
+
+ if (tokenType == UI_CODE_TOKEN_TYPE_DEFAULT) {
+ if (c == '#') {
+ tokenType = UI_CODE_TOKEN_TYPE_PREPROCESSOR;
+ startedPreprocessor = true;
+ } else if (bytes && c == '/' && *string == '/') {
+ tokenType = UI_CODE_TOKEN_TYPE_COMMENT;
+ } else if (bytes && c == '/' && *string == '*') {
+ tokenType = UI_CODE_TOKEN_TYPE_COMMENT, inComment = true;
+ } else if (c == '"') {
+ tokenType = UI_CODE_TOKEN_TYPE_STRING;
+ inChar = false;
+ startedString = true;
+ } else if (c == '\'') {
+ tokenType = UI_CODE_TOKEN_TYPE_STRING;
+ inChar = true;
+ startedString = true;
+ } else if (_UICharIsDigit(c) && !inIdentifier) {
+ tokenType = UI_CODE_TOKEN_TYPE_NUMBER;
+ } else if (!_UICharIsAlpha(c) && !_UICharIsDigit(c)) {
+ tokenType = UI_CODE_TOKEN_TYPE_OPERATOR;
+ inIdentifier = false;
+ } else {
+ inIdentifier = true;
+ }
+ }
+
+ int oldX = x;
+
+ if (c == '\t') {
+ x += ui.activeFont->glyphWidth, ti++;
+ while (ti % tabSize) x += ui.activeFont->glyphWidth, ti++, j++;
+ } else {
+ UIDrawGlyph(painter, x, y, c, colors[tokenType]);
+ x += ui.activeFont->glyphWidth, ti++;
+ }
+
+ if (selection && j >= selection->carets[0] && j < selection->carets[1]) {
+ UIDrawBlock(painter, UI_RECT_4(oldX, x, y, y + lineHeight), selection->colorBackground);
+ if (c != '\t') UIDrawGlyph(painter, oldX, y, c, selection->colorText);
+ }
+
+ if (selection && selection->carets[0] == j) {
+ UIDrawInvert(painter, UI_RECT_4(oldX, oldX + 1, y, y + lineHeight));
+ }
+
+ j++;
+ }
+
+ if (selection && selection->carets[0] == j) {
+ UIDrawInvert(painter, UI_RECT_4(x, x + 1, y, y + lineHeight));
+ }
+
+ return x;
+}
+
+void _UICodeUpdateSelection(UICode *code) {
+ bool swap = code->selection[3].line < code->selection[2].line
+ || (code->selection[3].line == code->selection[2].line && code->selection[3].offset < code->selection[2].offset);
+ code->selection[1 - swap] = code->selection[3];
+ code->selection[0 + swap] = code->selection[2];
+ code->moveScrollToCaretNextLayout = true;
+ UIElementRefresh(&code->e);
+}
+
+void _UICodeSetVerticalMotionColumn(UICode *code, bool restore) {
+ if (restore) {
+ code->selection[3].offset = _UICodeColumnToByte(code, code->selection[3].line, code->verticalMotionColumn);
+ } else if (!code->useVerticalMotionColumn) {
+ code->useVerticalMotionColumn = true;
+ code->verticalMotionColumn = _UICodeByteToColumn(code, code->selection[3].line, code->selection[3].offset);
+ }
+}
+
+void _UICodeCopyText(void *cp) {
+ UICode *code = (UICode *) cp;
+
+ int from = code->lines[code->selection[0].line].offset + code->selection[0].offset;
+ int to = code->lines[code->selection[1].line].offset + code->selection[1].offset;
+
+ if (from != to) {
+ char *pasteText = (char *) UI_CALLOC(to - from + 2);
+ for (int i = from; i < to; i++) pasteText[i - from] = code->content[i];
+ _UIClipboardWriteText(code->e.window, pasteText);
+ }
+}
+
+int _UICodeMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UICode *code = (UICode *) element;
+
+ if (message == UI_MSG_LAYOUT) {
+ UIFont *previousFont = UIFontActivate(code->font);
+ int scrollBarSize = UI_SIZE_SCROLL_BAR * code->e.window->scale;
+ code->vScroll->maximum = code->lineCount * UIMeasureStringHeight();
+ code->hScroll->maximum = code->columns * code->font->glyphWidth; // TODO This doesn't take into account tab sizes!
+ int vSpace = code->vScroll->page = UI_RECT_HEIGHT(element->bounds);
+ int hSpace = code->hScroll->page = UI_RECT_WIDTH(element->bounds);
+
+ if (code->moveScrollToCaretNextLayout) {
+ int top = code->selection[3].line * UIMeasureStringHeight();
+ int bottom = top + UIMeasureStringHeight();
+ int context = UIMeasureStringHeight() * 2;
+ if (bottom > code->vScroll->position + vSpace - context) code->vScroll->position = bottom - vSpace + context;
+ if (top < code->vScroll->position + context) code->vScroll->position = top - context;
+ code->moveScrollToCaretNextLayout = code->moveScrollToFocusNextLayout = false;
+ // TODO Horizontal scrolling.
+ } else if (code->moveScrollToFocusNextLayout) {
+ int lineHeight = UIMeasureStringHeight();
+ int viewHeight = UI_RECT_HEIGHT(code->e.bounds);
+
+ int padding = lineHeight*5;
+ int prevPos = code->vScroll->position;
+ int newPos = (code->focused + 0.5) * lineHeight - viewHeight / 2;
+
+ if (!code->centerExecutionPointer) {
+ if (newPos-prevPos > viewHeight/2 - padding) {
+ newPos = newPos - (viewHeight/2 - padding);
+ } else if (newPos-prevPos < -(viewHeight/2 - padding)) {
+ newPos = newPos +(viewHeight/2 - padding);
+ } else {
+ newPos = prevPos;
+ }
+ }
+
+ code->vScroll->position = newPos;
+ }
+
+ if (!(code->e.flags & UI_CODE_NO_MARGIN)) hSpace -= UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP;
+ _UI_LAYOUT_SCROLL_BAR_PAIR(code);
+
+ UIFontActivate(previousFont);
+ } else if (message == UI_MSG_PAINT) {
+ UIFont *previousFont = UIFontActivate(code->font);
+
+ UIPainter *painter = (UIPainter *) dp;
+ UIRectangle lineBounds = element->bounds;
+
+ lineBounds.r = code->vScroll->e.bounds.l;
+
+ if (~code->e.flags & UI_CODE_NO_MARGIN) {
+ lineBounds.l += UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP;
+ }
+
+ int lineHeight = UIMeasureStringHeight();
+ lineBounds.t -= (int64_t) code->vScroll->position % lineHeight;
+
+ UIDrawBlock(painter, element->bounds, ui.theme.codeBackground);
+
+#ifdef __cplusplus
+ UIStringSelection selection = {};
+#else
+ UIStringSelection selection = { 0 };
+#endif
+ selection.colorBackground = ui.theme.selected;
+ selection.colorText = ui.theme.textSelected;
+
+ for (int i = code->vScroll->position / lineHeight; i < code->lineCount; i++) {
+ if (lineBounds.t > element->clip.b) {
+ break;
+ }
+
+ lineBounds.b = lineBounds.t + lineHeight;
+
+ if (~code->e.flags & UI_CODE_NO_MARGIN) {
+ char string[16];
+ int p = 16;
+ int lineNumber = i + 1;
+
+ while (lineNumber) {
+ string[--p] = (lineNumber % 10) + '0';
+ lineNumber /= 10;
+ }
+
+ UIRectangle marginBounds = lineBounds;
+ marginBounds.r = marginBounds.l - UI_SIZE_CODE_MARGIN_GAP;
+ marginBounds.l -= UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP;
+
+ uint32_t marginColor = UIElementMessage(element, UI_MSG_CODE_GET_MARGIN_COLOR, i + 1, 0);
+
+ if (marginColor) {
+ UIDrawBlock(painter, marginBounds, marginColor);
+ }
+
+ UIDrawString(painter, marginBounds, string + p, 16 - p,
+ marginColor ? ui.theme.codeDefault : ui.theme.codeComment, UI_ALIGN_RIGHT, NULL);
+ }
+
+ if (code->focused == i) {
+ UIDrawBlock(painter, lineBounds, ui.theme.codeFocused);
+ }
+
+ UIRectangle oldClip = painter->clip;
+ painter->clip = UIRectangleIntersection(oldClip, lineBounds);
+ if (code->hScroll) lineBounds.l -= (int64_t) code->hScroll->position;
+ selection.carets[0] = i == code->selection[0].line ? _UICodeByteToColumn(code, i, code->selection[0].offset) : 0;
+ selection.carets[1] = i == code->selection[1].line ? _UICodeByteToColumn(code, i, code->selection[1].offset) : code->lines[i].bytes;
+ int x = UIDrawStringHighlighted(painter, lineBounds, code->content + code->lines[i].offset, code->lines[i].bytes, code->tabSize,
+ element->window->focused == element && i >= code->selection[0].line && i <= code->selection[1].line ? &selection : NULL);
+ int y = (lineBounds.t + lineBounds.b - UIMeasureStringHeight()) / 2;
+
+ if (element->window->focused == element && i >= code->selection[0].line && i < code->selection[1].line) {
+ UIDrawBlock(painter, UI_RECT_4PD(x, y, code->font->glyphWidth, code->font->glyphHeight), selection.colorBackground);
+ }
+
+ if (code->hScroll) lineBounds.l += (int64_t) code->hScroll->position;
+ painter->clip = oldClip;
+
+ UICodeDecorateLine m;
+ m.x = x, m.y = y, m.bounds = lineBounds, m.index = i + 1, m.painter = painter;
+ UIElementMessage(element, UI_MSG_CODE_DECORATE_LINE, 0, &m);
+
+ lineBounds.t += lineHeight;
+ }
+
+ UIFontActivate(previousFont);
+ } else if (message == UI_MSG_SCROLLED) {
+ code->moveScrollToFocusNextLayout = false;
+ UIElementRefresh(element);
+ } else if (message == UI_MSG_MOUSE_WHEEL) {
+ return UIElementMessage(&code->vScroll->e, message, di, dp);
+ } else if (message == UI_MSG_GET_CURSOR) {
+ if (UICodeHitTest(code, element->window->cursorX, element->window->cursorY) < 0) {
+ return UI_CURSOR_FLIPPED_ARROW;
+ }
+
+ if (element->flags & UI_CODE_SELECTABLE) {
+ return UI_CURSOR_TEXT;
+ }
+ } else if (message == UI_MSG_LEFT_UP) {
+ UIElementAnimate(element, true);
+ } else if (message == UI_MSG_LEFT_DOWN && code->lineCount) {
+ int hitTest = UICodeHitTest(code, element->window->cursorX, element->window->cursorY);
+ code->leftDownInMargin = hitTest < 0;
+
+ if (hitTest > 0 && (element->flags & UI_CODE_SELECTABLE)) {
+ UICodePositionToByte(code, element->window->cursorX, element->window->cursorY, &code->selection[2].line, &code->selection[2].offset);
+ _UICodeMessage(element, UI_MSG_MOUSE_DRAG, di, dp);
+ UIElementFocus(element);
+ UIElementAnimate(element, false);
+ code->lastAnimateTime = UI_CLOCK();
+ }
+ } else if (message == UI_MSG_ANIMATE) {
+ if (element->window->pressed == element && element->window->pressedButton == 1 && code->lineCount && !code->leftDownInMargin) {
+ UI_CLOCK_T previous = code->lastAnimateTime;
+ UI_CLOCK_T current = UI_CLOCK();
+ UI_CLOCK_T deltaTicks = current - previous;
+ double deltaSeconds = (double) deltaTicks / UI_CLOCKS_PER_SECOND;
+ if (deltaSeconds > 0.1) deltaSeconds = 0.1;
+ int delta = deltaSeconds * 800;
+ if (!delta) { return 0; }
+ code->lastAnimateTime = current;
+
+ UIFont *previousFont = UIFontActivate(code->font);
+
+ if (element->window->cursorX < element->bounds.l + ((element->flags & UI_CODE_NO_MARGIN)
+ ? UI_SIZE_CODE_MARGIN_GAP : (UI_SIZE_CODE_MARGIN + UI_SIZE_CODE_MARGIN_GAP * 2))) {
+ code->hScroll->position -= delta;
+ } else if (element->window->cursorX >= code->vScroll->e.bounds.l - UI_SIZE_CODE_MARGIN_GAP) {
+ code->hScroll->position += delta;
+ }
+
+ if (element->window->cursorY < element->bounds.t + UI_SIZE_CODE_MARGIN_GAP) {
+ code->vScroll->position -= delta;
+ } else if (element->window->cursorY >= code->hScroll->e.bounds.t - UI_SIZE_CODE_MARGIN_GAP) {
+ code->vScroll->position += delta;
+ }
+
+ code->moveScrollToFocusNextLayout = false;
+ UIFontActivate(previousFont);
+ _UICodeMessage(element, UI_MSG_MOUSE_DRAG, di, dp);
+ UIElementRefresh(element);
+ }
+ } else if (message == UI_MSG_MOUSE_DRAG && element->window->pressedButton == 1 && code->lineCount && !code->leftDownInMargin) {
+ // TODO Double-click and triple-click dragging for word and line granularity respectively.
+ UICodePositionToByte(code, element->window->cursorX, element->window->cursorY, &code->selection[3].line, &code->selection[3].offset);
+ _UICodeUpdateSelection(code);
+ code->moveScrollToFocusNextLayout = code->moveScrollToCaretNextLayout = false;
+ code->useVerticalMotionColumn = false;
+ } else if (message == UI_MSG_KEY_TYPED && code->lineCount) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if ((m->code == UI_KEYCODE_LETTER('C') || m->code == UI_KEYCODE_LETTER('X') || m->code == UI_KEYCODE_INSERT)
+ && element->window->ctrl && !element->window->alt && !element->window->shift) {
+ _UICodeCopyText(code);
+ } else if ((m->code == UI_KEYCODE_UP || m->code == UI_KEYCODE_DOWN || m->code == UI_KEYCODE_PAGE_UP || m->code == UI_KEYCODE_PAGE_DOWN)
+ && !element->window->ctrl && !element->window->alt) {
+ UIFont *previousFont = UIFontActivate(code->font);
+ int lineHeight = UIMeasureStringHeight();
+
+ if (element->window->shift) {
+ if (m->code == UI_KEYCODE_UP) {
+ if (code->selection[3].line - 1 >= 0) {
+ _UICodeSetVerticalMotionColumn(code, false);
+ code->selection[3].line--;
+ _UICodeSetVerticalMotionColumn(code, true);
+ }
+ } else if (m->code == UI_KEYCODE_DOWN) {
+ if (code->selection[3].line + 1 < code->lineCount) {
+ _UICodeSetVerticalMotionColumn(code, false);
+ code->selection[3].line++;
+ _UICodeSetVerticalMotionColumn(code, true);
+ }
+ } else if (m->code == UI_KEYCODE_PAGE_UP || m->code == UI_KEYCODE_PAGE_DOWN) {
+ _UICodeSetVerticalMotionColumn(code, false);
+ int pageHeight = (element->bounds.t - code->hScroll->e.bounds.t) / lineHeight * 4 / 5;
+ code->selection[3].line += m->code == UI_KEYCODE_PAGE_UP ? pageHeight : -pageHeight;
+ if (code->selection[3].line < 0) code->selection[3].line = 0;
+ if (code->selection[3].line >= code->lineCount) code->selection[3].line = code->lineCount - 1;
+ _UICodeSetVerticalMotionColumn(code, true);
+ }
+
+ _UICodeUpdateSelection(code);
+ } else {
+ code->moveScrollToFocusNextLayout = false;
+ _UI_KEY_INPUT_VSCROLL(code, lineHeight, (element->bounds.t - code->hScroll->e.bounds.t) * 4 / 5 /* leave a few lines for context */);
+ }
+
+ UIFontActivate(previousFont);
+ } else if ((m->code == UI_KEYCODE_HOME || m->code == UI_KEYCODE_END) && !element->window->alt) {
+ if (element->window->shift) {
+ if (m->code == UI_KEYCODE_HOME) {
+ if (element->window->ctrl) code->selection[3].line = 0;
+ code->selection[3].offset = 0;
+ code->useVerticalMotionColumn = false;
+ } else {
+ if (element->window->ctrl) code->selection[3].line = code->lineCount - 1;
+ code->selection[3].offset = code->lines[code->selection[3].line].bytes;
+ code->useVerticalMotionColumn = false;
+ }
+
+ _UICodeUpdateSelection(code);
+ } else {
+ code->vScroll->position = m->code == UI_KEYCODE_HOME ? 0 : code->vScroll->maximum;
+ code->moveScrollToFocusNextLayout = false;
+ UIElementRefresh(&code->e);
+ }
+ } else if ((m->code == UI_KEYCODE_LEFT || m->code == UI_KEYCODE_RIGHT) && !element->window->alt) {
+ if (element->window->shift) {
+ UICodeMoveCaret(code, m->code == UI_KEYCODE_LEFT, element->window->ctrl);
+ } else if (!element->window->ctrl) {
+ code->hScroll->position += m->code == UI_KEYCODE_LEFT ? -ui.activeFont->glyphWidth : ui.activeFont->glyphWidth;
+ UIElementRefresh(&code->e);
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+
+ return 1;
+ } else if (message == UI_MSG_RIGHT_DOWN) {
+ int hitTest = UICodeHitTest(code, element->window->cursorX, element->window->cursorY);
+
+ if (hitTest > 0 && (element->flags & UI_CODE_SELECTABLE)) {
+ UIElementFocus(element);
+ UIMenu *menu = UIMenuCreate(&element->window->e, UI_MENU_NO_SCROLL);
+ UIMenuAddItem(menu, (code->selection[0].line == code->selection[1].line
+ && code->selection[0].offset == code->selection[1].offset) ? UI_ELEMENT_DISABLED : 0, "Copy", -1, _UICodeCopyText, code);
+ UIMenuShow(menu);
+ }
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(code->content);
+ UI_FREE(code->lines);
+ }
+
+ return 0;
+}
+
+void UICodeMoveCaret(UICode *code, bool backward, bool word) {
+ while (true) {
+ if (backward) {
+ if (code->selection[3].offset - 1 < 0) {
+ if (code->selection[3].line > 0) {
+ code->selection[3].line--;
+ code->selection[3].offset = code->lines[code->selection[3].line].bytes;
+ } else break;
+ } else _UI_MOVE_CARET_BACKWARD(code->selection[3].offset, code->content, code->lines[code->selection[3].line].offset + code->selection[3].offset, code->lines[code->selection[3].line].offset);
+ } else {
+ if (code->selection[3].offset + 1 > code->lines[code->selection[3].line].bytes) {
+ if (code->selection[3].line + 1 < code->lineCount) {
+ code->selection[3].line++;
+ code->selection[3].offset = 0;
+ } else break;
+ } else _UI_MOVE_CARET_FORWARD(code->selection[3].offset, code->content, code->contentBytes, code->lines[code->selection[3].line].offset + code->selection[3].offset);
+ }
+
+ if (!word) break;
+
+ if (code->selection[3].offset != 0 && code->selection[3].offset != code->lines[code->selection[3].line].bytes) {
+ _UI_MOVE_CARET_BY_WORD(code->content, code->contentBytes, code->lines[code->selection[3].line].offset + code->selection[3].offset);
+ }
+ }
+
+ code->useVerticalMotionColumn = false;
+ _UICodeUpdateSelection(code);
+}
+
+void UICodeFocusLine(UICode *code, int index) {
+ code->focused = index - 1;
+ code->moveScrollToFocusNextLayout = true;
+ UIElementRefresh(&code->e);
+}
+
+void UICodeInsertContent(UICode *code, const char *content, ptrdiff_t byteCount, bool replace) {
+ code->useVerticalMotionColumn = false;
+
+ UIFont *previousFont = UIFontActivate(code->font);
+
+ if (byteCount == -1) {
+ byteCount = _UIStringLength(content);
+ }
+
+ if (byteCount > 1000000000) {
+ byteCount = 1000000000;
+ }
+
+ if (replace) {
+ UI_FREE(code->content);
+ UI_FREE(code->lines);
+ code->content = NULL;
+ code->lines = NULL;
+ code->contentBytes = 0;
+ code->lineCount = 0;
+ code->columns = 0;
+ code->selection[0].line = code->selection[1].line = 0;
+ code->selection[0].offset = code->selection[1].offset = 0;
+ }
+
+ code->content = (char *) UI_REALLOC(code->content, code->contentBytes + byteCount);
+
+ if (!byteCount) {
+ return;
+ }
+
+ int lineCount = content[byteCount - 1] != '\n';
+
+ for (int i = 0; i < byteCount; i++) {
+ code->content[i + code->contentBytes] = content[i];
+
+ if (content[i] == '\n') {
+ lineCount++;
+ }
+ }
+
+ code->lines = (UICodeLine *) UI_REALLOC(code->lines, sizeof(UICodeLine) * (code->lineCount + lineCount));
+ int offset = 0, lineIndex = 0;
+
+ for (intptr_t i = 0; i <= byteCount && lineIndex < lineCount; i++) {
+ if (content[i] == '\n' || i == byteCount) {
+ UICodeLine line = { 0 };
+ line.offset = offset + code->contentBytes;
+ line.bytes = i - offset;
+ if (line.bytes > code->columns) code->columns = line.bytes;
+ code->lines[code->lineCount + lineIndex] = line;
+ lineIndex++;
+ offset = i + 1;
+ }
+ }
+
+ code->lineCount += lineCount;
+ code->contentBytes += byteCount;
+
+ if (!replace) {
+ code->vScroll->position = code->lineCount * UIMeasureStringHeight();
+ }
+
+ UIFontActivate(previousFont);
+ UIElementRepaint(&code->e, NULL);
+}
+
+UICode *UICodeCreate(UIElement *parent, uint32_t flags) {
+ UICode *code = (UICode *) UIElementCreate(sizeof(UICode), parent, flags, _UICodeMessage, "Code");
+ code->font = ui.activeFont;
+ code->vScroll = UIScrollBarCreate(&code->e, 0);
+ code->hScroll = UIScrollBarCreate(&code->e, UI_SCROLL_BAR_HORIZONTAL);
+ code->focused = -1;
+ code->tabSize = 4;
+ return code;
+}
+
+/////////////////////////////////////////
+// Gauges.
+/////////////////////////////////////////
+
+int _UIGaugeMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIGauge *gauge = (UIGauge *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return UI_SIZE_GAUGE_HEIGHT * element->window->scale;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return UI_SIZE_GAUGE_WIDTH * element->window->scale;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_GAUGE | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ NULL, 0, gauge->position, element->window->scale);
+ }
+
+ return 0;
+}
+
+void UIGaugeSetPosition(UIGauge *gauge, float position) {
+ if (position == gauge->position) return;
+ gauge->position = position;
+ UIElementRepaint(&gauge->e, NULL);
+}
+
+UIGauge *UIGaugeCreate(UIElement *parent, uint32_t flags) {
+ return (UIGauge *) UIElementCreate(sizeof(UIGauge), parent, flags, _UIGaugeMessage, "Gauge");
+}
+
+/////////////////////////////////////////
+// Sliders.
+/////////////////////////////////////////
+
+int _UISliderMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UISlider *slider = (UISlider *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return UI_SIZE_SLIDER_HEIGHT * element->window->scale;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return UI_SIZE_SLIDER_WIDTH * element->window->scale;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_SLIDER | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ NULL, 0, slider->position, element->window->scale);
+ } else if (message == UI_MSG_LEFT_DOWN || (message == UI_MSG_MOUSE_DRAG && element->window->pressedButton == 1)) {
+ UIRectangle bounds = element->bounds;
+ int thumbSize = UI_SIZE_SLIDER_THUMB * element->window->scale;
+ slider->position = (double) (element->window->cursorX - thumbSize / 2 - bounds.l) / (UI_RECT_WIDTH(bounds) - thumbSize);
+ if (slider->steps > 1) slider->position = (int) (slider->position * (slider->steps - 1) + 0.5f) / (double) (slider->steps - 1);
+ if (slider->position < 0) slider->position = 0;
+ if (slider->position > 1) slider->position = 1;
+ UIElementMessage(element, UI_MSG_VALUE_CHANGED, 0, 0);
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ }
+
+ return 0;
+}
+
+UISlider *UISliderCreate(UIElement *parent, uint32_t flags) {
+ return (UISlider *) UIElementCreate(sizeof(UISlider), parent, flags, _UISliderMessage, "Slider");
+}
+
+/////////////////////////////////////////
+// Tables.
+/////////////////////////////////////////
+
+int UITableHitTest(UITable *table, int x, int y) {
+ x -= table->e.bounds.l;
+
+ if (x < 0 || x >= table->vScroll->e.bounds.l) {
+ return -1;
+ }
+
+ y -= (table->e.bounds.t + UI_SIZE_TABLE_HEADER * table->e.window->scale) - table->vScroll->position;
+
+ int rowHeight = UI_SIZE_TABLE_ROW * table->e.window->scale;
+
+ if (y < 0 || y >= rowHeight * table->itemCount) {
+ return -1;
+ }
+
+ return y / rowHeight;
+}
+
+int UITableHeaderHitTest(UITable *table, int x, int y) {
+ if (!table->columnCount) return -1;
+ UIRectangle header = table->e.bounds;
+ header.b = header.t + UI_SIZE_TABLE_HEADER * table->e.window->scale;
+ header.l += UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+ int position = 0, index = 0;
+
+ while (true) {
+ int end = position;
+ for (; table->columns[end] != '\t' && table->columns[end]; end++);
+ header.r = header.l + table->columnWidths[index];
+ if (UIRectangleContains(header, x, y)) return index;
+ header.l += table->columnWidths[index] + UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+ if (table->columns[end] != '\t') break;
+ position = end + 1, index++;
+ }
+
+ return -1;
+}
+
+bool UITableEnsureVisible(UITable *table, int index) {
+ int rowHeight = UI_SIZE_TABLE_ROW * table->e.window->scale;
+ int y = index * rowHeight;
+ y -= table->vScroll->position;
+ int height = UI_RECT_HEIGHT(table->e.bounds) - UI_SIZE_TABLE_HEADER * table->e.window->scale - rowHeight;
+
+ if (y < 0) {
+ table->vScroll->position += y;
+ UIElementRefresh(&table->e);
+ return true;
+ } else if (y > height) {
+ table->vScroll->position -= height - y;
+ UIElementRefresh(&table->e);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void UITableResizeColumns(UITable *table) {
+ int position = 0;
+ int count = 0;
+
+ while (true) {
+ int end = position;
+ for (; table->columns[end] != '\t' && table->columns[end]; end++);
+ count++;
+ if (table->columns[end] == '\t') position = end + 1;
+ else break;
+ }
+
+ UI_FREE(table->columnWidths);
+ table->columnWidths = (int *) UI_MALLOC(count * sizeof(int));
+ table->columnCount = count;
+
+ position = 0;
+
+ char buffer[256];
+ UITableGetItem m = { 0 };
+ m.buffer = buffer;
+ m.bufferBytes = sizeof(buffer);
+
+ while (true) {
+ int end = position;
+ for (; table->columns[end] != '\t' && table->columns[end]; end++);
+
+ int longest = UIMeasureStringWidth(table->columns + position, end - position);
+
+ for (int i = 0; i < table->itemCount; i++) {
+ m.index = i;
+ int bytes = UIElementMessage(&table->e, UI_MSG_TABLE_GET_ITEM, 0, &m);
+ int width = UIMeasureStringWidth(buffer, bytes);
+
+ if (width > longest) {
+ longest = width;
+ }
+ }
+
+ table->columnWidths[m.column] = longest;
+ m.column++;
+ if (table->columns[end] == '\t') position = end + 1;
+ else break;
+ }
+
+ UIElementRepaint(&table->e, NULL);
+}
+
+int _UITableMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UITable *table = (UITable *) element;
+
+ if (message == UI_MSG_PAINT) {
+ UIPainter *painter = (UIPainter *) dp;
+ UIRectangle bounds = element->bounds;
+ bounds.r = table->vScroll->e.bounds.l;
+ UIDrawControl(painter, element->bounds, UI_DRAW_CONTROL_TABLE_BACKGROUND | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element), NULL, 0, 0, element->window->scale);
+ char buffer[256];
+ UIRectangle row = bounds;
+ int rowHeight = UI_SIZE_TABLE_ROW * element->window->scale;
+ UITableGetItem m = { 0 };
+ m.buffer = buffer;
+ m.bufferBytes = sizeof(buffer);
+ row.t += UI_SIZE_TABLE_HEADER * table->e.window->scale;
+ row.t -= (int64_t) table->vScroll->position % rowHeight;
+ int hovered = UITableHitTest(table, element->window->cursorX, element->window->cursorY);
+ UIRectangle oldClip = painter->clip;
+ painter->clip = UIRectangleIntersection(oldClip, UI_RECT_4(bounds.l, bounds.r,
+ bounds.t + (int) (UI_SIZE_TABLE_HEADER * element->window->scale), bounds.b));
+
+ for (int i = table->vScroll->position / rowHeight; i < table->itemCount; i++) {
+ if (row.t > painter->clip.b) {
+ break;
+ }
+
+ row.b = row.t + rowHeight;
+ m.index = i;
+ m.isSelected = false;
+ m.column = 0;
+ int bytes = UIElementMessage(element, UI_MSG_TABLE_GET_ITEM, 0, &m);
+
+ uint32_t rowFlags = (m.isSelected ? UI_DRAW_CONTROL_STATE_SELECTED : 0) | (hovered == i ? UI_DRAW_CONTROL_STATE_HOVERED : 0);
+ UIDrawControl(painter, row, UI_DRAW_CONTROL_TABLE_ROW | rowFlags, NULL, 0, 0, element->window->scale);
+
+ UIRectangle cell = row;
+ cell.l += UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale - (int64_t) table->hScroll->position;
+
+ for (int j = 0; j < table->columnCount; j++) {
+ if (j) {
+ m.column = j;
+ bytes = UIElementMessage(element, UI_MSG_TABLE_GET_ITEM, 0, &m);
+ }
+
+ cell.r = cell.l + table->columnWidths[j];
+ if ((size_t) bytes > m.bufferBytes && bytes > 0) bytes = m.bufferBytes;
+ UIDrawControl(painter, cell, UI_DRAW_CONTROL_TABLE_CELL | rowFlags, buffer, bytes, 0, element->window->scale);
+ cell.l += table->columnWidths[j] + UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+ }
+
+ row.t += rowHeight;
+ }
+
+ bounds = element->bounds;
+ painter->clip = UIRectangleIntersection(oldClip, bounds);
+ if (table->hScroll) bounds.l -= (int64_t) table->hScroll->position;
+
+ UIRectangle header = bounds;
+ header.b = header.t + UI_SIZE_TABLE_HEADER * table->e.window->scale;
+ header.l += UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+
+ int position = 0;
+ int index = 0;
+
+ if (table->columnCount) {
+ while (true) {
+ int end = position;
+ for (; table->columns[end] != '\t' && table->columns[end]; end++);
+
+ header.r = header.l + table->columnWidths[index];
+ UIDrawControl(painter, header, UI_DRAW_CONTROL_TABLE_HEADER | (index == table->columnHighlight ? UI_DRAW_CONTROL_STATE_SELECTED : 0),
+ table->columns + position, end - position, 0, element->window->scale);
+ header.l += table->columnWidths[index] + UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+
+ if (table->columns[end] == '\t') {
+ position = end + 1;
+ index++;
+ } else {
+ break;
+ }
+ }
+ }
+ } else if (message == UI_MSG_LAYOUT) {
+ int scrollBarSize = UI_SIZE_SCROLL_BAR * table->e.window->scale;
+ int columnGap = UI_SIZE_TABLE_COLUMN_GAP * table->e.window->scale;
+
+ table->vScroll->maximum = table->itemCount * UI_SIZE_TABLE_ROW * element->window->scale;
+ table->hScroll->maximum = columnGap;
+ for (int i = 0; i < table->columnCount; i++) { table->hScroll->maximum += table->columnWidths[i] + columnGap; }
+
+ int vSpace = table->vScroll->page = UI_RECT_HEIGHT(element->bounds) - UI_SIZE_TABLE_HEADER * element->window->scale;
+ int hSpace = table->hScroll->page = UI_RECT_WIDTH(element->bounds);
+ _UI_LAYOUT_SCROLL_BAR_PAIR(table);
+ } else if (message == UI_MSG_MOUSE_MOVE || message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_SCROLLED) {
+ UIElementRefresh(element);
+ } else if (message == UI_MSG_MOUSE_WHEEL) {
+ return UIElementMessage(&table->vScroll->e, message, di, dp);
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ UIElementFocus(element);
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if ((m->code == UI_KEYCODE_UP || m->code == UI_KEYCODE_DOWN || m->code == UI_KEYCODE_PAGE_UP || m->code == UI_KEYCODE_PAGE_DOWN
+ || m->code == UI_KEYCODE_HOME || m->code == UI_KEYCODE_END)
+ && !element->window->ctrl && !element->window->alt && !element->window->shift) {
+ _UI_KEY_INPUT_VSCROLL(table, UI_SIZE_TABLE_ROW * element->window->scale,
+ (element->bounds.t - table->hScroll->e.bounds.t + UI_SIZE_TABLE_HEADER) * 4 / 5);
+ return 1;
+ } else if ((m->code == UI_KEYCODE_LEFT || m->code == UI_KEYCODE_RIGHT)
+ && !element->window->ctrl && !element->window->alt && !element->window->shift) {
+ table->hScroll->position += m->code == UI_KEYCODE_LEFT ? -ui.activeFont->glyphWidth : ui.activeFont->glyphWidth;
+ UIElementRefresh(&table->e);
+ return 1;
+ }
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(table->columns);
+ UI_FREE(table->columnWidths);
+ }
+
+ return 0;
+}
+
+UITable *UITableCreate(UIElement *parent, uint32_t flags, const char *columns) {
+ UITable *table = (UITable *) UIElementCreate(sizeof(UITable), parent, flags, _UITableMessage, "Table");
+ table->vScroll = UIScrollBarCreate(&table->e, 0);
+ table->hScroll = UIScrollBarCreate(&table->e, UI_SCROLL_BAR_HORIZONTAL);
+ table->columns = UIStringCopy(columns, -1);
+ table->columnHighlight = -1;
+ return table;
+}
+
+/////////////////////////////////////////
+// Textboxes.
+/////////////////////////////////////////
+
+int _UITextboxByteToColumn(const char *string, int byte, ptrdiff_t bytes) {
+ return _UIByteToColumn(string, byte, bytes, 4);
+}
+
+int _UITextboxColumnToByte(const char *string, int column, ptrdiff_t bytes) {
+ return _UIColumnToByte(string, column, bytes, 4);
+}
+
+char *UITextboxToCString(UITextbox *textbox) {
+ char *buffer = (char *) UI_MALLOC(textbox->bytes + 1);
+
+ for (intptr_t i = 0; i < textbox->bytes; i++) {
+ buffer[i] = textbox->string[i];
+ }
+
+ buffer[textbox->bytes] = 0;
+ return buffer;
+}
+
+void UITextboxReplace(UITextbox *textbox, const char *text, ptrdiff_t bytes, bool sendChangedMessage) {
+ if (bytes == -1) bytes = _UIStringLength(text);
+ int deleteFrom = textbox->carets[0], deleteTo = textbox->carets[1];
+ if (deleteFrom > deleteTo) UI_SWAP(int, deleteFrom, deleteTo);
+
+ UI_MEMMOVE(&textbox->string[deleteFrom], &textbox->string[deleteTo], textbox->bytes - deleteTo);
+ textbox->bytes -= deleteTo - deleteFrom;
+ textbox->string = (char *) UI_REALLOC(textbox->string, textbox->bytes + bytes);
+ UI_MEMMOVE(&textbox->string[deleteFrom + bytes], &textbox->string[deleteFrom], textbox->bytes - deleteFrom);
+ UI_MEMMOVE(&textbox->string[deleteFrom], &text[0], bytes);
+ textbox->bytes += bytes;
+ textbox->carets[0] = deleteFrom + bytes;
+ textbox->carets[1] = textbox->carets[0];
+
+ if (sendChangedMessage) UIElementMessage(&textbox->e, UI_MSG_VALUE_CHANGED, 0, 0);
+ textbox->e.window->textboxModifiedFlag = true;
+ UIElementRepaint(&textbox->e, NULL);
+}
+
+void UITextboxClear(UITextbox *textbox, bool sendChangedMessage) {
+ textbox->carets[1] = 0;
+ textbox->carets[0] = textbox->bytes;
+ UITextboxReplace(textbox, "", 0, sendChangedMessage);
+}
+
+void UITextboxMoveCaret(UITextbox *textbox, bool backward, bool word) {
+ while (true) {
+ if (textbox->carets[0] > 0 && backward) {
+ _UI_MOVE_CARET_BACKWARD(textbox->carets[0], textbox->string, textbox->carets[0], 0);
+ } else if (textbox->carets[0] < textbox->bytes && !backward) {
+ _UI_MOVE_CARET_FORWARD(textbox->carets[0], textbox->string, textbox->bytes, textbox->carets[0]);
+ } else {
+ return;
+ }
+
+ if (!word) {
+ return;
+ } else if (textbox->carets[0] != textbox->bytes && textbox->carets[0] != 0) {
+ _UI_MOVE_CARET_BY_WORD(textbox->string, textbox->bytes, textbox->carets[0]);
+ }
+ }
+
+ UIElementRepaint(&textbox->e, NULL);
+}
+
+void _UITextboxCopyText(void *cp) {
+ UITextbox *textbox = (UITextbox *) cp;
+
+ int to = textbox->carets[0] > textbox->carets[1] ? textbox->carets[0] : textbox->carets[1];
+ int from = textbox->carets[0] < textbox->carets[1] ? textbox->carets[0] : textbox->carets[1];
+
+ if (from != to) {
+ char *pasteText = (char *) UI_CALLOC(to - from + 1);
+ for (int i = from; i < to; i++) pasteText[i - from] = textbox->string[i];
+ _UIClipboardWriteText(textbox->e.window, pasteText);
+ }
+}
+
+void _UITextboxPasteText(void *cp) {
+ UITextbox *textbox = (UITextbox *) cp;
+ size_t bytes;
+ char *text = _UIClipboardReadTextStart(textbox->e.window, &bytes);
+
+ if (text) {
+ for (size_t i = 0; i < bytes; i++) {
+ if (text[i] == '\n') text[i] = ' ';
+ }
+
+ UITextboxReplace(textbox, text, bytes, true);
+ }
+
+ _UIClipboardReadTextEnd(textbox->e.window, text);
+}
+
+int _UITextboxMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UITextbox *textbox = (UITextbox *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return UI_SIZE_TEXTBOX_HEIGHT * element->window->scale;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return UI_SIZE_TEXTBOX_WIDTH * element->window->scale;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_TEXTBOX | UI_DRAW_CONTROL_STATE_FROM_ELEMENT(element),
+ NULL, 0, 0, element->window->scale);
+
+ int scaledMargin = UI_SIZE_TEXTBOX_MARGIN * element->window->scale;
+ int totalWidth = UIMeasureStringWidth(textbox->string, textbox->bytes) + scaledMargin * 2;
+ UIRectangle textBounds = UIRectangleAdd(element->bounds, UI_RECT_1I(scaledMargin));
+
+ if (textbox->scroll > totalWidth - UI_RECT_WIDTH(textBounds)) {
+ textbox->scroll = totalWidth - UI_RECT_WIDTH(textBounds);
+ }
+
+ if (textbox->scroll < 0) {
+ textbox->scroll = 0;
+ }
+
+ int caretX = UIMeasureStringWidth(textbox->string, textbox->carets[0]) - textbox->scroll;
+
+ if (caretX < 0) {
+ textbox->scroll = caretX + textbox->scroll;
+ } else if (caretX > UI_RECT_WIDTH(textBounds)) {
+ textbox->scroll = caretX - UI_RECT_WIDTH(textBounds) + textbox->scroll + 1;
+ }
+
+#ifdef __cplusplus
+ UIStringSelection selection = {};
+#else
+ UIStringSelection selection = { 0 };
+#endif
+ selection.carets[0] = _UITextboxByteToColumn(textbox->string, textbox->carets[0], textbox->bytes);
+ selection.carets[1] = _UITextboxByteToColumn(textbox->string, textbox->carets[1], textbox->bytes);
+ selection.colorBackground = ui.theme.selected;
+ selection.colorText = ui.theme.textSelected;
+ textBounds.l -= textbox->scroll;
+
+ UIDrawString((UIPainter *) dp, textBounds, textbox->string, textbox->bytes,
+ (element->flags & UI_ELEMENT_DISABLED) ? ui.theme.textDisabled : ui.theme.text, UI_ALIGN_LEFT,
+ element->window->focused == element ? &selection : NULL);
+ } else if (message == UI_MSG_GET_CURSOR) {
+ return UI_CURSOR_TEXT;
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ int column = (element->window->cursorX - element->bounds.l + textbox->scroll - UI_SIZE_TEXTBOX_MARGIN * element->window->scale
+ + ui.activeFont->glyphWidth / 2) / ui.activeFont->glyphWidth;
+ textbox->carets[0] = textbox->carets[1] = column <= 0 ? 0 : _UITextboxColumnToByte(textbox->string, column, textbox->bytes);
+ UIElementFocus(element);
+ } else if (message == UI_MSG_UPDATE) {
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(textbox->string);
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+ bool handled = true;
+
+ if (textbox->rejectNextKey) {
+ textbox->rejectNextKey = false;
+ handled = false;
+ } else if (m->code == UI_KEYCODE_BACKSPACE || m->code == UI_KEYCODE_DELETE) {
+ if (textbox->carets[0] == textbox->carets[1]) {
+ UITextboxMoveCaret(textbox, m->code == UI_KEYCODE_BACKSPACE, element->window->ctrl);
+ }
+
+ UITextboxReplace(textbox, NULL, 0, true);
+ } else if (m->code == UI_KEYCODE_LEFT || m->code == UI_KEYCODE_RIGHT) {
+ if (textbox->carets[0] == textbox->carets[1] || element->window->shift) {
+ UITextboxMoveCaret(textbox, m->code == UI_KEYCODE_LEFT, element->window->ctrl);
+ if (!element->window->shift) textbox->carets[1] = textbox->carets[0];
+ } else {
+ textbox->carets[1 - element->window->shift] = textbox->carets[element->window->shift];
+ }
+ } else if (m->code == UI_KEYCODE_HOME || m->code == UI_KEYCODE_END) {
+ if (m->code == UI_KEYCODE_HOME) {
+ textbox->carets[0] = 0;
+ } else {
+ textbox->carets[0] = textbox->bytes;
+ }
+
+ if (!element->window->shift) {
+ textbox->carets[1] = textbox->carets[0];
+ }
+ } else if (m->code == UI_KEYCODE_LETTER('A') && element->window->ctrl) {
+ textbox->carets[1] = 0;
+ textbox->carets[0] = textbox->bytes;
+ } else if (m->textBytes && !element->window->alt && !element->window->ctrl && m->text[0] >= 0x20) {
+ UITextboxReplace(textbox, m->text, m->textBytes, true);
+ } else if ((m->code == UI_KEYCODE_LETTER('C') || m->code == UI_KEYCODE_LETTER('X') || m->code == UI_KEYCODE_INSERT)
+ && element->window->ctrl && !element->window->alt && !element->window->shift) {
+ _UITextboxCopyText(textbox);
+
+ if (m->code == UI_KEYCODE_LETTER('X')) {
+ UITextboxReplace(textbox, NULL, 0, true);
+ }
+ } else if ((m->code == UI_KEYCODE_LETTER('V') && element->window->ctrl && !element->window->alt && !element->window->shift)
+ || (m->code == UI_KEYCODE_INSERT && !element->window->ctrl && !element->window->alt && element->window->shift)) {
+ _UITextboxPasteText(textbox);
+ } else {
+ handled = false;
+ }
+
+ if (handled) {
+ UIElementRepaint(element, NULL);
+ return 1;
+ }
+ } else if (message == UI_MSG_RIGHT_DOWN) {
+ int c0 = textbox->carets[0], c1 = textbox->carets[1];
+ _UITextboxMessage(element, UI_MSG_LEFT_DOWN, di, dp);
+
+ if (c0 < c1 ? (textbox->carets[0] >= c0 && textbox->carets[0] < c1) : (textbox->carets[0] >= c1 && textbox->carets[0] < c0)) {
+ textbox->carets[0] = c0, textbox->carets[1] = c1; // Only move caret if clicking outside the existing selection.
+ }
+
+ UIMenu *menu = UIMenuCreate(&element->window->e, UI_MENU_NO_SCROLL);
+ UIMenuAddItem(menu, textbox->carets[0] == textbox->carets[1] ? UI_ELEMENT_DISABLED : 0, "Copy", -1, _UITextboxCopyText, textbox);
+ size_t pasteBytes;
+ char *paste = _UIClipboardReadTextStart(textbox->e.window, &pasteBytes);
+ UIMenuAddItem(menu, !paste || !pasteBytes ? UI_ELEMENT_DISABLED : 0, "Paste", -1, _UITextboxPasteText, textbox);
+ _UIClipboardReadTextEnd(textbox->e.window, paste);
+ UIMenuShow(menu);
+ }
+
+ return 0;
+}
+
+UITextbox *UITextboxCreate(UIElement *parent, uint32_t flags) {
+ return (UITextbox *) UIElementCreate(sizeof(UITextbox), parent, flags | UI_ELEMENT_TAB_STOP, _UITextboxMessage, "Textbox");
+}
+
+/////////////////////////////////////////
+// MDI clients.
+/////////////////////////////////////////
+
+int _UIMDIChildHitTest(UIMDIChild *mdiChild, int x, int y) {
+ UIElement *element = &mdiChild->e;
+ UI_MDI_CHILD_CALCULATE_LAYOUT(element->bounds, element->window->scale);
+ int cornerSize = UI_SIZE_MDI_CHILD_CORNER * element->window->scale;
+ if (!UIRectangleContains(element->bounds, x, y) || UIRectangleContains(content, x, y)) return -1;
+ else if (x < element->bounds.l + cornerSize && y < element->bounds.t + cornerSize) return 0b1010;
+ else if (x > element->bounds.r - cornerSize && y < element->bounds.t + cornerSize) return 0b0110;
+ else if (x < element->bounds.l + cornerSize && y > element->bounds.b - cornerSize) return 0b1001;
+ else if (x > element->bounds.r - cornerSize && y > element->bounds.b - cornerSize) return 0b0101;
+ else if (x < element->bounds.l + borderSize) return 0b1000;
+ else if (x > element->bounds.r - borderSize) return 0b0100;
+ else if (y < element->bounds.t + borderSize) return 0b0010;
+ else if (y > element->bounds.b - borderSize) return 0b0001;
+ else if (UIRectangleContains(title, x, y)) return 0b1111;
+ else return -1;
+}
+
+void _UIMDIChildCloseButton(void *_child) {
+ UIElement *child = (UIElement *) _child;
+
+ if (!UIElementMessage(child, UI_MSG_WINDOW_CLOSE, 0, 0)) {
+ UIElementDestroy(child);
+ UIElementRefresh(child->parent);
+ }
+}
+
+int _UIMDIChildMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIMDIChild *mdiChild = (UIMDIChild *) element;
+
+ if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_MDI_CHILD, mdiChild->title, mdiChild->titleBytes, 0, element->window->scale);
+ } else if (message == UI_MSG_GET_WIDTH) {
+ UIElement *child = element->childCount ? element->children[element->childCount - 1] : NULL;
+ int width = 2 * UI_SIZE_MDI_CHILD_BORDER;
+ width += (child ? UIElementMessage(child, message, di ? (di - UI_SIZE_MDI_CHILD_TITLE + UI_SIZE_MDI_CHILD_BORDER) : 0, dp) : 0);
+ if (width < UI_SIZE_MDI_CHILD_MINIMUM_WIDTH) width = UI_SIZE_MDI_CHILD_MINIMUM_WIDTH;
+ return width;
+ } else if (message == UI_MSG_GET_HEIGHT) {
+ UIElement *child = element->childCount ? element->children[element->childCount - 1] : NULL;
+ int height = UI_SIZE_MDI_CHILD_TITLE + UI_SIZE_MDI_CHILD_BORDER;
+ height += (child ? UIElementMessage(child, message, di ? (di - 2 * UI_SIZE_MDI_CHILD_BORDER) : 0, dp) : 0);
+ if (height < UI_SIZE_MDI_CHILD_MINIMUM_HEIGHT) height = UI_SIZE_MDI_CHILD_MINIMUM_HEIGHT;
+ return height;
+ } else if (message == UI_MSG_LAYOUT) {
+ UI_MDI_CHILD_CALCULATE_LAYOUT(element->bounds, element->window->scale);
+
+ int position = title.r;
+
+ for (uint32_t i = 0; i < element->childCount - 1; i++) {
+ UIElement *child = element->children[i];
+ int width = UIElementMessage(child, UI_MSG_GET_WIDTH, 0, 0);
+ UIElementMove(child, UI_RECT_4(position - width, position, title.t, title.b), false);
+ position -= width;
+ }
+
+ UIElement *child = element->childCount ? element->children[element->childCount - 1] : NULL;
+
+ if (child) {
+ UIElementMove(child, content, false);
+ }
+ } else if (message == UI_MSG_GET_CURSOR) {
+ int hitTest = _UIMDIChildHitTest(mdiChild, element->window->cursorX, element->window->cursorY);
+ if (hitTest == 0b1000) return UI_CURSOR_RESIZE_LEFT;
+ if (hitTest == 0b0010) return UI_CURSOR_RESIZE_UP;
+ if (hitTest == 0b0110) return UI_CURSOR_RESIZE_UP_RIGHT;
+ if (hitTest == 0b1010) return UI_CURSOR_RESIZE_UP_LEFT;
+ if (hitTest == 0b0100) return UI_CURSOR_RESIZE_RIGHT;
+ if (hitTest == 0b0001) return UI_CURSOR_RESIZE_DOWN;
+ if (hitTest == 0b1001) return UI_CURSOR_RESIZE_DOWN_LEFT;
+ if (hitTest == 0b0101) return UI_CURSOR_RESIZE_DOWN_RIGHT;
+ return UI_CURSOR_ARROW;
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ mdiChild->dragHitTest = _UIMDIChildHitTest(mdiChild, element->window->cursorX, element->window->cursorY);
+ mdiChild->dragOffset = UIRectangleAdd(element->bounds, UI_RECT_2(-element->window->cursorX, -element->window->cursorY));
+ } else if (message == UI_MSG_LEFT_UP) {
+ if (mdiChild->bounds.l < 0) mdiChild->bounds.r -= mdiChild->bounds.l, mdiChild->bounds.l = 0;
+ if (mdiChild->bounds.t < 0) mdiChild->bounds.b -= mdiChild->bounds.t, mdiChild->bounds.t = 0;
+ UIElementRefresh(element->parent);
+ } else if (message == UI_MSG_MOUSE_DRAG) {
+ if (mdiChild->dragHitTest > 0) {
+#define _UI_MDI_CHILD_MOVE_EDGE(bit, edge, cursor, size, opposite, negate, minimum, offset) \
+ if (mdiChild->dragHitTest & bit) mdiChild->bounds.edge = mdiChild->dragOffset.edge + element->window->cursor - element->parent->bounds.offset; \
+ if ((mdiChild->dragHitTest & bit) && size(mdiChild->bounds) < minimum) mdiChild->bounds.edge = mdiChild->bounds.opposite negate minimum;
+ _UI_MDI_CHILD_MOVE_EDGE(0b1000, l, cursorX, UI_RECT_WIDTH, r, -, UI_SIZE_MDI_CHILD_MINIMUM_WIDTH, l);
+ _UI_MDI_CHILD_MOVE_EDGE(0b0100, r, cursorX, UI_RECT_WIDTH, l, +, UI_SIZE_MDI_CHILD_MINIMUM_WIDTH, l);
+ _UI_MDI_CHILD_MOVE_EDGE(0b0010, t, cursorY, UI_RECT_HEIGHT, b, -, UI_SIZE_MDI_CHILD_MINIMUM_HEIGHT, t);
+ _UI_MDI_CHILD_MOVE_EDGE(0b0001, b, cursorY, UI_RECT_HEIGHT, t, +, UI_SIZE_MDI_CHILD_MINIMUM_HEIGHT, t);
+ UIElementRefresh(element->parent);
+ }
+ } else if (message == UI_MSG_DESTROY) {
+ UIMDIClient *client = (UIMDIClient *) element->parent;
+
+ if (client->active == mdiChild) {
+ client->active = (UIMDIChild *) (client->e.childCount == 1 ? NULL : client->e.children[client->e.childCount - 2]);
+ }
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(mdiChild->title);
+ }
+
+ return 0;
+}
+
+int _UIMDIClientMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIMDIClient *client = (UIMDIClient *) element;
+
+ if (message == UI_MSG_PAINT) {
+ if (~element->flags & UI_MDI_CLIENT_TRANSPARENT) {
+ UIDrawBlock((UIPainter *) dp, element->bounds, ui.theme.panel2);
+ }
+ } else if (message == UI_MSG_LAYOUT) {
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIMDIChild *mdiChild = (UIMDIChild *) element->children[i];
+ UI_ASSERT(mdiChild->e.messageClass == _UIMDIChildMessage);
+
+ if (UIRectangleEquals(mdiChild->bounds, UI_RECT_1(0))) {
+ int width = UIElementMessage(&mdiChild->e, UI_MSG_GET_WIDTH, 0, 0);
+ int height = UIElementMessage(&mdiChild->e, UI_MSG_GET_HEIGHT, width, 0);
+ if (client->cascade + width > element->bounds.r || client->cascade + height > element->bounds.b) client->cascade = 0;
+ mdiChild->bounds = UI_RECT_4(client->cascade, client->cascade + width, client->cascade, client->cascade + height);
+ client->cascade += UI_SIZE_MDI_CASCADE * element->window->scale;
+ }
+
+ UIRectangle bounds = UIRectangleAdd(mdiChild->bounds, UI_RECT_2(element->bounds.l, element->bounds.t));
+ UIElementMove(&mdiChild->e, bounds, false);
+ }
+ } else if (message == UI_MSG_PRESSED_DESCENDENT) {
+ UIMDIChild *child = (UIMDIChild *) dp;
+
+ if (child && child != client->active) {
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ if (element->children[i] == &child->e) {
+ UI_MEMMOVE(&element->children[i], &element->children[i + 1], sizeof(UIElement *) * (element->childCount - i - 1));
+ element->children[element->childCount - 1] = &child->e;
+ break;
+ }
+ }
+
+ client->active = child;
+ UIElementRefresh(element);
+ }
+ }
+
+ return 0;
+}
+
+UIMDIChild *UIMDIChildCreate(UIElement *parent, uint32_t flags, UIRectangle initialBounds, const char *title, ptrdiff_t titleBytes) {
+ UI_ASSERT(parent->messageClass == _UIMDIClientMessage);
+
+ UIMDIChild *mdiChild = (UIMDIChild *) UIElementCreate(sizeof(UIMDIChild), parent, flags, _UIMDIChildMessage, "MDIChild");
+ UIMDIClient *mdiClient = (UIMDIClient *) parent;
+
+ mdiChild->bounds = initialBounds;
+ mdiChild->title = UIStringCopy(title, (mdiChild->titleBytes = titleBytes));
+ mdiClient->active = mdiChild;
+
+ if (flags & UI_MDI_CHILD_CLOSE_BUTTON) {
+ UIButton *closeButton = UIButtonCreate(&mdiChild->e, UI_BUTTON_SMALL | UI_ELEMENT_NON_CLIENT, "X", 1);
+ closeButton->invoke = _UIMDIChildCloseButton;
+ closeButton->e.cp = mdiChild;
+ }
+
+ return mdiChild;
+}
+
+UIMDIClient *UIMDIClientCreate(UIElement *parent, uint32_t flags) {
+ return (UIMDIClient *) UIElementCreate(sizeof(UIMDIClient), parent, flags, _UIMDIClientMessage, "MDIClient");
+}
+
+/////////////////////////////////////////
+// Image displays.
+/////////////////////////////////////////
+
+void _UIImageDisplayUpdateViewport(UIImageDisplay *display) {
+ UIRectangle bounds = display->e.bounds;
+ bounds.r -= bounds.l, bounds.b -= bounds.t;
+
+ float minimumZoomX = 1, minimumZoomY = 1;
+ if (display->width > bounds.r) minimumZoomX = (float) bounds.r / display->width;
+ if (display->height > bounds.b) minimumZoomY = (float) bounds.b / display->height;
+ float minimumZoom = minimumZoomX < minimumZoomY ? minimumZoomX : minimumZoomY;
+
+ if (display->zoom < minimumZoom || (display->e.flags & _UI_IMAGE_DISPLAY_ZOOM_FIT)) {
+ display->zoom = minimumZoom;
+ display->e.flags |= _UI_IMAGE_DISPLAY_ZOOM_FIT;
+ }
+
+ if (display->panX < 0) display->panX = 0;
+ if (display->panY < 0) display->panY = 0;
+ if (display->panX > display->width - bounds.r / display->zoom) display->panX = display->width - bounds.r / display->zoom;
+ if (display->panY > display->height - bounds.b / display->zoom) display->panY = display->height - bounds.b / display->zoom;
+
+ if (bounds.r && display->width * display->zoom <= bounds.r) display->panX = display->width / 2 - bounds.r / display->zoom / 2;
+ if (bounds.b && display->height * display->zoom <= bounds.b) display->panY = display->height / 2 - bounds.b / display->zoom / 2;
+}
+
+int _UIImageDisplayMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIImageDisplay *display = (UIImageDisplay *) element;
+
+ if (message == UI_MSG_GET_HEIGHT) {
+ return display->height;
+ } else if (message == UI_MSG_GET_WIDTH) {
+ return display->width;
+ } else if (message == UI_MSG_DEALLOCATE) {
+ UI_FREE(display->bits);
+ } else if (message == UI_MSG_PAINT) {
+ UIPainter *painter = (UIPainter *) dp;
+
+ int w = UI_RECT_WIDTH(element->bounds), h = UI_RECT_HEIGHT(element->bounds);
+ int x = _UILinearMap(0, display->panX, display->panX + w / display->zoom, 0, w) + element->bounds.l;
+ int y = _UILinearMap(0, display->panY, display->panY + h / display->zoom, 0, h) + element->bounds.t;
+
+ UIRectangle image = UI_RECT_4(x, x + (int) (display->width * display->zoom), y, (int) (y + display->height * display->zoom));
+ UIRectangle bounds = UIRectangleIntersection(painter->clip, UIRectangleIntersection(display->e.bounds, image));
+ if (!UI_RECT_VALID(bounds)) return 0;
+
+ if (display->zoom == 1) {
+ uint32_t *lineStart = (uint32_t *) painter->bits + bounds.t * painter->width + bounds.l;
+ uint32_t *sourceLineStart = display->bits + (bounds.l - image.l) + display->width * (bounds.t - image.t);
+
+ for (int i = 0; i < bounds.b - bounds.t; i++, lineStart += painter->width, sourceLineStart += display->width) {
+ uint32_t *destination = lineStart;
+ uint32_t *source = sourceLineStart;
+ int j = bounds.r - bounds.l;
+
+ do {
+ *destination = *source;
+ destination++;
+ source++;
+ } while (--j);
+ }
+ } else {
+ float zr = 1.0f / display->zoom;
+ uint32_t *destination = (uint32_t *) painter->bits;
+
+ for (int i = bounds.t; i < bounds.b; i++) {
+ int ty = (i - image.t) * zr;
+
+ for (int j = bounds.l; j < bounds.r; j++) {
+ int tx = (j - image.l) * zr;
+ destination[i * painter->width + j] = display->bits[ty * display->width + tx];
+ }
+ }
+ }
+ } else if (message == UI_MSG_MOUSE_WHEEL && (element->flags & UI_IMAGE_DISPLAY_INTERACTIVE)) {
+ display->e.flags &= ~_UI_IMAGE_DISPLAY_ZOOM_FIT;
+ int divisions = -di / 72;
+ float factor = 1;
+ float perDivision = element->window->ctrl ? 2.0f : element->window->alt ? 1.01f : 1.2f;
+ while (divisions > 0) factor *= perDivision, divisions--;
+ while (divisions < 0) factor /= perDivision, divisions++;
+ if (display->zoom * factor > 64) factor = 64 / display->zoom;
+ int mx = element->window->cursorX - element->bounds.l;
+ int my = element->window->cursorY - element->bounds.t;
+ display->zoom *= factor;
+ display->panX -= mx / display->zoom * (1 - factor);
+ display->panY -= my / display->zoom * (1 - factor);
+ _UIImageDisplayUpdateViewport(display);
+ UIElementRepaint(&display->e, NULL);
+ } else if (message == UI_MSG_LAYOUT && (element->flags & UI_IMAGE_DISPLAY_INTERACTIVE)) {
+ UIRectangle bounds = display->e.bounds;
+ bounds.r -= bounds.l, bounds.b -= bounds.t;
+ display->panX -= (bounds.r - display->previousWidth ) / 2 / display->zoom;
+ display->panY -= (bounds.b - display->previousHeight) / 2 / display->zoom;
+ display->previousWidth = bounds.r, display->previousHeight = bounds.b;
+ _UIImageDisplayUpdateViewport(display);
+ } else if (message == UI_MSG_GET_CURSOR && (element->flags & UI_IMAGE_DISPLAY_INTERACTIVE)
+ && (UI_RECT_WIDTH(element->bounds) < display->width * display->zoom
+ || UI_RECT_HEIGHT(element->bounds) < display->height * display->zoom)) {
+ return UI_CURSOR_HAND;
+ } else if (message == UI_MSG_MOUSE_DRAG) {
+ display->panX -= (element->window->cursorX - display->previousPanPointX) / display->zoom;
+ display->panY -= (element->window->cursorY - display->previousPanPointY) / display->zoom;
+ _UIImageDisplayUpdateViewport(display);
+ display->previousPanPointX = element->window->cursorX;
+ display->previousPanPointY = element->window->cursorY;
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ display->e.flags &= ~_UI_IMAGE_DISPLAY_ZOOM_FIT;
+ display->previousPanPointX = element->window->cursorX;
+ display->previousPanPointY = element->window->cursorY;
+ }
+
+ return 0;
+}
+
+void UIImageDisplaySetContent(UIImageDisplay *display, uint32_t *bits, size_t width, size_t height, size_t stride) {
+ UI_FREE(display->bits);
+
+ display->bits = (uint32_t *) UI_MALLOC(width * height * 4);
+ display->width = width;
+ display->height = height;
+
+ uint32_t *destination = display->bits;
+ uint32_t *source = bits;
+
+ for (uintptr_t row = 0; row < height; row++, source += stride / 4) {
+ for (uintptr_t i = 0; i < width; i++) {
+ *destination++ = source[i];
+ }
+ }
+
+ UIElementMeasurementsChanged(&display->e, 3);
+ UIElementRepaint(&display->e, NULL);
+}
+
+UIImageDisplay *UIImageDisplayCreate(UIElement *parent, uint32_t flags, uint32_t *bits, size_t width, size_t height, size_t stride) {
+ UIImageDisplay *display = (UIImageDisplay *) UIElementCreate(sizeof(UIImageDisplay), parent, flags, _UIImageDisplayMessage, "ImageDisplay");
+ display->zoom = 1.0f;
+ UIImageDisplaySetContent(display, bits, width, height, stride);
+ return display;
+}
+
+/////////////////////////////////////////
+// Modal dialogs.
+/////////////////////////////////////////
+
+int _UIDialogWrapperMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_LAYOUT) {
+ int width = UIElementMessage(element->children[0], UI_MSG_GET_WIDTH, 0, 0);
+ int height = UIElementMessage(element->children[0], UI_MSG_GET_HEIGHT, width, 0);
+ int cx = (element->bounds.l + element->bounds.r) / 2;
+ int cy = (element->bounds.t + element->bounds.b) / 2;
+ UIRectangle bounds = UI_RECT_4(cx - (width + 1) / 2, cx + width / 2, cy - (height + 1) / 2, cy + height / 2);
+ UIElementMove(element->children[0], bounds, false);
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->children[0]->bounds, UI_DRAW_CONTROL_MODAL_POPUP, NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *typed = (UIKeyTyped *) dp;
+
+ if (element->window->ctrl) return 0;
+ if (element->window->shift) return 0;
+
+ if (!ui.dialogCanExit) {
+ } else if (!element->window->alt && typed->code == UI_KEYCODE_ESCAPE) {
+ ui.dialogResult = "__C";
+ return 1;
+ } else if (!element->window->alt && typed->code == UI_KEYCODE_ENTER) {
+ ui.dialogResult = "__D";
+ return 1;
+ }
+
+ char c0 = 0, c1 = 0;
+
+ if (typed->textBytes == 1 && typed->text[0] >= 'a' && typed->text[0] <= 'z') {
+ c0 = typed->text[0], c1 = typed->text[0] - 'a' + 'A';
+ } else {
+ return 0;
+ }
+
+ UIElement *rowContainer = element->children[0];
+ UIElement *target = NULL;
+ bool duplicate = false;
+
+ for (uint32_t i = 0; i < rowContainer->childCount; i++) {
+ for (uint32_t j = 0; j < rowContainer->children[i]->childCount; j++) {
+ UIElement *item = rowContainer->children[i]->children[j];
+
+ if (item->messageClass == _UIButtonMessage) {
+ UIButton *button = (UIButton *) item;
+
+ if (button->label && button->labelBytes && (button->label[0] == c0 || button->label[0] == c1)) {
+ if (!target) {
+ target = &button->e;
+ } else {
+ duplicate = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (target) {
+ if (duplicate) {
+ UIElementFocus(target);
+ } else {
+ UIElementMessage(target, UI_MSG_CLICKED, 0, 0);
+ }
+
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+void _UIDialogButtonInvoke(void *cp) {
+ ui.dialogResult = (const char *) cp;
+}
+
+int _UIDialogDefaultButtonMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_PAINT && element->window->focused->messageClass != _UIButtonMessage) {
+ element->flags |= UI_BUTTON_CHECKED;
+ element->messageClass(element, message, di, dp);
+ element->flags &= ~UI_BUTTON_CHECKED;
+ return 1;
+ }
+
+ return 0;
+}
+
+int _UIDialogTextboxMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UITextbox *textbox = (UITextbox *) element;
+
+ if (message == UI_MSG_VALUE_CHANGED) {
+ char **buffer = (char **) element->cp;
+ *buffer = (char *) UI_REALLOC(*buffer, textbox->bytes + 1);
+ (*buffer)[textbox->bytes] = 0;
+
+ for (ptrdiff_t i = 0; i < textbox->bytes; i++) {
+ (*buffer)[i] = textbox->string[i];
+ }
+ } else if (message == UI_MSG_UPDATE && di == UI_UPDATE_FOCUSED && element->window->focused == element) {
+ textbox->carets[1] = 0;
+ textbox->carets[0] = textbox->bytes;
+ UIElementRepaint(element, NULL);
+ }
+
+ return 0;
+}
+
+const char *UIDialogShow(UIWindow *window, uint32_t flags, const char *format, ...) {
+ // Create the dialog wrapper and panel.
+
+ UI_ASSERT(!window->dialog);
+ window->dialog = UIElementCreate(sizeof(UIElement), &window->e, 0, _UIDialogWrapperMessage, "DialogWrapper");
+ UIPanel *panel = UIPanelCreate(window->dialog, UI_PANEL_MEDIUM_SPACING | UI_PANEL_COLOR_1);
+ panel->border = UI_RECT_1(UI_SIZE_PANE_MEDIUM_BORDER * 2);
+ window->e.children[0]->flags |= UI_ELEMENT_DISABLED;
+
+ // Create the dialog contents.
+
+ va_list arguments;
+ va_start(arguments, format);
+ UIPanel *row = NULL;
+ UIElement *focus = NULL;
+ UIButton *defaultButton = NULL;
+ UIButton *cancelButton = NULL;
+ uint32_t buttonCount = 0;
+
+ for (int i = 0; format[i]; i++) {
+ if (i == 0 || format[i - 1] == '\n') {
+ row = UIPanelCreate(&panel->e, UI_PANEL_HORIZONTAL | UI_ELEMENT_H_FILL);
+ row->gap = UI_SIZE_PANE_SMALL_GAP;
+ }
+
+ if (format[i] == ' ' || format[i] == '\n') {
+ } else if (format[i] == '%') {
+ i++;
+
+ if (format[i] == 'b' /* button */ || format[i] == 'B' /* default button */ || format[i] == 'C' /* cancel button */) {
+ const char *label = va_arg(arguments, const char *);
+ UIButton *button = UIButtonCreate(&row->e, 0, label, -1);
+ if (!focus) focus = &button->e;
+ if (format[i] == 'B') defaultButton = button;
+ if (format[i] == 'C') cancelButton = button;
+ buttonCount++;
+ button->invoke = _UIDialogButtonInvoke;
+ if (format[i] == 'B') button->e.messageUser = _UIDialogDefaultButtonMessage;
+ button->e.cp = (void *) label;
+ } else if (format[i] == 's' /* label from string */) {
+ const char *label = va_arg(arguments, const char *);
+ UILabelCreate(&row->e, 0, label, -1);
+ } else if (format[i] == 't' /* textbox */) {
+ char **buffer = va_arg(arguments, char **);
+ UITextbox *textbox = UITextboxCreate(&row->e, UI_ELEMENT_H_FILL);
+ if (!focus) focus = &textbox->e;
+ if (*buffer) UITextboxReplace(textbox, *buffer, _UIStringLength(*buffer), false);
+ textbox->e.cp = buffer;
+ textbox->e.messageUser = _UIDialogTextboxMessage;
+ } else if (format[i] == 'f' /* horizontal fill */) {
+ UISpacerCreate(&row->e, UI_ELEMENT_H_FILL, 0, 0);
+ } else if (format[i] == 'l' /* horizontal line */) {
+ UISpacerCreate(&row->e, UI_ELEMENT_BORDER | UI_ELEMENT_H_FILL, 0, 1);
+ } else if (format[i] == 'u' /* user */) {
+ UIDialogUserCallback callback = va_arg(arguments, UIDialogUserCallback);
+ callback(&row->e);
+ }
+ } else {
+ int j = i;
+ while (format[j] && format[j] != '%' && format[j] != '\n') j++;
+ UILabelCreate(&row->e, 0, format + i, j - i);
+ i = j - 1;
+ }
+ }
+
+ va_end(arguments);
+
+ window->dialogOldFocus = window->focused;
+ UIElementFocus(focus ? focus : window->dialog);
+
+ // Run the modal message loop.
+
+ int result;
+ ui.dialogResult = NULL;
+ ui.dialogCanExit = buttonCount != 0;
+ for (int i = 1; i <= 3; i++) _UIWindowSetPressed(window, NULL, i);
+ UIElementRefresh(&window->e);
+ _UIUpdate();
+ while (!ui.dialogResult && _UIMessageLoopSingle(&result));
+ ui.quit = !ui.dialogResult;
+
+ // Check for cancel/default action.
+
+ if (buttonCount == 1 && defaultButton && !cancelButton) {
+ cancelButton = defaultButton;
+ }
+
+ if (!ui.dialogResult) {
+ } else if (ui.dialogResult[0] == '_' && ui.dialogResult[1] == '_' && ui.dialogResult[2] == 'C' && ui.dialogResult[3] == 0 && cancelButton) {
+ ui.dialogResult = (const char *) cancelButton->e.cp;
+ } else if (ui.dialogResult[0] == '_' && ui.dialogResult[1] == '_' && ui.dialogResult[2] == 'D' && ui.dialogResult[3] == 0 && defaultButton) {
+ ui.dialogResult = (const char *) defaultButton->e.cp;
+ }
+
+ // Destroy the dialog.
+
+ window->e.children[0]->flags &= ~UI_ELEMENT_DISABLED;
+ UIElementDestroy(window->dialog);
+ window->dialog = NULL;
+ UIElementRefresh(&window->e);
+ if (window->dialogOldFocus) UIElementFocus(window->dialogOldFocus);
+ return ui.dialogResult ? ui.dialogResult : "";
+}
+
+/////////////////////////////////////////
+// Menus (common).
+/////////////////////////////////////////
+
+bool _UIMenusClose() {
+ UIWindow *window = ui.windows;
+ bool anyClosed = false;
+
+ while (window) {
+ if (window->e.flags & UI_WINDOW_MENU) {
+ UIElementDestroy(&window->e);
+ anyClosed = true;
+ }
+
+ window = window->next;
+ }
+
+ return anyClosed;
+}
+
+#if !defined(UI_ESSENCE) && !defined(UI_COCOA)
+int _UIMenuItemMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_CLICKED) {
+ _UIMenusClose();
+ }
+
+ return 0;
+}
+
+int _UIMenuMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ UIMenu *menu = (UIMenu *) element;
+
+ if (message == UI_MSG_GET_WIDTH) {
+ int width = 0;
+
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (~child->flags & UI_ELEMENT_NON_CLIENT) {
+ int w = UIElementMessage(child, UI_MSG_GET_WIDTH, 0, 0);
+ if (w > width) width = w;
+ }
+ }
+
+ return width + 4 + UI_SIZE_SCROLL_BAR;
+ } else if (message == UI_MSG_GET_HEIGHT) {
+ int height = 0;
+
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (~child->flags & UI_ELEMENT_NON_CLIENT) {
+ height += UIElementMessage(child, UI_MSG_GET_HEIGHT, 0, 0);
+ }
+ }
+
+ return height + 4;
+ } else if (message == UI_MSG_PAINT) {
+ UIDrawControl((UIPainter *) dp, element->bounds, UI_DRAW_CONTROL_MENU, NULL, 0, 0, element->window->scale);
+ } else if (message == UI_MSG_LAYOUT) {
+ int position = element->bounds.t + 2 - menu->vScroll->position;
+ int totalHeight = 0;
+ int scrollBarSize = (menu->e.flags & UI_MENU_NO_SCROLL) ? 0 : UI_SIZE_SCROLL_BAR;
+
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (~child->flags & UI_ELEMENT_NON_CLIENT) {
+ int height = UIElementMessage(child, UI_MSG_GET_HEIGHT, 0, 0);
+ UIElementMove(child, UI_RECT_4(element->bounds.l + 2, element->bounds.r - scrollBarSize - 2,
+ position, position + height), false);
+ position += height;
+ totalHeight += height;
+ }
+ }
+
+ UIRectangle scrollBarBounds = element->bounds;
+ scrollBarBounds.l = scrollBarBounds.r - scrollBarSize * element->window->scale;
+ menu->vScroll->maximum = totalHeight;
+ menu->vScroll->page = UI_RECT_HEIGHT(element->bounds);
+ UIElementMove(&menu->vScroll->e, scrollBarBounds, true);
+ } else if (message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if (m->code == UI_KEYCODE_ESCAPE) {
+ _UIMenusClose();
+ return 1;
+ }
+ } else if (message == UI_MSG_MOUSE_WHEEL) {
+ return UIElementMessage(&menu->vScroll->e, message, di, dp);
+ } else if (message == UI_MSG_SCROLLED) {
+ UIElementRefresh(element);
+ }
+
+ return 0;
+}
+
+void UIMenuAddItem(UIMenu *menu, uint32_t flags, const char *label, ptrdiff_t labelBytes, void (*invoke)(void *cp), void *cp) {
+ UIButton *button = UIButtonCreate(&menu->e, flags | UI_BUTTON_MENU_ITEM, label, labelBytes);
+ button->invoke = invoke;
+ button->e.messageUser = _UIMenuItemMessage;
+ button->e.cp = cp;
+}
+
+void _UIMenuPrepare(UIMenu *menu, int *width, int *height) {
+ *width = UIElementMessage(&menu->e, UI_MSG_GET_WIDTH, 0, 0);
+ *height = UIElementMessage(&menu->e, UI_MSG_GET_HEIGHT, 0, 0);
+
+ if (menu->e.flags & UI_MENU_PLACE_ABOVE) {
+ menu->pointY -= *height;
+ }
+}
+
+UIMenu *UIMenuCreate(UIElement *parent, uint32_t flags) {
+ UIWindow *window = UIWindowCreate(parent->window, UI_WINDOW_MENU, 0, 0, 0);
+ UIMenu *menu = (UIMenu *) UIElementCreate(sizeof(UIMenu), &window->e, flags, _UIMenuMessage, "Menu");
+ menu->vScroll = UIScrollBarCreate(&menu->e, UI_ELEMENT_NON_CLIENT);
+ menu->parentWindow = parent->window;
+
+ if (parent->parent) {
+ UIRectangle screenBounds = UIElementScreenBounds(parent);
+ menu->pointX = screenBounds.l;
+ menu->pointY = (flags & UI_MENU_PLACE_ABOVE) ? (screenBounds.t + 1) : (screenBounds.b - 1);
+ } else {
+ int x = 0, y = 0;
+ _UIWindowGetScreenPosition(parent->window, &x, &y);
+
+ menu->pointX = parent->window->cursorX + x;
+ menu->pointY = parent->window->cursorY + y;
+ }
+
+ return menu;
+}
+#endif
+
+/////////////////////////////////////////
+// Miscellaneous core functions.
+/////////////////////////////////////////
+
+UIRectangle UIElementScreenBounds(UIElement *element) {
+ int x = 0, y = 0;
+ _UIWindowGetScreenPosition(element->window, &x, &y);
+ return UIRectangleAdd(element->bounds, UI_RECT_2(x, y));
+}
+
+void UIWindowRegisterShortcut(UIWindow *window, UIShortcut shortcut) {
+ if (window->shortcutCount + 1 > window->shortcutAllocated) {
+ window->shortcutAllocated = (window->shortcutCount + 1) * 2;
+ window->shortcuts = (UIShortcut *) UI_REALLOC(window->shortcuts, window->shortcutAllocated * sizeof(UIShortcut));
+ }
+
+ window->shortcuts[window->shortcutCount++] = shortcut;
+}
+
+void UIElementSetDisabled(UIElement *element, bool disabled) {
+ if (element->window->focused == element && disabled) {
+ UIElementFocus(&element->window->e);
+ }
+
+ if ((element->flags & UI_ELEMENT_DISABLED) && disabled) return;
+ if ((~element->flags & UI_ELEMENT_DISABLED) && !disabled) return;
+
+ if (disabled) element->flags |= UI_ELEMENT_DISABLED;
+ else element->flags &= ~UI_ELEMENT_DISABLED;
+
+ UIElementMessage(element, UI_MSG_UPDATE, UI_UPDATE_DISABLED, 0);
+}
+
+void UIElementFocus(UIElement *element) {
+ UIElement *previous = element->window->focused;
+ if (previous == element) return;
+ element->window->focused = element;
+ if (previous) UIElementMessage(previous, UI_MSG_UPDATE, UI_UPDATE_FOCUSED, 0);
+ if (element) UIElementMessage(element, UI_MSG_UPDATE, UI_UPDATE_FOCUSED, 0);
+
+#ifdef UI_DEBUG
+ _UIInspectorRefresh();
+#endif
+}
+
+/////////////////////////////////////////
+// Update cycles.
+/////////////////////////////////////////
+
+void UIElementRefresh(UIElement *element) {
+ UIElementRelayout(element);
+ UIElementRepaint(element, NULL);
+}
+
+void UIElementRelayout(UIElement *element) {
+ if (element->flags & UI_ELEMENT_RELAYOUT) {
+ return;
+ }
+
+ element->flags |= UI_ELEMENT_RELAYOUT;
+ UIElement *ancestor = element->parent;
+
+ while (ancestor) {
+ ancestor->flags |= UI_ELEMENT_RELAYOUT_DESCENDENT;
+ ancestor = ancestor->parent;
+ }
+}
+
+void UIElementMeasurementsChanged(UIElement *element, int which) {
+ if (!element->parent) {
+ return; // This is the window element.
+ }
+
+ while (true) {
+ if (element->parent->flags & UI_ELEMENT_DESTROY) return;
+ which &= ~UIElementMessage(element->parent, UI_MSG_GET_CHILD_STABILITY, which, element);
+ if (!which) break;
+ element->flags |= UI_ELEMENT_RELAYOUT;
+ element = element->parent;
+ }
+
+ UIElementRelayout(element);
+}
+
+void UIElementRepaint(UIElement *element, UIRectangle *region) {
+ if (!region) {
+ region = &element->bounds;
+ }
+
+ UIRectangle r = UIRectangleIntersection(*region, element->clip);
+
+ if (!UI_RECT_VALID(r)) {
+ return;
+ }
+
+ if (UI_RECT_VALID(element->window->updateRegion)) {
+ element->window->updateRegion = UIRectangleBounding(element->window->updateRegion, r);
+ } else {
+ element->window->updateRegion = r;
+ }
+}
+
+void UIElementMove(UIElement *element, UIRectangle bounds, bool layout) {
+ UIRectangle clip = element->parent? UIRectangleIntersection(element->parent->clip, bounds) : bounds;
+ bool moved = !UIRectangleEquals(element->bounds, bounds) || !UIRectangleEquals(element->clip, clip);
+
+ if (moved) {
+ layout = true;
+
+ UIElementRepaint(&element->window->e, &element->clip);
+ UIElementRepaint(&element->window->e, &clip);
+
+ element->bounds = bounds;
+ element->clip = clip;
+ }
+
+ if (element->flags & UI_ELEMENT_RELAYOUT) {
+ layout = true;
+ }
+
+ if (layout) {
+ UIElementMessage(element, UI_MSG_LAYOUT, 0, 0);
+ } else if (element->flags & UI_ELEMENT_RELAYOUT_DESCENDENT) {
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElementMove(element->children[i], element->children[i]->bounds, false);
+ }
+ }
+
+ element->flags &= ~(UI_ELEMENT_RELAYOUT_DESCENDENT | UI_ELEMENT_RELAYOUT);
+}
+
+void _UIElementPaint(UIElement *element, UIPainter *painter) {
+ if (element->flags & UI_ELEMENT_HIDE) {
+ return;
+ }
+
+ // Clip painting to the element's clip.
+
+ painter->clip = UIRectangleIntersection(element->clip, painter->clip);
+
+ if (!UI_RECT_VALID(painter->clip)) {
+ return;
+ }
+
+ // Paint the element.
+
+ UIElementMessage(element, UI_MSG_PAINT, 0, painter);
+
+ // Paint its children.
+
+ UIRectangle previousClip = painter->clip;
+
+ for (uintptr_t i = 0; i < element->childCount; i++) {
+ painter->clip = previousClip;
+ _UIElementPaint(element->children[i], painter);
+ }
+
+ // Draw the foreground and border.
+
+ painter->clip = previousClip;
+ UIElementMessage(element, UI_MSG_PAINT_FOREGROUND, 0, painter);
+
+ if (element->flags & UI_ELEMENT_BORDER) {
+ UIDrawBorder(painter, element->bounds, ui.theme.border, UI_RECT_1((int) element->window->scale));
+ }
+}
+
+bool _UIDestroy(UIElement *element) {
+ if (element->flags & UI_ELEMENT_DESTROY_DESCENDENT) {
+ element->flags &= ~UI_ELEMENT_DESTROY_DESCENDENT;
+
+ for (uintptr_t i = 0; i < element->childCount; i++) {
+ if (_UIDestroy(element->children[i])) {
+ UI_MEMMOVE(&element->children[i], &element->children[i + 1], sizeof(UIElement *) * (element->childCount - i - 1));
+ element->childCount--, i--;
+ }
+ }
+ }
+
+ if (element->flags & UI_ELEMENT_DESTROY) {
+ UIElementMessage(element, UI_MSG_DEALLOCATE, 0, 0);
+
+ if (element->window->pressed == element) {
+ _UIWindowSetPressed(element->window, NULL, 0);
+ }
+
+ if (element->window->hovered == element) {
+ element->window->hovered = &element->window->e;
+ }
+
+ if (element->window->focused == element) {
+ element->window->focused = NULL;
+ }
+
+ if (element->window->dialogOldFocus == element) {
+ element->window->dialogOldFocus = NULL;
+ }
+
+ UIElementAnimate(element, true);
+ UI_FREE(element->children);
+ UI_FREE(element);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+void _UIUpdate() {
+ UIWindow *window = ui.windows;
+ UIWindow **link = &ui.windows;
+
+ while (window) {
+ UIWindow *next = window->next;
+
+ UIElementMessage(&window->e, UI_MSG_WINDOW_UPDATE_START, 0, 0);
+ UIElementMessage(&window->e, UI_MSG_WINDOW_UPDATE_BEFORE_DESTROY, 0, 0);
+
+ if (_UIDestroy(&window->e)) {
+ *link = next;
+ } else {
+ link = &window->next;
+
+ UIElementMessage(&window->e, UI_MSG_WINDOW_UPDATE_BEFORE_LAYOUT, 0, 0);
+ UIElementMove(&window->e, window->e.bounds, false);
+ UIElementMessage(&window->e, UI_MSG_WINDOW_UPDATE_BEFORE_PAINT, 0, 0);
+
+ if (UI_RECT_VALID(window->updateRegion)) {
+#ifdef __cplusplus
+ UIPainter painter = {};
+#else
+ UIPainter painter = { 0 };
+#endif
+ painter.bits = window->bits;
+ painter.width = window->width;
+ painter.height = window->height;
+ painter.clip = UIRectangleIntersection(UI_RECT_2S(window->width, window->height), window->updateRegion);
+ _UIElementPaint(&window->e, &painter);
+ _UIWindowEndPaint(window, &painter);
+ window->updateRegion = UI_RECT_1(0);
+
+#ifdef UI_DEBUG
+ window->lastFullFillCount = (float) painter.fillCount / (UI_RECT_WIDTH(window->updateRegion) * UI_RECT_HEIGHT(window->updateRegion));
+#endif
+ }
+
+ UIElementMessage(&window->e, UI_MSG_WINDOW_UPDATE_END, 0, 0);
+ }
+
+ window = next;
+ }
+}
+
+/////////////////////////////////////////
+// Input event handling.
+/////////////////////////////////////////
+
+void _UIWindowSetPressed(UIWindow *window, UIElement *element, int button) {
+ UIElement *previous = window->pressed;
+ window->pressed = element;
+ window->pressedButton = button;
+ if (previous) UIElementMessage(previous, UI_MSG_UPDATE, UI_UPDATE_PRESSED, 0);
+ if (element) UIElementMessage(element, UI_MSG_UPDATE, UI_UPDATE_PRESSED, 0);
+
+ UIElement *ancestor = element;
+ UIElement *child = NULL;
+
+ while (ancestor) {
+ UIElementMessage(ancestor, UI_MSG_PRESSED_DESCENDENT, 0, child);
+ child = ancestor;
+ ancestor = ancestor->parent;
+ }
+}
+
+UIElement *UIElementFindByPoint(UIElement *element, int x, int y) {
+ for (uint32_t i = element->childCount; i > 0; i--) {
+ UIElement *child = element->children[i - 1];
+
+ if ((~child->flags & UI_ELEMENT_HIDE) && UIRectangleContains(child->clip, x, y)) {
+ return UIElementFindByPoint(child, x, y);
+ }
+ }
+
+ return element;
+}
+
+bool UIMenusOpen() {
+ UIWindow *window = ui.windows;
+
+ while (window) {
+ if (window->e.flags & UI_WINDOW_MENU) {
+ return true;
+ }
+
+ window = window->next;
+ }
+
+ return false;
+}
+
+UIElement *_UIElementNextOrPreviousSibling(UIElement *element, bool previous) {
+ if (!element->parent) {
+ return NULL;
+ }
+
+ for (uint32_t i = 0; i < element->parent->childCount; i++) {
+ if (element->parent->children[i] == element) {
+ if (previous) {
+ return i > 0 ? element->parent->children[i - 1] : NULL;
+ } else {
+ return i < element->parent->childCount - 1 ? element->parent->children[i + 1] : NULL;
+ }
+ }
+ }
+
+ UI_ASSERT(false);
+ return NULL;
+}
+
+bool _UIWindowInputEvent(UIWindow *window, UIMessage message, int di, void *dp) {
+ bool handled = true;
+
+ if (window->pressed) {
+ if (message == UI_MSG_MOUSE_MOVE) {
+ UIElementMessage(window->pressed, UI_MSG_MOUSE_DRAG, di, dp);
+ } else if (message == UI_MSG_LEFT_UP && window->pressedButton == 1) {
+ if (window->hovered == window->pressed) {
+ UIElementMessage(window->pressed, UI_MSG_CLICKED, di, dp);
+ if (ui.quit || ui.dialogResult) goto end;
+ }
+
+ if (window->pressed) {
+ UIElementMessage(window->pressed, UI_MSG_LEFT_UP, di, dp);
+ if (ui.quit || ui.dialogResult) goto end;
+ _UIWindowSetPressed(window, NULL, 1);
+ }
+ } else if (message == UI_MSG_MIDDLE_UP && window->pressedButton == 2) {
+ UIElementMessage(window->pressed, UI_MSG_MIDDLE_UP, di, dp);
+ if (ui.quit || ui.dialogResult) goto end;
+ _UIWindowSetPressed(window, NULL, 2);
+ } else if (message == UI_MSG_RIGHT_UP && window->pressedButton == 3) {
+ UIElementMessage(window->pressed, UI_MSG_RIGHT_UP, di, dp);
+ if (ui.quit || ui.dialogResult) goto end;
+ _UIWindowSetPressed(window, NULL, 3);
+ }
+ }
+
+ if (window->pressed) {
+ bool inside = UIRectangleContains(window->pressed->clip, window->cursorX, window->cursorY);
+
+ if (inside && window->hovered == &window->e) {
+ window->hovered = window->pressed;
+ UIElementMessage(window->pressed, UI_MSG_UPDATE, UI_UPDATE_HOVERED, 0);
+ } else if (!inside && window->hovered == window->pressed) {
+ window->hovered = &window->e;
+ UIElementMessage(window->pressed, UI_MSG_UPDATE, UI_UPDATE_HOVERED, 0);
+ }
+
+ if (ui.quit || ui.dialogResult) goto end;
+ }
+
+ if (!window->pressed) {
+ UIElement *hovered = UIElementFindByPoint(&window->e, window->cursorX, window->cursorY);
+
+ if (message == UI_MSG_MOUSE_MOVE) {
+ UIElementMessage(hovered, UI_MSG_MOUSE_MOVE, di, dp);
+
+ int cursor = UIElementMessage(window->hovered, UI_MSG_GET_CURSOR, di, dp);
+
+ if (cursor != window->cursorStyle) {
+ window->cursorStyle = cursor;
+ _UIWindowSetCursor(window, cursor);
+ }
+ } else if (message == UI_MSG_LEFT_DOWN) {
+ if ((window->e.flags & UI_WINDOW_MENU) || !_UIMenusClose()) {
+ _UIWindowSetPressed(window, hovered, 1);
+ UIElementMessage(hovered, UI_MSG_LEFT_DOWN, di, dp);
+ }
+ } else if (message == UI_MSG_MIDDLE_DOWN) {
+ if ((window->e.flags & UI_WINDOW_MENU) || !_UIMenusClose()) {
+ _UIWindowSetPressed(window, hovered, 2);
+ UIElementMessage(hovered, UI_MSG_MIDDLE_DOWN, di, dp);
+ }
+ } else if (message == UI_MSG_RIGHT_DOWN) {
+ if ((window->e.flags & UI_WINDOW_MENU) || !_UIMenusClose()) {
+ _UIWindowSetPressed(window, hovered, 3);
+ UIElementMessage(hovered, UI_MSG_RIGHT_DOWN, di, dp);
+ }
+ } else if (message == UI_MSG_MOUSE_WHEEL) {
+ UIElement *element = hovered;
+
+ while (element) {
+ if (UIElementMessage(element, UI_MSG_MOUSE_WHEEL, di, dp)) {
+ break;
+ }
+
+ element = element->parent;
+ }
+ } else if (message == UI_MSG_KEY_TYPED || message == UI_MSG_KEY_RELEASED) {
+ handled = false;
+
+ if (window->focused) {
+ UIElement *element = window->focused;
+
+ while (element) {
+ if (UIElementMessage(element, message, di, dp)) {
+ handled = true;
+ break;
+ }
+
+ element = element->parent;
+ }
+ } else {
+ if (UIElementMessage(&window->e, message, di, dp)) {
+ handled = true;
+ }
+ }
+
+ if (!handled && !UIMenusOpen() && message == UI_MSG_KEY_TYPED) {
+ UIKeyTyped *m = (UIKeyTyped *) dp;
+
+ if (m->code == UI_KEYCODE_TAB && !window->ctrl && !window->alt) {
+ UIElement *start = window->focused ? window->focused : &window->e;
+ UIElement *element = start;
+
+ do {
+ if (element->childCount && !(element->flags & (UI_ELEMENT_HIDE | UI_ELEMENT_DISABLED))) {
+ element = window->shift ? element->children[element->childCount - 1] : element->children[0];
+ continue;
+ }
+
+ while (element) {
+ UIElement *sibling = _UIElementNextOrPreviousSibling(element, window->shift);
+ if (sibling) { element = sibling; break; }
+ element = element->parent;
+ }
+
+ if (!element) {
+ element = &window->e;
+ }
+ } while (element != start && ((~element->flags & UI_ELEMENT_TAB_STOP)
+ || (element->flags & (UI_ELEMENT_HIDE | UI_ELEMENT_DISABLED))));
+
+ if (~element->flags & UI_ELEMENT_WINDOW) {
+ UIElementFocus(element);
+ }
+
+ handled = true;
+ } else if (!window->dialog) {
+ for (intptr_t i = window->shortcutCount - 1; i >= 0; i--) {
+ UIShortcut *shortcut = window->shortcuts + i;
+
+ if (shortcut->code == m->code && shortcut->ctrl == window->ctrl
+ && shortcut->shift == window->shift && shortcut->alt == window->alt) {
+ shortcut->invoke(shortcut->cp);
+ handled = true;
+ break;
+ }
+ }
+ } else if (window->dialog) {
+ UIElementMessage(window->dialog, message, di, dp);
+ }
+ }
+ }
+
+ if (ui.quit || ui.dialogResult) goto end;
+
+ if (hovered != window->hovered) {
+ UIElement *previous = window->hovered;
+ window->hovered = hovered;
+ UIElementMessage(previous, UI_MSG_UPDATE, UI_UPDATE_HOVERED, 0);
+ UIElementMessage(window->hovered, UI_MSG_UPDATE, UI_UPDATE_HOVERED, 0);
+ }
+ }
+
+ end: _UIUpdate();
+ return handled;
+}
+
+/////////////////////////////////////////
+// Font handling.
+/////////////////////////////////////////
+
+// Taken from https://commons.wikimedia.org/wiki/File:Codepage-437.png
+// Public domain.
+
+const uint64_t _uiFont[] = {
+ 0x0000000000000000UL, 0x0000000000000000UL, 0xBD8181A5817E0000UL, 0x000000007E818199UL, 0xC3FFFFDBFF7E0000UL, 0x000000007EFFFFE7UL, 0x7F7F7F3600000000UL, 0x00000000081C3E7FUL,
+ 0x7F3E1C0800000000UL, 0x0000000000081C3EUL, 0xE7E73C3C18000000UL, 0x000000003C1818E7UL, 0xFFFF7E3C18000000UL, 0x000000003C18187EUL, 0x3C18000000000000UL, 0x000000000000183CUL,
+ 0xC3E7FFFFFFFFFFFFUL, 0xFFFFFFFFFFFFE7C3UL, 0x42663C0000000000UL, 0x00000000003C6642UL, 0xBD99C3FFFFFFFFFFUL, 0xFFFFFFFFFFC399BDUL, 0x331E4C5870780000UL, 0x000000001E333333UL,
+ 0x3C666666663C0000UL, 0x0000000018187E18UL, 0x0C0C0CFCCCFC0000UL, 0x00000000070F0E0CUL, 0xC6C6C6FEC6FE0000UL, 0x0000000367E7E6C6UL, 0xE73CDB1818000000UL, 0x000000001818DB3CUL,
+ 0x1F7F1F0F07030100UL, 0x000000000103070FUL, 0x7C7F7C7870604000UL, 0x0000000040607078UL, 0x1818187E3C180000UL, 0x0000000000183C7EUL, 0x6666666666660000UL, 0x0000000066660066UL,
+ 0xD8DEDBDBDBFE0000UL, 0x00000000D8D8D8D8UL, 0x6363361C06633E00UL, 0x0000003E63301C36UL, 0x0000000000000000UL, 0x000000007F7F7F7FUL, 0x1818187E3C180000UL, 0x000000007E183C7EUL,
+ 0x1818187E3C180000UL, 0x0000000018181818UL, 0x1818181818180000UL, 0x00000000183C7E18UL, 0x7F30180000000000UL, 0x0000000000001830UL, 0x7F060C0000000000UL, 0x0000000000000C06UL,
+ 0x0303000000000000UL, 0x0000000000007F03UL, 0xFF66240000000000UL, 0x0000000000002466UL, 0x3E1C1C0800000000UL, 0x00000000007F7F3EUL, 0x3E3E7F7F00000000UL, 0x0000000000081C1CUL,
+ 0x0000000000000000UL, 0x0000000000000000UL, 0x18183C3C3C180000UL, 0x0000000018180018UL, 0x0000002466666600UL, 0x0000000000000000UL, 0x36367F3636000000UL, 0x0000000036367F36UL,
+ 0x603E0343633E1818UL, 0x000018183E636160UL, 0x1830634300000000UL, 0x000000006163060CUL, 0x3B6E1C36361C0000UL, 0x000000006E333333UL, 0x000000060C0C0C00UL, 0x0000000000000000UL,
+ 0x0C0C0C0C18300000UL, 0x0000000030180C0CUL, 0x30303030180C0000UL, 0x000000000C183030UL, 0xFF3C660000000000UL, 0x000000000000663CUL, 0x7E18180000000000UL, 0x0000000000001818UL,
+ 0x0000000000000000UL, 0x0000000C18181800UL, 0x7F00000000000000UL, 0x0000000000000000UL, 0x0000000000000000UL, 0x0000000018180000UL, 0x1830604000000000UL, 0x000000000103060CUL,
+ 0xDBDBC3C3663C0000UL, 0x000000003C66C3C3UL, 0x1818181E1C180000UL, 0x000000007E181818UL, 0x0C183060633E0000UL, 0x000000007F630306UL, 0x603C6060633E0000UL, 0x000000003E636060UL,
+ 0x7F33363C38300000UL, 0x0000000078303030UL, 0x603F0303037F0000UL, 0x000000003E636060UL, 0x633F0303061C0000UL, 0x000000003E636363UL, 0x18306060637F0000UL, 0x000000000C0C0C0CUL,
+ 0x633E6363633E0000UL, 0x000000003E636363UL, 0x607E6363633E0000UL, 0x000000001E306060UL, 0x0000181800000000UL, 0x0000000000181800UL, 0x0000181800000000UL, 0x000000000C181800UL,
+ 0x060C183060000000UL, 0x000000006030180CUL, 0x00007E0000000000UL, 0x000000000000007EUL, 0x6030180C06000000UL, 0x00000000060C1830UL, 0x18183063633E0000UL, 0x0000000018180018UL,
+ 0x7B7B63633E000000UL, 0x000000003E033B7BUL, 0x7F6363361C080000UL, 0x0000000063636363UL, 0x663E6666663F0000UL, 0x000000003F666666UL, 0x03030343663C0000UL, 0x000000003C664303UL,
+ 0x66666666361F0000UL, 0x000000001F366666UL, 0x161E1646667F0000UL, 0x000000007F664606UL, 0x161E1646667F0000UL, 0x000000000F060606UL, 0x7B030343663C0000UL, 0x000000005C666363UL,
+ 0x637F636363630000UL, 0x0000000063636363UL, 0x18181818183C0000UL, 0x000000003C181818UL, 0x3030303030780000UL, 0x000000001E333333UL, 0x1E1E366666670000UL, 0x0000000067666636UL,
+ 0x06060606060F0000UL, 0x000000007F664606UL, 0xC3DBFFFFE7C30000UL, 0x00000000C3C3C3C3UL, 0x737B7F6F67630000UL, 0x0000000063636363UL, 0x63636363633E0000UL, 0x000000003E636363UL,
+ 0x063E6666663F0000UL, 0x000000000F060606UL, 0x63636363633E0000UL, 0x000070303E7B6B63UL, 0x363E6666663F0000UL, 0x0000000067666666UL, 0x301C0663633E0000UL, 0x000000003E636360UL,
+ 0x18181899DBFF0000UL, 0x000000003C181818UL, 0x6363636363630000UL, 0x000000003E636363UL, 0xC3C3C3C3C3C30000UL, 0x00000000183C66C3UL, 0xDBC3C3C3C3C30000UL, 0x000000006666FFDBUL,
+ 0x18183C66C3C30000UL, 0x00000000C3C3663CUL, 0x183C66C3C3C30000UL, 0x000000003C181818UL, 0x0C183061C3FF0000UL, 0x00000000FFC38306UL, 0x0C0C0C0C0C3C0000UL, 0x000000003C0C0C0CUL,
+ 0x1C0E070301000000UL, 0x0000000040607038UL, 0x30303030303C0000UL, 0x000000003C303030UL, 0x0000000063361C08UL, 0x0000000000000000UL, 0x0000000000000000UL, 0x0000FF0000000000UL,
+ 0x0000000000180C0CUL, 0x0000000000000000UL, 0x3E301E0000000000UL, 0x000000006E333333UL, 0x66361E0606070000UL, 0x000000003E666666UL, 0x03633E0000000000UL, 0x000000003E630303UL,
+ 0x33363C3030380000UL, 0x000000006E333333UL, 0x7F633E0000000000UL, 0x000000003E630303UL, 0x060F0626361C0000UL, 0x000000000F060606UL, 0x33336E0000000000UL, 0x001E33303E333333UL,
+ 0x666E360606070000UL, 0x0000000067666666UL, 0x18181C0018180000UL, 0x000000003C181818UL, 0x6060700060600000UL, 0x003C666660606060UL, 0x1E36660606070000UL, 0x000000006766361EUL,
+ 0x18181818181C0000UL, 0x000000003C181818UL, 0xDBFF670000000000UL, 0x00000000DBDBDBDBUL, 0x66663B0000000000UL, 0x0000000066666666UL, 0x63633E0000000000UL, 0x000000003E636363UL,
+ 0x66663B0000000000UL, 0x000F06063E666666UL, 0x33336E0000000000UL, 0x007830303E333333UL, 0x666E3B0000000000UL, 0x000000000F060606UL, 0x06633E0000000000UL, 0x000000003E63301CUL,
+ 0x0C0C3F0C0C080000UL, 0x00000000386C0C0CUL, 0x3333330000000000UL, 0x000000006E333333UL, 0xC3C3C30000000000UL, 0x00000000183C66C3UL, 0xC3C3C30000000000UL, 0x0000000066FFDBDBUL,
+ 0x3C66C30000000000UL, 0x00000000C3663C18UL, 0x6363630000000000UL, 0x001F30607E636363UL, 0x18337F0000000000UL, 0x000000007F63060CUL, 0x180E181818700000UL, 0x0000000070181818UL,
+ 0x1800181818180000UL, 0x0000000018181818UL, 0x18701818180E0000UL, 0x000000000E181818UL, 0x000000003B6E0000UL, 0x0000000000000000UL, 0x63361C0800000000UL, 0x00000000007F6363UL,
+};
+
+void UIDrawGlyph(UIPainter *painter, int x0, int y0, int c, uint32_t color) {
+#ifdef UI_FREETYPE
+ UIFont *font = ui.activeFont;
+
+ if (font->isFreeType) {
+#ifdef UI_UNICODE
+ if (c < 0) c = '?';
+#else
+ if (c < 0 || c > 127) c = '?';
+#endif
+ if (c == '\r') c = ' ';
+
+ if (!font->glyphsRendered[c]) {
+ FT_Load_Char(font->font, c == 24 ? 0x2191 : c == 25 ? 0x2193 : c == 26 ? 0x2192 : c == 27 ? 0x2190 : c, FT_LOAD_DEFAULT);
+#ifdef UI_FREETYPE_SUBPIXEL
+ FT_Render_Glyph(font->font->glyph, FT_RENDER_MODE_LCD);
+#else
+ FT_Render_Glyph(font->font->glyph, FT_RENDER_MODE_NORMAL);
+#endif
+ FT_Bitmap_Copy(ui.ft, &font->font->glyph->bitmap, &font->glyphs[c]);
+ font->glyphOffsetsX[c] = font->font->glyph->bitmap_left;
+ font->glyphOffsetsY[c] = font->font->size->metrics.ascender / 64 - font->font->glyph->bitmap_top;
+ font->glyphsRendered[c] = true;
+ }
+
+ FT_Bitmap *bitmap = &font->glyphs[c];
+ x0 += font->glyphOffsetsX[c], y0 += font->glyphOffsetsY[c];
+
+ for (int y = 0; y < (int) bitmap->rows; y++) {
+ if (y0 + y < painter->clip.t) continue;
+ if (y0 + y >= painter->clip.b) break;
+
+ int width = bitmap->pixel_mode == FT_PIXEL_MODE_LCD ? bitmap->width / 3 : bitmap->width;
+
+ for (int x = 0; x < width; x++) {
+ if (x0 + x < painter->clip.l) continue;
+ if (x0 + x >= painter->clip.r) break;
+
+ uint32_t *destination = painter->bits + (x0 + x) + (y0 + y) * painter->width;
+ uint32_t original = *destination, ra, ga, ba;
+
+ if (bitmap->pixel_mode == FT_PIXEL_MODE_LCD) {
+ ra = ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 0];
+ ga = ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 1];
+ ba = ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 2];
+ ra += (ga - ra) / 2, ba += (ga - ba) / 2; // TODO Gamma correct blending!
+ } else if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) {
+ ra = (((uint8_t *) bitmap->buffer)[(x >> 3) + y * bitmap->pitch] & (0x80 >> (x & 7))) ? 0xFF : 0;
+ ga = ra, ba = ra;
+ } else if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) {
+ ra = ((uint8_t *) bitmap->buffer)[x + y * bitmap->pitch];
+ ga = ra, ba = ra;
+ } else {
+ ra = ga = ba = 0;
+ }
+
+ uint32_t r2 = (255 - ra) * ((original & 0x000000FF) >> 0);
+ uint32_t g2 = (255 - ga) * ((original & 0x0000FF00) >> 8);
+ uint32_t b2 = (255 - ba) * ((original & 0x00FF0000) >> 16);
+ uint32_t r1 = ra * ((color & 0x000000FF) >> 0);
+ uint32_t g1 = ga * ((color & 0x0000FF00) >> 8);
+ uint32_t b1 = ba * ((color & 0x00FF0000) >> 16);
+
+ uint32_t result = 0xFF000000 | (0x00FF0000 & ((b1 + b2) << 8))
+ | (0x0000FF00 & ((g1 + g2) << 0))
+ | (0x000000FF & ((r1 + r2) >> 8));
+ *destination = result;
+ }
+ }
+
+ return;
+ }
+#endif
+
+ if (c < 0 || c > 127) c = '?';
+
+ UIRectangle rectangle = UIRectangleIntersection(painter->clip, UI_RECT_4(x0, x0 + 8, y0, y0 + 16));
+
+ const uint8_t *data = (const uint8_t *) _uiFont + c * 16;
+
+ for (int i = rectangle.t; i < rectangle.b; i++) {
+ uint32_t *bits = painter->bits + i * painter->width + rectangle.l;
+ uint8_t byte = data[i - y0];
+
+ for (int j = rectangle.l; j < rectangle.r; j++) {
+ if (byte & (1 << (j - x0))) {
+ *bits = color;
+ }
+
+ bits++;
+ }
+ }
+}
+
+UIFont *UIFontCreate(const char *cPath, uint32_t size) {
+ UIFont *font = (UIFont *) UI_CALLOC(sizeof(UIFont));
+
+#ifdef UI_FREETYPE
+#ifdef UI_UNICODE
+ font->glyphs = (FT_Bitmap *) UI_CALLOC(sizeof(FT_Bitmap) * (_UNICODE_MAX_CODEPOINT + 1));
+ font->glyphsRendered = (bool *) UI_CALLOC(sizeof(bool) * (_UNICODE_MAX_CODEPOINT + 1));
+ font->glyphOffsetsX = (int *) UI_CALLOC(sizeof(int) * (_UNICODE_MAX_CODEPOINT + 1));
+ font->glyphOffsetsY = (int *) UI_CALLOC(sizeof(int) * (_UNICODE_MAX_CODEPOINT + 1));
+#endif
+ if (cPath) {
+ int ret = FT_New_Face(ui.ft, cPath, 0, &font->font);
+ if (ret == 0) {
+ FT_Select_Charmap(font->font, FT_ENCODING_UNICODE);
+ if (FT_HAS_FIXED_SIZES(font->font) && font->font->num_fixed_sizes) {
+ // Look for the smallest strike that's at least `size`.
+ int j = 0;
+
+ for (int i = 0; i < font->font->num_fixed_sizes; i++) {
+ if ((uint32_t) font->font->available_sizes[i].height >= size
+ && font->font->available_sizes[i].y_ppem < font->font->available_sizes[j].y_ppem) {
+ j = i;
+ }
+ }
+
+ FT_Set_Pixel_Sizes(font->font, font->font->available_sizes[j].x_ppem / 64, font->font->available_sizes[j].y_ppem / 64);
+ } else {
+ FT_Set_Char_Size(font->font, 0, size * 64, 100, 100);
+ }
+
+ FT_Load_Char(font->font, 'a', FT_LOAD_DEFAULT);
+ font->glyphWidth = font->font->glyph->advance.x / 64;
+ font->glyphHeight = (font->font->size->metrics.ascender - font->font->size->metrics.descender) / 64;
+ font->isFreeType = true;
+ return font;
+ } else
+ printf("Cannot load font %s : %d\n", cPath, ret);
+ }
+#endif
+
+ font->glyphWidth = 9;
+ font->glyphHeight = 16;
+ return font;
+}
+
+UIFont *UIFontActivate(UIFont *font) {
+ UIFont *previous = ui.activeFont;
+ ui.activeFont = font;
+ return previous;
+}
+
+/////////////////////////////////////////
+// Debugging.
+/////////////////////////////////////////
+
+#ifdef UI_DEBUG
+
+void UIInspectorLog(const char *cFormat, ...) {
+ va_list arguments;
+ va_start(arguments, cFormat);
+ char buffer[4096];
+ vsnprintf(buffer, sizeof(buffer), cFormat, arguments);
+ UICodeInsertContent(ui.inspectorLog, buffer, -1, false);
+ va_end(arguments);
+ UIElementRefresh(&ui.inspectorLog->e);
+}
+
+UIElement *_UIInspectorFindNthElement(UIElement *element, int *index, int *depth) {
+ if (*index == 0) {
+ return element;
+ }
+
+ *index = *index - 1;
+
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (!(child->flags & (UI_ELEMENT_DESTROY | UI_ELEMENT_HIDE))) {
+ UIElement *result = _UIInspectorFindNthElement(child, index, depth);
+
+ if (result) {
+ if (depth) {
+ *depth = *depth + 1;
+ }
+
+ return result;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+int _UIInspectorTableMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (!ui.inspectorTarget) {
+ return 0;
+ }
+
+ if (message == UI_MSG_TABLE_GET_ITEM) {
+ UITableGetItem *m = (UITableGetItem *) dp;
+ int index = m->index;
+ int depth = 0;
+ UIElement *element = _UIInspectorFindNthElement(&ui.inspectorTarget->e, &index, &depth);
+ if (!element) return 0;
+
+ if (m->column == 0) {
+ return snprintf(m->buffer, m->bufferBytes, "%.*s%s", depth * 2, " ", element->cClassName);
+ } else if (m->column == 1) {
+ return snprintf(m->buffer, m->bufferBytes, "%d:%d, %d:%d", UI_RECT_ALL(element->bounds));
+ } else if (m->column == 2) {
+ return snprintf(m->buffer, m->bufferBytes, "%d%c", element->id, element->window->focused == element ? '*' : ' ');
+ }
+ } else if (message == UI_MSG_MOUSE_MOVE) {
+ int index = UITableHitTest(ui.inspectorTable, element->window->cursorX, element->window->cursorY);
+ UIElement *element = NULL;
+ if (index >= 0) element = _UIInspectorFindNthElement(&ui.inspectorTarget->e, &index, NULL);
+ UIWindow *window = ui.inspectorTarget;
+ UIPainter painter = { 0 };
+ window->updateRegion = window->e.bounds;
+ painter.bits = window->bits;
+ painter.width = window->width;
+ painter.height = window->height;
+ painter.clip = UI_RECT_2S(window->width, window->height);
+
+ for (int i = 0; i < window->width * window->height; i++) {
+ window->bits[i] = 0xFF00FF;
+ }
+
+ _UIElementPaint(&window->e, &painter);
+ painter.clip = UI_RECT_2S(window->width, window->height);
+
+ if (element) {
+ UIDrawInvert(&painter, element->bounds);
+ UIDrawInvert(&painter, UIRectangleAdd(element->bounds, UI_RECT_1I(4)));
+ }
+
+ _UIWindowEndPaint(window, &painter);
+ }
+
+ return 0;
+}
+
+void _UIInspectorCreate() {
+ ui.inspector = UIWindowCreate(0, UI_WINDOW_INSPECTOR, "Inspector", 0, 0);
+ UISplitPane *splitPane = UISplitPaneCreate(&ui.inspector->e, 0, 0.5f);
+ ui.inspectorTable = UITableCreate(&splitPane->e, 0, "Class\tBounds\tID");
+ ui.inspectorTable->e.messageUser = _UIInspectorTableMessage;
+ ui.inspectorLog = UICodeCreate(&splitPane->e, 0);
+}
+
+int _UIInspectorCountElements(UIElement *element) {
+ int count = 1;
+
+ for (uint32_t i = 0; i < element->childCount; i++) {
+ UIElement *child = element->children[i];
+
+ if (!(child->flags & (UI_ELEMENT_DESTROY | UI_ELEMENT_HIDE))) {
+ count += _UIInspectorCountElements(child);
+ }
+ }
+
+ return count;
+}
+
+void _UIInspectorRefresh() {
+ if (!ui.inspectorTarget || !ui.inspector || !ui.inspectorTable) return;
+ ui.inspectorTable->itemCount = _UIInspectorCountElements(&ui.inspectorTarget->e);
+ UITableResizeColumns(ui.inspectorTable);
+ UIElementRefresh(&ui.inspectorTable->e);
+}
+
+void _UIInspectorSetFocusedWindow(UIWindow *window) {
+ if (!ui.inspector || !ui.inspectorTable) return;
+
+ if (window->e.flags & UI_WINDOW_INSPECTOR) {
+ return;
+ }
+
+ if (ui.inspectorTarget != window) {
+ ui.inspectorTarget = window;
+ _UIInspectorRefresh();
+ }
+}
+
+#else
+
+void _UIInspectorCreate() {}
+void _UIInspectorSetFocusedWindow(UIWindow *window) {}
+void _UIInspectorRefresh() {}
+
+#endif
+
+/////////////////////////////////////////
+// Automation for tests.
+/////////////////////////////////////////
+
+#ifdef UI_AUTOMATION_TESTS
+
+int UIAutomationRunTests();
+
+void UIAutomationProcessMessage() {
+ int result;
+ _UIMessageLoopSingle(&result);
+}
+
+void UIAutomationKeyboardTypeSingle(intptr_t code, bool ctrl, bool shift, bool alt) {
+ UIWindow *window = ui.windows; // TODO Get the focused window.
+ UIKeyTyped m = { 0 };
+ m.code = code;
+ window->ctrl = ctrl;
+ window->alt = alt;
+ window->shift = shift;
+ _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m);
+ window->ctrl = false;
+ window->alt = false;
+ window->shift = false;
+}
+
+void UIAutomationKeyboardType(const char *string) {
+ UIWindow *window = ui.windows; // TODO Get the focused window.
+
+ UIKeyTyped m = { 0 };
+ char c[2];
+ m.text = c;
+ m.textBytes = 1;
+ c[1] = 0;
+
+ for (int i = 0; string[i]; i++) {
+ window->ctrl = false;
+ window->alt = false;
+ window->shift = (c[0] >= 'A' && c[0] <= 'Z');
+ c[0] = string[i];
+ m.code = (c[0] >= 'A' && c[0] <= 'Z') ? UI_KEYCODE_LETTER(c[0])
+ : c[0] == '\n' ? UI_KEYCODE_ENTER
+ : c[0] == '\t' ? UI_KEYCODE_TAB
+ : c[0] == ' ' ? UI_KEYCODE_SPACE
+ : (c[0] >= '0' && c[0] <= '9') ? UI_KEYCODE_DIGIT(c[0]) : 0;
+ _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m);
+ }
+
+ window->ctrl = false;
+ window->alt = false;
+ window->shift = false;
+}
+
+bool UIAutomationCheckCodeLineMatches(UICode *code, int lineIndex, const char *input) {
+ if (lineIndex < 1 || lineIndex > code->lineCount) return false;
+ int bytes = 0;
+ for (int i = 0; input[i]; i++) bytes++;
+ if (bytes != code->lines[lineIndex - 1].bytes) return false;
+ for (int i = 0; input[i]; i++) if (code->content[code->lines[lineIndex - 1].offset + i] != input[i]) return false;
+ return true;
+}
+
+bool UIAutomationCheckTableItemMatches(UITable *table, int row, int column, const char *input) {
+ int bytes = 0;
+ for (int i = 0; input[i]; i++) bytes++;
+ if (row < 0 || row >= table->itemCount) return false;
+ if (column < 0 || column >= table->columnCount) return false;
+ char *buffer = (char *) UI_MALLOC(bytes + 1);
+ UITableGetItem m = { 0 };
+ m.buffer = buffer;
+ m.bufferBytes = bytes + 1;
+ m.column = column;
+ m.index = row;
+ int length = UIElementMessage(&table->e, UI_MSG_TABLE_GET_ITEM, 0, &m);
+ if (length != bytes) return false;
+ for (int i = 0; input[i]; i++) if (buffer[i] != input[i]) return false;
+ return true;
+}
+
+#endif
+
+/////////////////////////////////////////
+// Common platform layer functionality.
+/////////////////////////////////////////
+
+void _UIWindowDestroyCommon(UIWindow *window) {
+ UI_FREE(window->bits);
+ UI_FREE(window->shortcuts);
+}
+
+void _UIInitialiseCommon() {
+ ui.theme = uiThemeClassic;
+
+#ifdef UI_FREETYPE
+ FT_Init_FreeType(&ui.ft);
+#else
+ UIFontActivate(UIFontCreate(0, 0));
+#endif
+}
+
+void _UIWindowAdd(UIWindow *window) {
+ window->scale = 1.0f;
+ window->e.window = window;
+ window->hovered = &window->e;
+ window->next = ui.windows;
+ ui.windows = window;
+}
+
+int _UIWindowMessageCommon(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_LAYOUT && element->childCount) {
+ UIElementMove(element->children[0], element->bounds, false);
+ if (element->window->dialog) UIElementMove(element->window->dialog, element->bounds, false);
+ UIElementRepaint(element, NULL);
+ } else if (message == UI_MSG_GET_CHILD_STABILITY) {
+ return 3; // Both width and height of the child element are ignored.
+ }
+
+ return 0;
+}
+
+int UIMessageLoop() {
+ _UIInspectorCreate();
+ _UIUpdate();
+#ifdef UI_AUTOMATION_TESTS
+ return UIAutomationRunTests();
+#else
+ int result = 0;
+ while (!ui.quit && _UIMessageLoopSingle(&result)) ui.dialogResult = NULL;
+ return result;
+#endif
+}
+
+/////////////////////////////////////////
+// Platform layers.
+/////////////////////////////////////////
+
+#ifdef UI_LINUX
+
+const int UI_KEYCODE_A = XK_a;
+const int UI_KEYCODE_BACKSPACE = XK_BackSpace;
+const int UI_KEYCODE_DELETE = XK_Delete;
+const int UI_KEYCODE_DOWN = XK_Down;
+const int UI_KEYCODE_END = XK_End;
+const int UI_KEYCODE_ENTER = XK_Return;
+const int UI_KEYCODE_ESCAPE = XK_Escape;
+const int UI_KEYCODE_F1 = XK_F1;
+const int UI_KEYCODE_HOME = XK_Home;
+const int UI_KEYCODE_LEFT = XK_Left;
+const int UI_KEYCODE_RIGHT = XK_Right;
+const int UI_KEYCODE_SPACE = XK_space;
+const int UI_KEYCODE_TAB = XK_Tab;
+const int UI_KEYCODE_UP = XK_Up;
+const int UI_KEYCODE_INSERT = XK_Insert;
+const int UI_KEYCODE_0 = XK_0;
+const int UI_KEYCODE_BACKTICK = XK_grave;
+const int UI_KEYCODE_PAGE_DOWN = XK_Page_Down;
+const int UI_KEYCODE_PAGE_UP = XK_Page_Up;
+
+int _UIWindowMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_DEALLOCATE) {
+ UIWindow *window = (UIWindow *) element;
+ _UIWindowDestroyCommon(window);
+ window->image->data = NULL;
+ XDestroyImage(window->image);
+ XDestroyIC(window->xic);
+ XDestroyWindow(ui.display, ((UIWindow *) element)->window);
+ }
+
+ return _UIWindowMessageCommon(element, message, di, dp);
+}
+
+UIWindow *UIWindowCreate(UIWindow *owner, uint32_t flags, const char *cTitle, int _width, int _height) {
+ _UIMenusClose();
+
+ UIWindow *window = (UIWindow *) UIElementCreate(sizeof(UIWindow), NULL, flags | UI_ELEMENT_WINDOW, _UIWindowMessage, "Window");
+ _UIWindowAdd(window);
+ if (owner) window->scale = owner->scale;
+
+ int width = (flags & UI_WINDOW_MENU) ? 1 : _width ? _width : 800;
+ int height = (flags & UI_WINDOW_MENU) ? 1 : _height ? _height : 600;
+
+ XSetWindowAttributes attributes = {};
+ attributes.override_redirect = flags & UI_WINDOW_MENU;
+
+ char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP");
+ if (xdg_current_desktop && strcmp(xdg_current_desktop, "Hyprland") == 0) {
+ window->window = XCreateWindow(ui.display, DefaultRootWindow(ui.display), 0, 0, width, height, 1, 0,
+ InputOutput, CopyFromParent, CWOverrideRedirect, &attributes);
+ XSetWindowBorderWidth(ui.display, window->window, 0);
+ } else {
+ window->window = XCreateWindow(ui.display, DefaultRootWindow(ui.display), 0, 0, width, height, 0, 0,
+ InputOutput, CopyFromParent, CWOverrideRedirect, &attributes);
+ }
+ if (cTitle) XStoreName(ui.display, window->window, cTitle);
+ XSelectInput(ui.display, window->window, SubstructureNotifyMask | ExposureMask | PointerMotionMask
+ | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | StructureNotifyMask
+ | EnterWindowMask | LeaveWindowMask | ButtonMotionMask | KeymapStateMask | FocusChangeMask | PropertyChangeMask);
+
+ if (flags & UI_WINDOW_MAXIMIZE) {
+ Atom atoms[2] = { XInternAtom(ui.display, "_NET_WM_STATE_MAXIMIZED_HORZ", 0), XInternAtom(ui.display, "_NET_WM_STATE_MAXIMIZED_VERT", 0) };
+ XChangeProperty(ui.display, window->window, XInternAtom(ui.display, "_NET_WM_STATE", 0), XA_ATOM, 32, PropModeReplace, (unsigned char *) atoms, 2);
+ }
+
+ if (~flags & UI_WINDOW_MENU) {
+ XMapRaised(ui.display, window->window);
+ }
+
+ if (flags & UI_WINDOW_CENTER_IN_OWNER) {
+ int x = 0, y = 0;
+ _UIWindowGetScreenPosition(owner, &x, &y);
+ XMoveResizeWindow(ui.display, window->window, x + owner->width / 2 - width / 2, y + owner->height / 2 - height / 2, width, height);
+ }
+
+ XSetWMProtocols(ui.display, window->window, &ui.windowClosedID, 1);
+ window->image = XCreateImage(ui.display, ui.visual, 24, ZPixmap, 0, NULL, 10, 10, 32, 0);
+
+ window->xic = XCreateIC(ui.xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, window->window, XNFocusWindow, window->window, NULL);
+
+ int dndVersion = 4;
+ XChangeProperty(ui.display, window->window, ui.dndAwareID, XA_ATOM, 32 /* bits */, PropModeReplace, (uint8_t *) &dndVersion, 1);
+
+ return window;
+}
+
+Display *_UIX11GetDisplay() {
+ return ui.display;
+}
+
+UIWindow *_UIFindWindow(Window window) {
+ UIWindow *w = ui.windows;
+
+ while (w) {
+ if (w->window == window) {
+ return w;
+ }
+
+ w = w->next;
+ }
+
+ return NULL;
+}
+
+void _UIClipboardWriteText(UIWindow *window, char *text) {
+ UI_FREE(ui.pasteText);
+ ui.pasteText = text;
+ XSetSelectionOwner(ui.display, ui.clipboardID, window->window, 0);
+}
+
+char *_UIClipboardReadTextStart(UIWindow *window, size_t *bytes) {
+ Window clipboardOwner = XGetSelectionOwner(ui.display, ui.clipboardID);
+
+ if (clipboardOwner == None) {
+ return NULL;
+ }
+
+ if (_UIFindWindow(clipboardOwner)) {
+ *bytes = strlen(ui.pasteText);
+ char *copy = (char *) UI_MALLOC(*bytes);
+ memcpy(copy, ui.pasteText, *bytes);
+ return copy;
+ }
+
+ XConvertSelection(ui.display, ui.clipboardID, XA_STRING, ui.xSelectionDataID, window->window, CurrentTime);
+ XSync(ui.display, 0);
+ XNextEvent(ui.display, &ui.copyEvent);
+
+ // Hack to get around the fact that PropertyNotify arrives before SelectionNotify.
+ // We need PropertyNotify for incremental transfers.
+ while (ui.copyEvent.type == PropertyNotify) {
+ XNextEvent(ui.display, &ui.copyEvent);
+ }
+
+ if (ui.copyEvent.type == SelectionNotify && ui.copyEvent.xselection.selection == ui.clipboardID && ui.copyEvent.xselection.property) {
+ Atom target;
+ // This `itemAmount` is actually `bytes_after_return`
+ unsigned long size, itemAmount;
+ char *data;
+ int format;
+ XGetWindowProperty(ui.copyEvent.xselection.display, ui.copyEvent.xselection.requestor, ui.copyEvent.xselection.property, 0L, ~0L, 0,
+ AnyPropertyType, &target, &format, &size, &itemAmount, (unsigned char **) &data);
+
+ // We have to allocate for incremental transfers but we don't have to allocate for non-incremental transfers.
+ // I'm allocating for both here to make _UIClipboardReadTextEnd work the same for both
+ if (target != ui.incrID) {
+ *bytes = size;
+ char *copy = (char *) UI_MALLOC(*bytes);
+ memcpy(copy, data, *bytes);
+ XFree(data);
+ XDeleteProperty(ui.copyEvent.xselection.display, ui.copyEvent.xselection.requestor, ui.copyEvent.xselection.property);
+ return copy;
+ }
+
+ XFree(data);
+ XDeleteProperty(ui.display, ui.copyEvent.xselection.requestor, ui.copyEvent.xselection.property);
+ XSync(ui.display, 0);
+
+ *bytes = 0;
+ char *fullData = NULL;
+
+ while (true) {
+ // TODO Timeout.
+ XNextEvent(ui.display, &ui.copyEvent);
+
+ if (ui.copyEvent.type == PropertyNotify) {
+ // The other case - PropertyDelete would be caused by us and can be ignored
+ if (ui.copyEvent.xproperty.state == PropertyNewValue) {
+ unsigned long chunkSize;
+
+ // Note that this call deletes the property.
+ XGetWindowProperty(ui.display, ui.copyEvent.xproperty.window, ui.copyEvent.xproperty.atom, 0L, ~0L,
+ True, AnyPropertyType, &target, &format, &chunkSize, &itemAmount, (unsigned char **) &data);
+
+ if (chunkSize == 0) {
+ return fullData;
+ } else {
+ ptrdiff_t currentOffset = *bytes;
+ *bytes += chunkSize;
+ fullData = (char *) UI_REALLOC(fullData, *bytes);
+ memcpy(fullData + currentOffset, data, chunkSize);
+ }
+
+ XFree(data);
+ }
+ }
+ }
+ } else {
+ // TODO What should happen in this case? Is the next event always going to be the selection event?
+ return NULL;
+ }
+}
+
+void _UIClipboardReadTextEnd(UIWindow *window, char *text) {
+ if (text) {
+ UI_FREE(text);
+ }
+}
+
+void UIInitialise() {
+ _UIInitialiseCommon();
+
+ XInitThreads();
+
+ ui.display = XOpenDisplay(NULL);
+ ui.visual = XDefaultVisual(ui.display, 0);
+
+ ui.windowClosedID = XInternAtom(ui.display, "WM_DELETE_WINDOW", 0);
+ ui.primaryID = XInternAtom(ui.display, "PRIMARY", 0);
+ ui.dndEnterID = XInternAtom(ui.display, "XdndEnter", 0);
+ ui.dndPositionID = XInternAtom(ui.display, "XdndPosition", 0);
+ ui.dndStatusID = XInternAtom(ui.display, "XdndStatus", 0);
+ ui.dndActionCopyID = XInternAtom(ui.display, "XdndActionCopy", 0);
+ ui.dndDropID = XInternAtom(ui.display, "XdndDrop", 0);
+ ui.dndSelectionID = XInternAtom(ui.display, "XdndSelection", 0);
+ ui.dndFinishedID = XInternAtom(ui.display, "XdndFinished", 0);
+ ui.dndAwareID = XInternAtom(ui.display, "XdndAware", 0);
+ ui.uriListID = XInternAtom(ui.display, "text/uri-list", 0);
+ ui.plainTextID = XInternAtom(ui.display, "text/plain", 0);
+ ui.clipboardID = XInternAtom(ui.display, "CLIPBOARD", 0);
+ ui.xSelectionDataID = XInternAtom(ui.display, "XSEL_DATA", 0);
+ ui.textID = XInternAtom(ui.display, "TEXT", 0);
+ ui.targetID = XInternAtom(ui.display, "TARGETS", 0);
+ ui.incrID = XInternAtom(ui.display, "INCR", 0);
+
+ ui.cursors[UI_CURSOR_ARROW] = XCreateFontCursor(ui.display, XC_left_ptr);
+ ui.cursors[UI_CURSOR_TEXT] = XCreateFontCursor(ui.display, XC_xterm);
+ ui.cursors[UI_CURSOR_SPLIT_V] = XCreateFontCursor(ui.display, XC_sb_v_double_arrow);
+ ui.cursors[UI_CURSOR_SPLIT_H] = XCreateFontCursor(ui.display, XC_sb_h_double_arrow);
+ ui.cursors[UI_CURSOR_FLIPPED_ARROW] = XCreateFontCursor(ui.display, XC_right_ptr);
+ ui.cursors[UI_CURSOR_CROSS_HAIR] = XCreateFontCursor(ui.display, XC_crosshair);
+ ui.cursors[UI_CURSOR_HAND] = XCreateFontCursor(ui.display, XC_hand1);
+ ui.cursors[UI_CURSOR_RESIZE_UP] = XCreateFontCursor(ui.display, XC_top_side);
+ ui.cursors[UI_CURSOR_RESIZE_LEFT] = XCreateFontCursor(ui.display, XC_left_side);
+ ui.cursors[UI_CURSOR_RESIZE_UP_RIGHT] = XCreateFontCursor(ui.display, XC_top_right_corner);
+ ui.cursors[UI_CURSOR_RESIZE_UP_LEFT] = XCreateFontCursor(ui.display, XC_top_left_corner);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN] = XCreateFontCursor(ui.display, XC_bottom_side);
+ ui.cursors[UI_CURSOR_RESIZE_RIGHT] = XCreateFontCursor(ui.display, XC_right_side);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN_LEFT] = XCreateFontCursor(ui.display, XC_bottom_left_corner);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN_RIGHT] = XCreateFontCursor(ui.display, XC_bottom_right_corner);
+
+ XSetLocaleModifiers("");
+
+ ui.xim = XOpenIM(ui.display, 0, 0, 0);
+
+ if(!ui.xim){
+ XSetLocaleModifiers("@im=none");
+ ui.xim = XOpenIM(ui.display, 0, 0, 0);
+ }
+}
+
+void _UIWindowSetCursor(UIWindow *window, int cursor) {
+ XDefineCursor(ui.display, window->window, ui.cursors[cursor]);
+}
+
+void _UIX11ResetCursor(UIWindow *window) {
+ XDefineCursor(ui.display, window->window, ui.cursors[UI_CURSOR_ARROW]);
+}
+
+void _UIWindowEndPaint(UIWindow *window, UIPainter *painter) {
+ (void) painter;
+
+ XPutImage(ui.display, window->window, DefaultGC(ui.display, 0), window->image,
+ UI_RECT_TOP_LEFT(window->updateRegion), UI_RECT_TOP_LEFT(window->updateRegion),
+ UI_RECT_SIZE(window->updateRegion));
+}
+
+void _UIWindowGetScreenPosition(UIWindow *window, int *_x, int *_y) {
+ Window child;
+ XTranslateCoordinates(ui.display, window->window, DefaultRootWindow(ui.display), 0, 0, _x, _y, &child);
+}
+
+void UIMenuShow(UIMenu *menu) {
+ Window child;
+
+ // Find the screen that contains the point the menu was created at.
+ Screen *menuScreen = NULL;
+ int screenX, screenY;
+
+ for (int i = 0; i < ScreenCount(ui.display); i++) {
+ Screen *screen = ScreenOfDisplay(ui.display, i);
+ int x, y;
+ XTranslateCoordinates(ui.display, screen->root, DefaultRootWindow(ui.display), 0, 0, &x, &y, &child);
+
+ if (menu->pointX >= x && menu->pointX < x + screen->width && menu->pointY >= y && menu->pointY < y + screen->height) {
+ menuScreen = screen;
+ screenX = x, screenY = y;
+ break;
+ }
+ }
+
+ int width, height;
+ _UIMenuPrepare(menu, &width, &height);
+
+ {
+ // Clamp the menu to the bounds of the window.
+ // This step shouldn't be necessary with the screen clamping below, but there are some buggy X11 drivers that report screen sizes incorrectly.
+ int wx, wy;
+ UIWindow *parentWindow = menu->parentWindow;
+ XTranslateCoordinates(ui.display, parentWindow->window, DefaultRootWindow(ui.display), 0, 0, &wx, &wy, &child);
+ if (menu->pointX + width > wx + parentWindow->width) menu->pointX = wx + parentWindow->width - width;
+ if (menu->pointY + height > wy + parentWindow->height) menu->pointY = wy + parentWindow->height - height;
+ if (menu->pointX < wx) menu->pointX = wx;
+ if (menu->pointY < wy) menu->pointY = wy;
+ }
+
+ if (menuScreen) {
+ // Clamp to the bounds of the screen.
+ if (menu->pointX + width > screenX + menuScreen->width) menu->pointX = screenX + menuScreen->width - width;
+ if (menu->pointY + height > screenY + menuScreen->height) menu->pointY = screenY + menuScreen->height - height;
+ if (menu->pointX < screenX) menu->pointX = screenX;
+ if (menu->pointY < screenY) menu->pointY = screenY;
+ if (menu->pointX + width > screenX + menuScreen->width) width = screenX + menuScreen->width - menu->pointX;
+ if (menu->pointY + height > screenY + menuScreen->height) height = screenY + menuScreen->height - menu->pointY;
+ }
+
+ Atom properties[] = {
+ XInternAtom(ui.display, "_NET_WM_WINDOW_TYPE", true),
+ XInternAtom(ui.display, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", true),
+ XInternAtom(ui.display, "_MOTIF_WM_HINTS", true),
+ };
+
+ XChangeProperty(ui.display, menu->e.window->window, properties[0], XA_ATOM, 32, PropModeReplace, (uint8_t *) properties, 2);
+ XSetTransientForHint(ui.display, menu->e.window->window, DefaultRootWindow(ui.display));
+
+ struct Hints {
+ int flags;
+ int functions;
+ int decorations;
+ int inputMode;
+ int status;
+ };
+
+ struct Hints hints = { 0 };
+ hints.flags = 2;
+ XChangeProperty(ui.display, menu->e.window->window, properties[2], properties[2], 32, PropModeReplace, (uint8_t *) &hints, 5);
+
+ XMapWindow(ui.display, menu->e.window->window);
+ XMoveResizeWindow(ui.display, menu->e.window->window, menu->pointX, menu->pointY, width, height);
+}
+
+void UIWindowPack(UIWindow *window, int _width) {
+ int width = _width ? _width : UIElementMessage(window->e.children[0], UI_MSG_GET_WIDTH, 0, 0);
+ int height = UIElementMessage(window->e.children[0], UI_MSG_GET_HEIGHT, width, 0);
+ XResizeWindow(ui.display, window->window, width, height);
+}
+
+bool _UIProcessEvent(XEvent *event) {
+ if (event->type == ClientMessage && (Atom) event->xclient.data.l[0] == ui.windowClosedID) {
+ UIWindow *window = _UIFindWindow(event->xclient.window);
+ if (!window) return false;
+ bool exit = !UIElementMessage(&window->e, UI_MSG_WINDOW_CLOSE, 0, 0);
+ if (exit) return true;
+ _UIUpdate();
+ return false;
+ } else if (event->type == Expose) {
+ UIWindow *window = _UIFindWindow(event->xexpose.window);
+ if (!window) return false;
+ XPutImage(ui.display, window->window, DefaultGC(ui.display, 0), window->image, 0, 0, 0, 0, window->width, window->height);
+ } else if (event->type == ConfigureNotify) {
+ UIWindow *window = _UIFindWindow(event->xconfigure.window);
+ if (!window) return false;
+
+ if (window->width != event->xconfigure.width || window->height != event->xconfigure.height) {
+ window->width = event->xconfigure.width;
+ window->height = event->xconfigure.height;
+ window->bits = (uint32_t *) UI_REALLOC(window->bits, window->width * window->height * 4);
+ window->image->width = window->width;
+ window->image->height = window->height;
+ window->image->bytes_per_line = window->width * 4;
+ window->image->data = (char *) window->bits;
+ window->e.bounds = UI_RECT_2S(window->width, window->height);
+ window->e.clip = UI_RECT_2S(window->width, window->height);
+#ifdef UI_DEBUG
+ for (int i = 0; i < window->width * window->height; i++) window->bits[i] = 0xFF00FF;
+#endif
+ UIElementRelayout(&window->e);
+ _UIUpdate();
+ }
+ } else if (event->type == MotionNotify) {
+ UIWindow *window = _UIFindWindow(event->xmotion.window);
+ if (!window) return false;
+ window->cursorX = event->xmotion.x;
+ window->cursorY = event->xmotion.y;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (event->type == LeaveNotify) {
+ UIWindow *window = _UIFindWindow(event->xcrossing.window);
+ if (!window) return false;
+
+ if (!window->pressed) {
+ window->cursorX = -1;
+ window->cursorY = -1;
+ }
+
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (event->type == ButtonPress || event->type == ButtonRelease) {
+ UIWindow *window = _UIFindWindow(event->xbutton.window);
+ if (!window) return false;
+ window->cursorX = event->xbutton.x;
+ window->cursorY = event->xbutton.y;
+
+ if (event->xbutton.button >= 1 && event->xbutton.button <= 3) {
+ _UIWindowInputEvent(window, (UIMessage) ((event->type == ButtonPress ? UI_MSG_LEFT_DOWN : UI_MSG_LEFT_UP)
+ + event->xbutton.button * 2 - 2), 0, 0);
+ } else if (event->xbutton.button == 4) {
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_WHEEL, -72, 0);
+ } else if (event->xbutton.button == 5) {
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_WHEEL, 72, 0);
+ }
+
+ _UIInspectorSetFocusedWindow(window);
+ } else if (event->type == KeyPress) {
+ UIWindow *window = _UIFindWindow(event->xkey.window);
+ if (!window) return false;
+
+ if (event->xkey.x == 0x7123 && event->xkey.y == 0x7456) {
+ // HACK! See UIWindowPostMessage.
+ uintptr_t p = ((uintptr_t) (event->xkey.x_root & 0xFFFF) << 0) | ((uintptr_t) (event->xkey.y_root & 0xFFFF) << 16);
+#if INTPTR_MAX == INT64_MAX
+ p |= (uintptr_t) (event->xkey.time & 0xFFFFFFFF) << 32;
+#endif
+ UIElementMessage(&window->e, (UIMessage) event->xkey.state, 0, (void *) p);
+ _UIUpdate();
+ } else {
+ char text[32];
+ KeySym symbol = NoSymbol;
+ Status status;
+ // printf("%ld, %s\n", symbol, text);
+ UIKeyTyped m = { 0 };
+ m.textBytes = Xutf8LookupString(window->xic, &event->xkey, text, sizeof(text) - 1, &symbol, &status);
+ m.text = text;
+ m.code = XLookupKeysym(&event->xkey, 0);
+
+ if (symbol == XK_Control_L || symbol == XK_Control_R) {
+ window->ctrl = true;
+ window->ctrlCode = event->xkey.keycode;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (symbol == XK_Shift_L || symbol == XK_Shift_R) {
+ window->shift = true;
+ window->shiftCode = event->xkey.keycode;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (symbol == XK_Alt_L || symbol == XK_Alt_R) {
+ window->alt = true;
+ window->altCode = event->xkey.keycode;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (symbol == XK_KP_Left) {
+ m.code = UI_KEYCODE_LEFT;
+ } else if (symbol == XK_KP_Right) {
+ m.code = UI_KEYCODE_RIGHT;
+ } else if (symbol == XK_KP_Up) {
+ m.code = UI_KEYCODE_UP;
+ } else if (symbol == XK_KP_Down) {
+ m.code = UI_KEYCODE_DOWN;
+ } else if (symbol == XK_KP_Home) {
+ m.code = UI_KEYCODE_HOME;
+ } else if (symbol == XK_KP_End) {
+ m.code = UI_KEYCODE_END;
+ } else if (symbol == XK_KP_Enter) {
+ m.code = UI_KEYCODE_ENTER;
+ } else if (symbol == XK_KP_Delete) {
+ m.code = UI_KEYCODE_DELETE;
+ } else if (symbol == XK_KP_Page_Up) {
+ m.code = UI_KEYCODE_UP;
+ } else if (symbol == XK_KP_Page_Down) {
+ m.code = UI_KEYCODE_DOWN;
+ }
+
+ _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m);
+ }
+ } else if (event->type == KeyRelease) {
+ UIWindow *window = _UIFindWindow(event->xkey.window);
+ if (!window) return false;
+
+ if (event->xkey.keycode == window->ctrlCode) {
+ window->ctrl = false;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (event->xkey.keycode == window->shiftCode) {
+ window->shift = false;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (event->xkey.keycode == window->altCode) {
+ window->alt = false;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else {
+ char text[32];
+ KeySym symbol = NoSymbol;
+ Status status;
+ UIKeyTyped m = { 0 };
+ m.textBytes = Xutf8LookupString(window->xic, &event->xkey, text, sizeof(text) - 1, &symbol, &status);
+ m.text = text;
+ m.code = XLookupKeysym(&event->xkey, 0);
+ _UIWindowInputEvent(window, UI_MSG_KEY_RELEASED, 0, &m);
+ }
+ } else if (event->type == FocusIn) {
+ UIWindow *window = _UIFindWindow(event->xfocus.window);
+ if (!window) return false;
+ window->ctrl = window->shift = window->alt = false;
+ UIElementMessage(&window->e, UI_MSG_WINDOW_ACTIVATE, 0, 0);
+ } else if (event->type == FocusOut || event->type == ResizeRequest) {
+ _UIMenusClose();
+ _UIUpdate();
+ } else if (event->type == ClientMessage && event->xclient.message_type == ui.dndEnterID) {
+ UIWindow *window = _UIFindWindow(event->xclient.window);
+ if (!window) return false;
+ window->dragSource = (Window) event->xclient.data.l[0];
+ } else if (event->type == ClientMessage && event->xclient.message_type == ui.dndPositionID) {
+ UIWindow *window = _UIFindWindow(event->xclient.window);
+ if (!window) return false;
+ XClientMessageEvent m = { 0 };
+ m.type = ClientMessage;
+ m.display = event->xclient.display;
+ m.window = (Window) event->xclient.data.l[0];
+ m.message_type = ui.dndStatusID;
+ m.format = 32;
+ m.data.l[0] = window->window;
+ m.data.l[1] = true;
+ m.data.l[4] = ui.dndActionCopyID;
+ XSendEvent(ui.display, m.window, False, NoEventMask, (XEvent *) &m);
+ XFlush(ui.display);
+ } else if (event->type == ClientMessage && event->xclient.message_type == ui.dndDropID) {
+ UIWindow *window = _UIFindWindow(event->xclient.window);
+ if (!window) return false;
+
+ // TODO Dropping text.
+
+ if (!XConvertSelection(ui.display, ui.dndSelectionID, ui.uriListID, ui.primaryID, window->window, event->xclient.data.l[2])) {
+ XClientMessageEvent m = { 0 };
+ m.type = ClientMessage;
+ m.display = ui.display;
+ m.window = window->dragSource;
+ m.message_type = ui.dndFinishedID;
+ m.format = 32;
+ m.data.l[0] = window->window;
+ m.data.l[1] = 0;
+ m.data.l[2] = ui.dndActionCopyID;
+ XSendEvent(ui.display, m.window, False, NoEventMask, (XEvent *) &m);
+ XFlush(ui.display);
+ }
+ } else if (event->type == SelectionNotify) {
+ UIWindow *window = _UIFindWindow(event->xselection.requestor);
+ if (!window) return false;
+ if (!window->dragSource) return false;
+
+ Atom type = None;
+ int format = 0;
+ unsigned long count = 0, bytesLeft = 0;
+ uint8_t *data = NULL;
+ XGetWindowProperty(ui.display, window->window, ui.primaryID, 0, 65536, False, AnyPropertyType, &type, &format, &count, &bytesLeft, &data);
+
+ if (format == 8 /* bits per character */) {
+ if (event->xselection.target == ui.uriListID) {
+ char *copy = (char *) UI_MALLOC(count);
+ int fileCount = 0;
+
+ for (int i = 0; i < (int) count; i++) {
+ copy[i] = data[i];
+
+ if (i && data[i - 1] == '\r' && data[i] == '\n') {
+ fileCount++;
+ }
+ }
+
+ char **files = (char **) UI_MALLOC(sizeof(char *) * fileCount);
+ fileCount = 0;
+
+ for (int i = 0; i < (int) count; i++) {
+ char *s = copy + i;
+ while (!(i && data[i - 1] == '\r' && data[i] == '\n' && i < (int) count)) i++;
+ copy[i - 1] = 0;
+
+ for (int j = 0; s[j]; j++) {
+ if (s[j] == '%' && s[j + 1] && s[j + 2]) {
+ char n[3];
+ n[0] = s[j + 1], n[1] = s[j + 2], n[2] = 0;
+ s[j] = strtol(n, NULL, 16);
+ if (!s[j]) break;
+ UI_MEMMOVE(s + j + 1, s + j + 3, strlen(s) - j - 2);
+ }
+ }
+
+ if (s[0] == 'f' && s[1] == 'i' && s[2] == 'l' && s[3] == 'e' && s[4] == ':' && s[5] == '/' && s[6] == '/') {
+ files[fileCount++] = s + 7;
+ }
+ }
+
+ UIElementMessage(&window->e, UI_MSG_WINDOW_DROP_FILES, fileCount, files);
+
+ UI_FREE(files);
+ UI_FREE(copy);
+ } else if (event->xselection.target == ui.plainTextID) {
+ // TODO.
+ }
+ }
+
+ XFree(data);
+
+ XClientMessageEvent m = { 0 };
+ m.type = ClientMessage;
+ m.display = ui.display;
+ m.window = window->dragSource;
+ m.message_type = ui.dndFinishedID;
+ m.format = 32;
+ m.data.l[0] = window->window;
+ m.data.l[1] = true;
+ m.data.l[2] = ui.dndActionCopyID;
+ XSendEvent(ui.display, m.window, False, NoEventMask, (XEvent *) &m);
+ XFlush(ui.display);
+
+ window->dragSource = 0; // Drag complete.
+ _UIUpdate();
+ } else if (event->type == SelectionRequest) {
+ UIWindow *window = _UIFindWindow(event->xclient.window);
+ if (!window) return false;
+
+ if ((XGetSelectionOwner(ui.display, ui.clipboardID) == window->window)
+ && (event->xselectionrequest.selection == ui.clipboardID)) {
+ XSelectionRequestEvent requestEvent = event->xselectionrequest;
+ Atom utf8ID = XInternAtom(ui.display, "UTF8_STRING", 1);
+ if (utf8ID == None) utf8ID = XA_STRING;
+
+ Atom type = requestEvent.target;
+ type = (type == ui.textID) ? XA_STRING : type;
+ int changePropertyResult = 0;
+
+ if(requestEvent.target == XA_STRING || requestEvent.target == ui.textID || requestEvent.target == utf8ID) {
+ changePropertyResult = XChangeProperty(requestEvent.display, requestEvent.requestor, requestEvent.property,
+ type, 8, PropModeReplace, (const unsigned char *) ui.pasteText, strlen(ui.pasteText));
+ } else if (requestEvent.target == ui.targetID) {
+ changePropertyResult = XChangeProperty(requestEvent.display, requestEvent.requestor, requestEvent.property,
+ XA_ATOM, 32, PropModeReplace, (unsigned char *) &utf8ID, 1);
+ }
+
+ if(changePropertyResult == 0 || changePropertyResult == 1) {
+ XSelectionEvent sendEvent = {
+ .type = SelectionNotify,
+ .serial = requestEvent.serial,
+ .send_event = requestEvent.send_event,
+ .display = requestEvent.display,
+ .requestor = requestEvent.requestor,
+ .selection = requestEvent.selection,
+ .target = requestEvent.target,
+ .property = requestEvent.property,
+ .time = requestEvent.time
+ };
+
+ XSendEvent(ui.display, requestEvent.requestor, 0, 0, (XEvent *) &sendEvent);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool _UIMessageLoopSingle(int *result) {
+ XEvent events[64];
+
+ if (ui.animatingCount) {
+ if (XPending(ui.display)) {
+ XNextEvent(ui.display, events + 0);
+ } else {
+ _UIProcessAnimations();
+ return true;
+ }
+ } else {
+ XNextEvent(ui.display, events + 0);
+ }
+
+ int p = 1;
+
+ int configureIndex = -1, motionIndex = -1, exposeIndex = -1;
+
+ while (p < 64 && XPending(ui.display)) {
+ XNextEvent(ui.display, events + p);
+
+#define _UI_MERGE_EVENTS(a, b) \
+ if (events[p].type == a) { \
+ if (b != -1) events[b].type = 0; \
+ b = p; \
+ }
+
+ _UI_MERGE_EVENTS(ConfigureNotify, configureIndex);
+ _UI_MERGE_EVENTS(MotionNotify, motionIndex);
+ _UI_MERGE_EVENTS(Expose, exposeIndex);
+
+ p++;
+ }
+
+ for (int i = 0; i < p; i++) {
+ if (!events[i].type) {
+ continue;
+ }
+
+ if (_UIProcessEvent(events + i)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void UIWindowPostMessage(UIWindow *window, UIMessage message, void *_dp) {
+ // HACK! Xlib doesn't seem to have a nice way to do this,
+ // so send a specially crafted key press event instead.
+ // TODO Maybe ClientMessage is what this should use?
+ uintptr_t dp = (uintptr_t) _dp;
+ XKeyEvent event = { 0 };
+ event.display = ui.display;
+ event.window = window->window;
+ event.root = DefaultRootWindow(ui.display);
+ event.subwindow = None;
+#if INTPTR_MAX == INT64_MAX
+ event.time = dp >> 32;
+#endif
+ event.x = 0x7123;
+ event.y = 0x7456;
+ event.x_root = (dp >> 0) & 0xFFFF;
+ event.y_root = (dp >> 16) & 0xFFFF;
+ event.same_screen = True;
+ event.keycode = 1;
+ event.state = message;
+ event.type = KeyPress;
+ XSendEvent(ui.display, window->window, True, KeyPressMask, (XEvent *) &event);
+ XFlush(ui.display);
+}
+
+#endif
+
+#ifdef UI_WINDOWS
+
+const int UI_KEYCODE_A = 'A';
+const int UI_KEYCODE_0 = '0';
+const int UI_KEYCODE_BACKSPACE = VK_BACK;
+const int UI_KEYCODE_DELETE = VK_DELETE;
+const int UI_KEYCODE_DOWN = VK_DOWN;
+const int UI_KEYCODE_END = VK_END;
+const int UI_KEYCODE_ENTER = VK_RETURN;
+const int UI_KEYCODE_ESCAPE = VK_ESCAPE;
+const int UI_KEYCODE_F1 = VK_F1;
+const int UI_KEYCODE_HOME = VK_HOME;
+const int UI_KEYCODE_LEFT = VK_LEFT;
+const int UI_KEYCODE_RIGHT = VK_RIGHT;
+const int UI_KEYCODE_SPACE = VK_SPACE;
+const int UI_KEYCODE_TAB = VK_TAB;
+const int UI_KEYCODE_UP = VK_UP;
+const int UI_KEYCODE_INSERT = VK_INSERT;
+const int UI_KEYCODE_PAGE_UP = VK_PRIOR;
+const int UI_KEYCODE_PAGE_DOWN = VK_NEXT;
+
+int _UIWindowMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_DEALLOCATE) {
+ UIWindow *window = (UIWindow *) element;
+ _UIWindowDestroyCommon(window);
+ SetWindowLongPtr(window->hwnd, GWLP_USERDATA, 0);
+ DestroyWindow(window->hwnd);
+ }
+
+ return _UIWindowMessageCommon(element, message, di, dp);
+}
+
+LRESULT CALLBACK _UIWindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
+ UIWindow *window = (UIWindow *) GetWindowLongPtr(hwnd, GWLP_USERDATA);
+
+ if (!window || ui.assertionFailure) {
+ return DefWindowProc(hwnd, message, wParam, lParam);
+ }
+
+ if (message == WM_CLOSE) {
+ if (UIElementMessage(&window->e, UI_MSG_WINDOW_CLOSE, 0, 0)) {
+ _UIUpdate();
+ return 0;
+ } else {
+ PostQuitMessage(0);
+ }
+ } else if (message == WM_SIZE) {
+ RECT client;
+ GetClientRect(hwnd, &client);
+ window->width = client.right;
+ window->height = client.bottom;
+ window->bits = (uint32_t *) UI_REALLOC(window->bits, window->width * window->height * 4);
+ window->e.bounds = UI_RECT_2S(window->width, window->height);
+ window->e.clip = UI_RECT_2S(window->width, window->height);
+ UIElementRelayout(&window->e);
+ _UIUpdate();
+ } else if (message == WM_MOUSEMOVE) {
+ if (!window->trackingLeave) {
+ window->trackingLeave = true;
+ TRACKMOUSEEVENT leave = { 0 };
+ leave.cbSize = sizeof(TRACKMOUSEEVENT);
+ leave.dwFlags = TME_LEAVE;
+ leave.hwndTrack = hwnd;
+ TrackMouseEvent(&leave);
+ }
+
+ POINT cursor;
+ GetCursorPos(&cursor);
+ ScreenToClient(hwnd, &cursor);
+ window->cursorX = cursor.x;
+ window->cursorY = cursor.y;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (message == WM_MOUSELEAVE) {
+ window->trackingLeave = false;
+
+ if (!window->pressed) {
+ window->cursorX = -1;
+ window->cursorY = -1;
+ }
+
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (message == WM_LBUTTONDOWN) {
+ SetCapture(hwnd);
+ _UIWindowInputEvent(window, UI_MSG_LEFT_DOWN, 0, 0);
+ } else if (message == WM_LBUTTONUP) {
+ if (window->pressedButton == 1) ReleaseCapture();
+ _UIWindowInputEvent(window, UI_MSG_LEFT_UP, 0, 0);
+ } else if (message == WM_MBUTTONDOWN) {
+ SetCapture(hwnd);
+ _UIWindowInputEvent(window, UI_MSG_MIDDLE_DOWN, 0, 0);
+ } else if (message == WM_MBUTTONUP) {
+ if (window->pressedButton == 2) ReleaseCapture();
+ _UIWindowInputEvent(window, UI_MSG_MIDDLE_UP, 0, 0);
+ } else if (message == WM_RBUTTONDOWN) {
+ SetCapture(hwnd);
+ _UIWindowInputEvent(window, UI_MSG_RIGHT_DOWN, 0, 0);
+ } else if (message == WM_RBUTTONUP) {
+ if (window->pressedButton == 3) ReleaseCapture();
+ _UIWindowInputEvent(window, UI_MSG_RIGHT_UP, 0, 0);
+ } else if (message == WM_MOUSEWHEEL) {
+ int delta = (int) wParam >> 16;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_WHEEL, -delta, 0);
+ } else if (message == WM_KEYDOWN) {
+ window->ctrl = GetKeyState(VK_CONTROL) & 0x8000;
+ window->shift = GetKeyState(VK_SHIFT) & 0x8000;
+ window->alt = GetKeyState(VK_MENU) & 0x8000;
+
+ UIKeyTyped m = { 0 };
+ m.code = wParam;
+ _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m);
+ } else if (message == WM_CHAR) {
+ UIKeyTyped m = { 0 };
+ char c = wParam;
+ m.text = &c;
+ m.textBytes = 1;
+ _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m);
+ } else if (message == WM_PAINT) {
+ PAINTSTRUCT paint;
+ HDC dc = BeginPaint(hwnd, &paint);
+ BITMAPINFOHEADER info = { 0 };
+ info.biSize = sizeof(info);
+ info.biWidth = window->width, info.biHeight = -window->height;
+ info.biPlanes = 1, info.biBitCount = 32;
+ StretchDIBits(dc, 0, 0, UI_RECT_SIZE(window->e.bounds), 0, 0, UI_RECT_SIZE(window->e.bounds),
+ window->bits, (BITMAPINFO *) &info, DIB_RGB_COLORS, SRCCOPY);
+ EndPaint(hwnd, &paint);
+ } else if (message == WM_SETCURSOR && LOWORD(lParam) == HTCLIENT) {
+ SetCursor(ui.cursors[window->cursorStyle]);
+ return 1;
+ } else if (message == WM_SETFOCUS || message == WM_KILLFOCUS) {
+ _UIMenusClose();
+
+ if (message == WM_SETFOCUS) {
+ _UIInspectorSetFocusedWindow(window);
+ UIElementMessage(&window->e, UI_MSG_WINDOW_ACTIVATE, 0, 0);
+ }
+ } else if (message == WM_MOUSEACTIVATE && (window->e.flags & UI_WINDOW_MENU)) {
+ return MA_NOACTIVATE;
+ } else if (message == WM_DROPFILES) {
+ HDROP drop = (HDROP) wParam;
+ int count = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0);
+ char **files = (char **) UI_MALLOC(sizeof(char *) * count);
+
+ for (int i = 0; i < count; i++) {
+ int length = DragQueryFile(drop, i, NULL, 0);
+ files[i] = (char *) UI_MALLOC(length + 1);
+ files[i][length] = 0;
+ DragQueryFile(drop, i, files[i], length + 1);
+ }
+
+ UIElementMessage(&window->e, UI_MSG_WINDOW_DROP_FILES, count, files);
+ for (int i = 0; i < count; i++) UI_FREE(files[i]);
+ UI_FREE(files);
+ DragFinish(drop);
+ _UIUpdate();
+ } else if (message == WM_APP + 1) {
+ UIElementMessage(&window->e, (UIMessage) wParam, 0, (void *) lParam);
+ _UIUpdate();
+ } else {
+ if (message == WM_NCLBUTTONDOWN || message == WM_NCMBUTTONDOWN || message == WM_NCRBUTTONDOWN) {
+ if (~window->e.flags & UI_WINDOW_MENU) {
+ _UIMenusClose();
+ _UIUpdate();
+ }
+ }
+
+ return DefWindowProc(hwnd, message, wParam, lParam);
+ }
+
+ return 0;
+}
+
+void UIInitialise() {
+ ui.heap = GetProcessHeap();
+
+ _UIInitialiseCommon();
+
+ ui.cursors[UI_CURSOR_ARROW] = LoadCursor(NULL, IDC_ARROW);
+ ui.cursors[UI_CURSOR_TEXT] = LoadCursor(NULL, IDC_IBEAM);
+ ui.cursors[UI_CURSOR_SPLIT_V] = LoadCursor(NULL, IDC_SIZENS);
+ ui.cursors[UI_CURSOR_SPLIT_H] = LoadCursor(NULL, IDC_SIZEWE);
+ ui.cursors[UI_CURSOR_FLIPPED_ARROW] = LoadCursor(NULL, IDC_ARROW);
+ ui.cursors[UI_CURSOR_CROSS_HAIR] = LoadCursor(NULL, IDC_CROSS);
+ ui.cursors[UI_CURSOR_HAND] = LoadCursor(NULL, IDC_HAND);
+ ui.cursors[UI_CURSOR_RESIZE_UP] = LoadCursor(NULL, IDC_SIZENS);
+ ui.cursors[UI_CURSOR_RESIZE_LEFT] = LoadCursor(NULL, IDC_SIZEWE);
+ ui.cursors[UI_CURSOR_RESIZE_UP_RIGHT] = LoadCursor(NULL, IDC_SIZENESW);
+ ui.cursors[UI_CURSOR_RESIZE_UP_LEFT] = LoadCursor(NULL, IDC_SIZENWSE);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN] = LoadCursor(NULL, IDC_SIZENS);
+ ui.cursors[UI_CURSOR_RESIZE_RIGHT] = LoadCursor(NULL, IDC_SIZEWE);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN_LEFT] = LoadCursor(NULL, IDC_SIZENESW);
+ ui.cursors[UI_CURSOR_RESIZE_DOWN_RIGHT] = LoadCursor(NULL, IDC_SIZENWSE);
+
+ WNDCLASS windowClass = { 0 };
+ windowClass.lpfnWndProc = _UIWindowProcedure;
+ windowClass.lpszClassName = "normal";
+ RegisterClass(&windowClass);
+ windowClass.style |= CS_DROPSHADOW;
+ windowClass.lpszClassName = "shadow";
+ RegisterClass(&windowClass);
+}
+
+bool _UIMessageLoopSingle(int *result) {
+ MSG message = { 0 };
+
+ if (ui.animating) {
+ if (PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) {
+ if (message.message == WM_QUIT) {
+ *result = message.wParam;
+ return false;
+ }
+
+ TranslateMessage(&message);
+ DispatchMessage(&message);
+ } else {
+ _UIProcessAnimations();
+ }
+ } else {
+ if (!GetMessage(&message, NULL, 0, 0)) {
+ *result = message.wParam;
+ return false;
+ }
+
+ TranslateMessage(&message);
+ DispatchMessage(&message);
+ }
+
+ return true;
+}
+
+void UIMenuShow(UIMenu *menu) {
+ int width, height;
+ _UIMenuPrepare(menu, &width, &height);
+ MoveWindow(menu->e.window->hwnd, menu->pointX, menu->pointY, width, height, FALSE);
+ ShowWindow(menu->e.window->hwnd, SW_SHOWNOACTIVATE);
+}
+
+UIWindow *UIWindowCreate(UIWindow *owner, uint32_t flags, const char *cTitle, int width, int height) {
+ _UIMenusClose();
+
+ UIWindow *window = (UIWindow *) UIElementCreate(sizeof(UIWindow), NULL, flags | UI_ELEMENT_WINDOW, _UIWindowMessage, "Window");
+ _UIWindowAdd(window);
+ if (owner) window->scale = owner->scale;
+
+ if (flags & UI_WINDOW_MENU) {
+ UI_ASSERT(owner);
+
+ window->hwnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_NOACTIVATE, "shadow", 0, WS_POPUP,
+ 0, 0, 0, 0, owner->hwnd, NULL, NULL, NULL);
+ } else {
+ window->hwnd = CreateWindowEx(WS_EX_ACCEPTFILES, "normal", cTitle, WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT, width ? width : CW_USEDEFAULT, height ? height : CW_USEDEFAULT,
+ owner ? owner->hwnd : NULL, NULL, NULL, NULL);
+ }
+
+ SetWindowLongPtr(window->hwnd, GWLP_USERDATA, (LONG_PTR) window);
+
+ if (~flags & UI_WINDOW_MENU) {
+ ShowWindow(window->hwnd, SW_SHOW);
+ PostMessage(window->hwnd, WM_SIZE, 0, 0);
+ }
+
+ return window;
+}
+
+void _UIWindowEndPaint(UIWindow *window, UIPainter *painter) {
+ HDC dc = GetDC(window->hwnd);
+ BITMAPINFOHEADER info = { 0 };
+ info.biSize = sizeof(info);
+ info.biWidth = window->width, info.biHeight = window->height;
+ info.biPlanes = 1, info.biBitCount = 32;
+ StretchDIBits(dc,
+ UI_RECT_TOP_LEFT(window->updateRegion), UI_RECT_SIZE(window->updateRegion),
+ window->updateRegion.l, window->updateRegion.b + 1,
+ UI_RECT_WIDTH(window->updateRegion), -UI_RECT_HEIGHT(window->updateRegion),
+ window->bits, (BITMAPINFO *) &info, DIB_RGB_COLORS, SRCCOPY);
+ ReleaseDC(window->hwnd, dc);
+}
+
+void _UIWindowSetCursor(UIWindow *window, int cursor) {
+ SetCursor(ui.cursors[cursor]);
+}
+
+void _UIWindowGetScreenPosition(UIWindow *window, int *_x, int *_y) {
+ POINT p;
+ p.x = 0;
+ p.y = 0;
+ ClientToScreen(window->hwnd, &p);
+ *_x = p.x;
+ *_y = p.y;
+}
+
+void UIWindowPostMessage(UIWindow *window, UIMessage message, void *_dp) {
+ PostMessage(window->hwnd, WM_APP + 1, (WPARAM) message, (LPARAM) _dp);
+}
+
+void *_UIHeapReAlloc(void *pointer, size_t size) {
+ if (pointer) {
+ if (size) {
+ return HeapReAlloc(ui.heap, 0, pointer, size);
+ } else {
+ UI_FREE(pointer);
+ return NULL;
+ }
+ } else {
+ if (size) {
+ return UI_MALLOC(size);
+ } else {
+ return NULL;
+ }
+ }
+}
+
+void _UIClipboardWriteText(UIWindow *window, char *text) {
+ if (OpenClipboard(window->hwnd)) {
+ EmptyClipboard();
+ HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, _UIStringLength(text) + 1);
+ char *copy = (char *) GlobalLock(memory);
+ for (uintptr_t i = 0; text[i]; i++) copy[i] = text[i];
+ GlobalUnlock(copy);
+ SetClipboardData(CF_TEXT, memory);
+ CloseClipboard();
+ }
+}
+
+char *_UIClipboardReadTextStart(UIWindow *window, size_t *bytes) {
+ if (!OpenClipboard(window->hwnd)) {
+ return NULL;
+ }
+
+ HANDLE memory = GetClipboardData(CF_TEXT);
+
+ if (!memory) {
+ CloseClipboard();
+ return NULL;
+ }
+
+ char *buffer = (char *) GlobalLock(memory);
+
+ if (!buffer) {
+ CloseClipboard();
+ return NULL;
+ }
+
+ size_t byteCount = GlobalSize(memory);
+
+ if (byteCount < 1) {
+ GlobalUnlock(memory);
+ CloseClipboard();
+ return NULL;
+ }
+
+ char *copy = (char *) UI_MALLOC(byteCount + 1);
+ for (uintptr_t i = 0; i < byteCount; i++) copy[i] = buffer[i];
+ copy[byteCount] = 0; // Just in case.
+
+ GlobalUnlock(memory);
+ CloseClipboard();
+
+ if (bytes) *bytes = _UIStringLength(copy);
+ return copy;
+}
+
+void _UIClipboardReadTextEnd(UIWindow *window, char *text) {
+ UI_FREE(text);
+}
+
+void *_UIMemmove(void *dest, const void *src, size_t n) {
+ if ((uintptr_t) dest < (uintptr_t) src) {
+ uint8_t *dest8 = (uint8_t *) dest;
+ const uint8_t *src8 = (const uint8_t *) src;
+ for (uintptr_t i = 0; i < n; i++) {
+ dest8[i] = src8[i];
+ }
+ return dest;
+ } else {
+ uint8_t *dest8 = (uint8_t *) dest;
+ const uint8_t *src8 = (const uint8_t *) src;
+ for (uintptr_t i = n; i; i--) {
+ dest8[i - 1] = src8[i - 1];
+ }
+ return dest;
+ }
+}
+
+#endif
+
+#ifdef UI_ESSENCE
+
+const int UI_KEYCODE_A = ES_SCANCODE_A;
+const int UI_KEYCODE_0 = ES_SCANCODE_0;
+const int UI_KEYCODE_BACKSPACE = ES_SCANCODE_BACKSPACE;
+const int UI_KEYCODE_DELETE = ES_SCANCODE_DELETE;
+const int UI_KEYCODE_DOWN = ES_SCANCODE_DOWN_ARROW;
+const int UI_KEYCODE_END = ES_SCANCODE_END;
+const int UI_KEYCODE_ENTER = ES_SCANCODE_ENTER;
+const int UI_KEYCODE_ESCAPE = ES_SCANCODE_ESCAPE;
+const int UI_KEYCODE_F1 = ES_SCANCODE_F1;
+const int UI_KEYCODE_HOME = ES_SCANCODE_HOME;
+const int UI_KEYCODE_LEFT = ES_SCANCODE_LEFT_ARROW;
+const int UI_KEYCODE_RIGHT = ES_SCANCODE_RIGHT_ARROW;
+const int UI_KEYCODE_SPACE = ES_SCANCODE_SPACE;
+const int UI_KEYCODE_TAB = ES_SCANCODE_TAB;
+const int UI_KEYCODE_UP = ES_SCANCODE_UP_ARROW;
+const int UI_KEYCODE_INSERT = ES_SCANCODE_INSERT;
+const int UI_KEYCODE_PAGE_UP = ES_SCANCODE_PAGE_UP;
+const int UI_KEYCODE_PAGE_DOWN = ES_SCANCODE_PAGE_DOWN;
+
+int _UIWindowMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_DEALLOCATE) {
+ // TODO Non-main windows.
+ element->window = NULL;
+ EsInstanceCloseReference(ui.instance);
+ }
+
+ return _UIWindowMessageCommon(element, message, di, dp);
+}
+
+void UIInitialise() {
+ _UIInitialiseCommon();
+
+ while (true) {
+ EsMessage *message = EsMessageReceive();
+
+ if (message->type == ES_MSG_INSTANCE_CREATE) {
+ ui.instance = EsInstanceCreate(message, NULL, 0);
+ break;
+ }
+ }
+}
+
+bool _UIMessageLoopSingle(int *result) {
+ if (ui.animating) {
+ // TODO.
+ } else {
+ _UIMessageProcess(EsMessageReceive());
+ }
+
+ return true;
+}
+
+UIMenu *UIMenuCreate(UIElement *parent, uint32_t flags) {
+ ui.menuIndex = 0;
+ return EsMenuCreate(parent->window->window, ES_MENU_AT_CURSOR);
+}
+
+void _UIMenuItemCallback(EsMenu *menu, EsGeneric context) {
+ ((void (*)(void *)) ui.menuData[context.u * 2 + 0])(ui.menuData[context.u * 2 + 1]);
+}
+
+void UIMenuAddItem(UIMenu *menu, uint32_t flags, const char *label, ptrdiff_t labelBytes, void (*invoke)(void *cp), void *cp) {
+ EsAssert(ui.menuIndex < 128);
+ ui.menuData[ui.menuIndex * 2 + 0] = (void *) invoke;
+ ui.menuData[ui.menuIndex * 2 + 1] = cp;
+ EsMenuAddItem(menu, (flags & UI_BUTTON_CHECKED) ? ES_MENU_ITEM_CHECKED : ES_FLAGS_DEFAULT,
+ label, labelBytes, _UIMenuItemCallback, ui.menuIndex);
+ ui.menuIndex++;
+}
+
+void UIMenuShow(UIMenu *menu) {
+ EsMenuShow(menu);
+}
+
+int _UIWindowCanvasMessage(EsElement *element, EsMessage *message) {
+ UIWindow *window = (UIWindow *) element->window->userData.p;
+
+ if (!window) {
+ return 0;
+ } else if (message->type == ES_MSG_PAINT) {
+ EsRectangle bounds = ES_RECT_4PD(message->painter->offsetX, message->painter->offsetY, window->width, window->height);
+ EsDrawBitmap(message->painter, bounds, window->bits, window->width * 4, 0xFFFF);
+ } else if (message->type == ES_MSG_LAYOUT) {
+ EsElementGetSize(element, &window->width, &window->height);
+ window->bits = (uint32_t *) UI_REALLOC(window->bits, window->width * window->height * 4);
+ window->e.bounds = UI_RECT_2S(window->width, window->height);
+ window->e.clip = UI_RECT_2S(window->width, window->height);
+ UIElementRelayout(&window->e);
+ _UIUpdate();
+ } else if (message->type == ES_MSG_SCROLL_WHEEL) {
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_WHEEL, -message->scrollWheel.dy, 0);
+ } else if (message->type == ES_MSG_MOUSE_MOVED || message->type == ES_MSG_HOVERED_END
+ || message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_RIGHT_DRAG || message->type == ES_MSG_MOUSE_MIDDLE_DRAG) {
+ EsPoint point = EsMouseGetPosition(element);
+ window->cursorX = point.x, window->cursorY = point.y;
+ _UIWindowInputEvent(window, UI_MSG_MOUSE_MOVE, 0, 0);
+ } else if (message->type == ES_MSG_KEY_UP) {
+ window->ctrl = EsKeyboardIsCtrlHeld();
+ window->shift = EsKeyboardIsShiftHeld();
+ window->alt = EsKeyboardIsAltHeld();
+ } else if (message->type == ES_MSG_KEY_DOWN) {
+ window->ctrl = EsKeyboardIsCtrlHeld();
+ window->shift = EsKeyboardIsShiftHeld();
+ window->alt = EsKeyboardIsAltHeld();
+ UIKeyTyped m = { 0 };
+ char c[64];
+ m.text = c;
+ m.textBytes = EsMessageGetInputText(message, c);
+ m.code = message->keyboard.scancode;
+ return _UIWindowInputEvent(window, UI_MSG_KEY_TYPED, 0, &m) ? ES_HANDLED : 0;
+ } else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) {
+ _UIInspectorSetFocusedWindow(window);
+ } else if (message->type == ES_MSG_USER_START) {
+ UIElementMessage(&window->e, (UIMessage) message->user.context1.u, 0, (void *) message->user.context2.p);
+ _UIUpdate();
+ } else if (message->type == ES_MSG_GET_CURSOR) {
+ message->cursorStyle = ES_CURSOR_NORMAL;
+ if (window->cursor == UI_CURSOR_TEXT) message->cursorStyle = ES_CURSOR_TEXT;
+ if (window->cursor == UI_CURSOR_SPLIT_V) message->cursorStyle = ES_CURSOR_SPLIT_VERTICAL;
+ if (window->cursor == UI_CURSOR_SPLIT_H) message->cursorStyle = ES_CURSOR_SPLIT_HORIZONTAL;
+ if (window->cursor == UI_CURSOR_FLIPPED_ARROW) message->cursorStyle = ES_CURSOR_SELECT_LINES;
+ if (window->cursor == UI_CURSOR_CROSS_HAIR) message->cursorStyle = ES_CURSOR_CROSS_HAIR_PICK;
+ if (window->cursor == UI_CURSOR_HAND) message->cursorStyle = ES_CURSOR_HAND_HOVER;
+ if (window->cursor == UI_CURSOR_RESIZE_UP) message->cursorStyle = ES_CURSOR_RESIZE_VERTICAL;
+ if (window->cursor == UI_CURSOR_RESIZE_LEFT) message->cursorStyle = ES_CURSOR_RESIZE_HORIZONTAL;
+ if (window->cursor == UI_CURSOR_RESIZE_UP_RIGHT) message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_1;
+ if (window->cursor == UI_CURSOR_RESIZE_UP_LEFT) message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_2;
+ if (window->cursor == UI_CURSOR_RESIZE_DOWN) message->cursorStyle = ES_CURSOR_RESIZE_VERTICAL;
+ if (window->cursor == UI_CURSOR_RESIZE_RIGHT) message->cursorStyle = ES_CURSOR_RESIZE_HORIZONTAL;
+ if (window->cursor == UI_CURSOR_RESIZE_DOWN_RIGHT) message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_1;
+ if (window->cursor == UI_CURSOR_RESIZE_DOWN_LEFT) message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_2;
+ }
+
+ else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) _UIWindowInputEvent(window, UI_MSG_LEFT_DOWN, 0, 0);
+ else if (message->type == ES_MSG_MOUSE_LEFT_UP) _UIWindowInputEvent(window, UI_MSG_LEFT_UP, 0, 0);
+ else if (message->type == ES_MSG_MOUSE_MIDDLE_DOWN) _UIWindowInputEvent(window, UI_MSG_MIDDLE_DOWN, 0, 0);
+ else if (message->type == ES_MSG_MOUSE_MIDDLE_UP) _UIWindowInputEvent(window, UI_MSG_MIDDLE_UP, 0, 0);
+ else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) _UIWindowInputEvent(window, UI_MSG_RIGHT_DOWN, 0, 0);
+ else if (message->type == ES_MSG_MOUSE_RIGHT_UP) _UIWindowInputEvent(window, UI_MSG_RIGHT_UP, 0, 0);
+
+ else return 0;
+
+ return ES_HANDLED;
+}
+
+UIWindow *UIWindowCreate(UIWindow *owner, uint32_t flags, const char *cTitle, int width, int height) {
+ _UIMenusClose();
+
+ UIWindow *window = (UIWindow *) UIElementCreate(sizeof(UIWindow), NULL, flags | UI_ELEMENT_WINDOW, _UIWindowMessage, "Window");
+ _UIWindowAdd(window);
+ if (owner) window->scale = owner->scale;
+
+ if (flags & UI_WINDOW_MENU) {
+ // TODO.
+ } else {
+ // TODO Non-main windows.
+ window->window = ui.instance->window;
+ window->window->userData = window;
+ window->canvas = EsCustomElementCreate(window->window, ES_CELL_FILL | ES_ELEMENT_FOCUSABLE);
+ window->canvas->messageUser = _UIWindowCanvasMessage;
+ EsWindowSetTitle(window->window, cTitle, -1);
+ EsElementFocus(window->canvas);
+ }
+
+ return window;
+}
+
+void _UIWindowEndPaint(UIWindow *window, UIPainter *painter) {
+ EsElementRepaint(window->canvas, &window->updateRegion);
+}
+
+void _UIWindowSetCursor(UIWindow *window, int cursor) {
+ window->cursor = cursor;
+}
+
+void _UIWindowGetScreenPosition(UIWindow *window, int *_x, int *_y) {
+ EsRectangle r = EsElementGetScreenBounds(window->window);
+ *_x = r.l, *_y = r.t;
+}
+
+void UIWindowPostMessage(UIWindow *window, UIMessage message, void *_dp) {
+ EsMessage m = {};
+ m.type = ES_MSG_USER_START;
+ m.user.context1.u = message;
+ m.user.context2.p = _dp;
+ EsMessagePost(window->canvas, &m);
+}
+
+void _UIClipboardWriteText(UIWindow *window, char *text) {
+ EsClipboardAddText(ES_CLIPBOARD_PRIMARY, text, -1);
+ UI_FREE(text);
+}
+
+char *_UIClipboardReadTextStart(UIWindow *window, size_t *bytes) {
+ return EsClipboardReadText(ES_CLIPBOARD_PRIMARY, bytes, NULL);
+}
+
+void _UIClipboardReadTextEnd(UIWindow *window, char *text) {
+ EsHeapFree(text);
+}
+
+#endif
+
+#ifdef UI_COCOA
+
+// TODO Standard keyboard shortcuts (Command+Q, Command+W).
+
+const int UI_KEYCODE_A = -100; // TODO Keyboard layout support.
+const int UI_KEYCODE_F1 = -70;
+const int UI_KEYCODE_0 = -50;
+const int UI_KEYCODE_INSERT = -30;
+
+const int UI_KEYCODE_BACKSPACE = kVK_Delete;
+const int UI_KEYCODE_DELETE = kVK_ForwardDelete;
+const int UI_KEYCODE_DOWN = kVK_DownArrow;
+const int UI_KEYCODE_END = kVK_End;
+const int UI_KEYCODE_ENTER = kVK_Return;
+const int UI_KEYCODE_ESCAPE = kVK_Escape;
+const int UI_KEYCODE_HOME = kVK_Home;
+const int UI_KEYCODE_LEFT = kVK_LeftArrow;
+const int UI_KEYCODE_RIGHT = kVK_RightArrow;
+const int UI_KEYCODE_SPACE = kVK_Space;
+const int UI_KEYCODE_TAB = kVK_Tab;
+const int UI_KEYCODE_UP = kVK_UpArrow;
+const int UI_KEYCODE_BACKTICK = kVK_ANSI_Grave; // TODO Keyboard layout support.
+const int UI_KEYCODE_PAGE_UP = kVK_PageUp;
+const int UI_KEYCODE_PAGE_DOWN = kVK_PageDown;
+
+int (*_cocoaAppMain)(int, char **);
+int _cocoaArgc;
+char **_cocoaArgv;
+
+struct _UIPostedMessage {
+ UIMessage message;
+ void *dp;
+};
+
+char *_UIUTF8StringFromNSString(NSString *string) {
+ NSUInteger size = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ char *buffer = (char *) UI_MALLOC(size + 1);
+ buffer[size] = 0;
+ [string getBytes:buffer maxLength:size usedLength:NULL encoding:NSUTF8StringEncoding options:0 range:NSMakeRange(0, [string length]) remainingRange:NULL];
+ return buffer;
+}
+
+int _UICocoaRemapKey(int code) {
+ if (code == kVK_ANSI_A) { return UI_KEYCODE_LETTER('A'); }
+ if (code == kVK_ANSI_B) { return UI_KEYCODE_LETTER('B'); }
+ if (code == kVK_ANSI_C) { return UI_KEYCODE_LETTER('C'); }
+ if (code == kVK_ANSI_D) { return UI_KEYCODE_LETTER('D'); }
+ if (code == kVK_ANSI_E) { return UI_KEYCODE_LETTER('E'); }
+ if (code == kVK_ANSI_F) { return UI_KEYCODE_LETTER('F'); }
+ if (code == kVK_ANSI_G) { return UI_KEYCODE_LETTER('G'); }
+ if (code == kVK_ANSI_H) { return UI_KEYCODE_LETTER('H'); }
+ if (code == kVK_ANSI_I) { return UI_KEYCODE_LETTER('I'); }
+ if (code == kVK_ANSI_J) { return UI_KEYCODE_LETTER('J'); }
+ if (code == kVK_ANSI_K) { return UI_KEYCODE_LETTER('K'); }
+ if (code == kVK_ANSI_L) { return UI_KEYCODE_LETTER('L'); }
+ if (code == kVK_ANSI_M) { return UI_KEYCODE_LETTER('M'); }
+ if (code == kVK_ANSI_N) { return UI_KEYCODE_LETTER('N'); }
+ if (code == kVK_ANSI_O) { return UI_KEYCODE_LETTER('O'); }
+ if (code == kVK_ANSI_P) { return UI_KEYCODE_LETTER('P'); }
+ if (code == kVK_ANSI_Q) { return UI_KEYCODE_LETTER('Q'); }
+ if (code == kVK_ANSI_R) { return UI_KEYCODE_LETTER('R'); }
+ if (code == kVK_ANSI_S) { return UI_KEYCODE_LETTER('S'); }
+ if (code == kVK_ANSI_T) { return UI_KEYCODE_LETTER('T'); }
+ if (code == kVK_ANSI_U) { return UI_KEYCODE_LETTER('U'); }
+ if (code == kVK_ANSI_V) { return UI_KEYCODE_LETTER('V'); }
+ if (code == kVK_ANSI_W) { return UI_KEYCODE_LETTER('W'); }
+ if (code == kVK_ANSI_X) { return UI_KEYCODE_LETTER('X'); }
+ if (code == kVK_ANSI_Y) { return UI_KEYCODE_LETTER('Y'); }
+ if (code == kVK_ANSI_Z) { return UI_KEYCODE_LETTER('Z'); }
+
+ if (code == kVK_ANSI_0) { return UI_KEYCODE_DIGIT('0'); }
+ if (code == kVK_ANSI_1) { return UI_KEYCODE_DIGIT('1'); }
+ if (code == kVK_ANSI_2) { return UI_KEYCODE_DIGIT('2'); }
+ if (code == kVK_ANSI_3) { return UI_KEYCODE_DIGIT('3'); }
+ if (code == kVK_ANSI_4) { return UI_KEYCODE_DIGIT('4'); }
+ if (code == kVK_ANSI_5) { return UI_KEYCODE_DIGIT('5'); }
+ if (code == kVK_ANSI_6) { return UI_KEYCODE_DIGIT('6'); }
+ if (code == kVK_ANSI_7) { return UI_KEYCODE_DIGIT('7'); }
+ if (code == kVK_ANSI_8) { return UI_KEYCODE_DIGIT('8'); }
+ if (code == kVK_ANSI_9) { return UI_KEYCODE_DIGIT('9'); }
+
+ if (code == kVK_F1) { return UI_KEYCODE_FKEY( 1); }
+ if (code == kVK_F2) { return UI_KEYCODE_FKEY( 2); }
+ if (code == kVK_F3) { return UI_KEYCODE_FKEY( 3); }
+ if (code == kVK_F4) { return UI_KEYCODE_FKEY( 4); }
+ if (code == kVK_F5) { return UI_KEYCODE_FKEY( 5); }
+ if (code == kVK_F6) { return UI_KEYCODE_FKEY( 6); }
+ if (code == kVK_F7) { return UI_KEYCODE_FKEY( 7); }
+ if (code == kVK_F8) { return UI_KEYCODE_FKEY( 8); }
+ if (code == kVK_F9) { return UI_KEYCODE_FKEY( 9); }
+ if (code == kVK_F10) { return UI_KEYCODE_FKEY(10); }
+ if (code == kVK_F11) { return UI_KEYCODE_FKEY(11); }
+ if (code == kVK_F12) { return UI_KEYCODE_FKEY(12); }
+
+ return code;
+}
+
+@interface UICocoaApplicationDelegate : NSObject<NSApplicationDelegate>
+@end
+
+@interface UICocoaWindowDelegate : NSObject<NSWindowDelegate>
+@property (nonatomic) UIWindow *uiWindow;
+@end
+
+@interface UICocoaMainView : NSView
+- (void)handlePostedMessage:(id)message;
+- (void)eventCommon:(NSEvent *)event;
+@property (nonatomic) UIWindow *uiWindow;
+@end
+
+@implementation UICocoaApplicationDelegate
+- (void)applicationWillFinishLaunching:(NSNotification *)notification {
+ int code = _cocoaAppMain(_cocoaArgc, _cocoaArgv);
+ if (code) exit(code);
+}
+@end
+
+@implementation UICocoaWindowDelegate
+- (void)windowDidBecomeKey:(NSNotification *)notification {
+ UIElementMessage(&_uiWindow->e, UI_MSG_WINDOW_ACTIVATE, 0, 0);
+ _UIUpdate();
+}
+
+- (void)windowDidResize:(NSNotification *)notification {
+ _uiWindow->width = ((UICocoaMainView *) _uiWindow->view).frame.size.width;
+ _uiWindow->height = ((UICocoaMainView *) _uiWindow->view).frame.size.height;
+ _uiWindow->bits = (uint32_t *) UI_REALLOC(_uiWindow->bits, _uiWindow->width * _uiWindow->height * 4);
+ _uiWindow->e.bounds = UI_RECT_2S(_uiWindow->width, _uiWindow->height);
+ _uiWindow->e.clip = UI_RECT_2S(_uiWindow->width, _uiWindow->height);
+ UIElementRelayout(&_uiWindow->e);
+ _UIUpdate();
+}
+@end
+
+@implementation UICocoaMainView
+- (void)handlePostedMessage:(id)_message {
+ _UIPostedMessage *message = (_UIPostedMessage *) _message;
+ _UIWindowInputEvent(_uiWindow, message->message, 0, message->dp);
+ UI_FREE(message);
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)onMenuItemSelected:(NSMenuItem *)menuItem {
+ ((void (*)(void *)) ui.menuData[menuItem.tag * 2 + 0])(ui.menuData[menuItem.tag * 2 + 1]);
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ const unsigned char *data = (const unsigned char *) _uiWindow->bits;
+ NSDrawBitmap(NSMakeRect(0, 0, _uiWindow->width, _uiWindow->height), _uiWindow->width, _uiWindow->height,
+ 8 /* bits per channel */, 4 /* channels per pixel */,
+ 32 /* bits per pixel */, 4 * _uiWindow->width /* bytes per row */, NO /* planar */, YES /* has alpha */,
+ NSDeviceRGBColorSpace /* color space */, &data /* data */);
+}
+
+- (void)eventCommon:(NSEvent *)event {
+ NSPoint cursor = [self convertPoint:[event locationInWindow] fromView:nil];
+ _uiWindow->cursorX = cursor.x, _uiWindow->cursorY = _uiWindow->height - cursor.y - 1;
+ _uiWindow->ctrl = event.modifierFlags & NSEventModifierFlagCommand;
+ _uiWindow->shift = event.modifierFlags & NSEventModifierFlagShift;
+ _uiWindow->alt = event.modifierFlags & NSEventModifierFlagOption;
+}
+
+- (void)keyDown:(NSEvent *)event {
+ [self eventCommon:event];
+ char *text = _UIUTF8StringFromNSString(event.characters);
+ UIKeyTyped m = { .code = _UICocoaRemapKey(event.keyCode), .text = text, .textBytes = (int) strlen(text) };
+ _UIWindowInputEvent(_uiWindow, UI_MSG_KEY_TYPED, 0, &m);
+ UI_FREE(text);
+}
+
+- (void)keyUp:(NSEvent *)event {
+ [self eventCommon:event];
+ UIKeyTyped m = { .code = _UICocoaRemapKey(event.keyCode) };
+ _UIWindowInputEvent(_uiWindow, UI_MSG_KEY_RELEASED, 0, &m);
+}
+
+- (void)mouseMoved:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_MOUSE_MOVE, 0, 0);
+}
+
+- (void)mouseExited:(NSEvent *)event { [self mouseMoved:event]; }
+- (void)flagsChanged:(NSEvent *)event { [self mouseMoved:event]; }
+- (void)mouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
+- (void)rightMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
+- (void)otherMouseDragged:(NSEvent *)event { [self mouseMoved:event]; }
+
+- (void)mouseDown:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_LEFT_DOWN, 0, 0);
+}
+
+- (void)mouseUp:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_LEFT_UP, 0, 0);
+}
+
+- (void)rightMouseDown:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_RIGHT_DOWN, 0, 0);
+}
+
+- (void)rightMouseUp:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_RIGHT_UP, 0, 0);
+}
+
+- (void)otherMouseDown:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_MIDDLE_DOWN, 0, 0);
+}
+
+- (void)otherMouseUp:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_MIDDLE_UP, 0, 0);
+}
+
+- (void)scrollWheel:(NSEvent *)event {
+ [self eventCommon:event];
+ _UIWindowInputEvent(_uiWindow, UI_MSG_MOUSE_WHEEL, -3 * event.deltaY, 0);
+ _UIWindowInputEvent(_uiWindow, UI_MSG_MOUSE_MOVE, 0, 0);
+}
+
+// TODO Animations.
+// TODO Drag and drop.
+// TODO Reporting window close.
+
+@end
+
+int _UIWindowMessage(UIElement *element, UIMessage message, int di, void *dp) {
+ if (message == UI_MSG_DEALLOCATE) {
+ UIWindow *window = (UIWindow *) element;
+ _UIWindowDestroyCommon(window);
+ [window->window close];
+ }
+
+ return _UIWindowMessageCommon(element, message, di, dp);
+}
+
+UIWindow *UIWindowCreate(UIWindow *owner, uint32_t flags, const char *cTitle, int _width, int _height) {
+ _UIMenusClose();
+ UIWindow *window = (UIWindow *) UIElementCreate(sizeof(UIWindow), NULL, flags | UI_ELEMENT_WINDOW, _UIWindowMessage, "Window");
+ _UIWindowAdd(window);
+ if (owner) window->scale = owner->scale;
+
+ NSRect frame = NSMakeRect(0, 0, _width ?: 800, _height ?: 600);
+ NSWindowStyleMask styleMask = NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled;
+ NSWindow *nsWindow = [[NSWindow alloc] initWithContentRect:frame styleMask:styleMask backing:NSBackingStoreBuffered defer:NO];
+ [nsWindow center];
+ [nsWindow setTitle:@(cTitle ?: "untitled")];
+ UICocoaWindowDelegate *delegate = [UICocoaWindowDelegate alloc];
+ [delegate setUiWindow:window];
+ nsWindow.delegate = delegate;
+ UICocoaMainView *view = [UICocoaMainView alloc];
+ window->window = nsWindow;
+ window->view = view;
+ window->width = frame.size.width;
+ window->height = frame.size.height;
+ window->bits = (uint32_t *) UI_REALLOC(window->bits, window->width * window->height * 4);
+ window->e.bounds = UI_RECT_2S(window->width, window->height);
+ window->e.clip = UI_RECT_2S(window->width, window->height);
+ [view setUiWindow:window];
+ [view initWithFrame:frame];
+ nsWindow.contentView = view;
+ [view addTrackingArea:[[NSTrackingArea alloc] initWithRect:frame
+ options:NSTrackingMouseMoved|NSTrackingActiveInKeyWindow|NSTrackingInVisibleRect owner:view userInfo:nil]];
+ [nsWindow setInitialFirstResponder:view];
+ [nsWindow makeKeyAndOrderFront:delegate];
+
+ // TODO UI_WINDOW_MAXIMIZE.
+
+ return window;
+}
+
+void _UIClipboardWriteText(UIWindow *window, char *text) {
+ // TODO Clipboard support.
+}
+
+char *_UIClipboardReadTextStart(UIWindow *window, size_t *bytes) {
+ // TODO Clipboard support.
+ return NULL;
+}
+
+void _UIClipboardReadTextEnd(UIWindow *window, char *text) {
+ UI_FREE(text);
+}
+
+void UIInitialise() {
+ _UIInitialiseCommon();
+}
+
+void _UIWindowSetCursor(UIWindow *window, int cursor) {
+ if (cursor == UI_CURSOR_TEXT) [[NSCursor IBeamCursor] set];
+ else if (cursor == UI_CURSOR_SPLIT_V) [[NSCursor resizeUpDownCursor] set];
+ else if (cursor == UI_CURSOR_SPLIT_H) [[NSCursor resizeLeftRightCursor] set];
+ else if (cursor == UI_CURSOR_FLIPPED_ARROW) [[NSCursor pointingHandCursor] set];
+ else if (cursor == UI_CURSOR_CROSS_HAIR) [[NSCursor crosshairCursor] set];
+ else if (cursor == UI_CURSOR_HAND) [[NSCursor pointingHandCursor] set];
+ else [[NSCursor arrowCursor] set];
+}
+
+void _UIWindowEndPaint(UIWindow *window, UIPainter *painter) {
+ for (int y = painter->clip.t; y < painter->clip.b; y++) {
+ for (int x = painter->clip.l; x < painter->clip.r; x++) {
+ uint32_t *p = &painter->bits[y * painter->width + x];
+ *p = 0xFF000000 | (*p & 0xFF00) | ((*p & 0xFF0000) >> 16) | ((*p & 0xFF) << 16);
+ }
+ }
+
+ [(UICocoaMainView *)window->view setNeedsDisplayInRect:((UICocoaMainView *)window->view).frame];
+}
+
+void _UIWindowGetScreenPosition(UIWindow *window, int *x, int *y) {
+ NSPoint point = [window->window convertPointToScreen:NSMakePoint(0, 0)];
+ *x = point.x, *y = point.y;
+}
+
+UIMenu *UIMenuCreate(UIElement *parent, uint32_t flags) {
+ // TODO Fix the vertical position.
+
+ if (parent->parent) {
+ UIRectangle screenBounds = UIElementScreenBounds(parent);
+ ui.menuX = screenBounds.l;
+ ui.menuY = screenBounds.b;
+ } else {
+ _UIWindowGetScreenPosition(parent->window, &ui.menuX, &ui.menuY);
+ ui.menuX += parent->window->cursorX;
+ ui.menuY += parent->window->cursorY;
+ }
+
+ ui.menuIndex = 0;
+ ui.menuWindow = parent->window;
+
+ NSMenu *menu = [[NSMenu alloc] init];
+ [menu setAutoenablesItems:NO];
+ return menu;
+}
+
+void UIMenuAddItem(UIMenu *menu, uint32_t flags, const char *label, ptrdiff_t labelBytes, void (*invoke)(void *cp), void *cp) {
+ if (ui.menuIndex == 128) return;
+ ui.menuData[ui.menuIndex * 2 + 0] = (void *) invoke;
+ ui.menuData[ui.menuIndex * 2 + 1] = cp;
+ NSString *title = [[NSString alloc] initWithBytes:label length:(labelBytes == -1 ? strlen(label) : labelBytes) encoding:NSUTF8StringEncoding];
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:@selector(onMenuItemSelected:) keyEquivalent:@""];
+ item.tag = ui.menuIndex++;
+ if (flags & UI_BUTTON_CHECKED) [item setState:NSControlStateValueOn];
+ [item setEnabled:((flags & UI_ELEMENT_DISABLED) ? NO : YES)];
+ [item setTarget:(UICocoaMainView *)ui.menuWindow->view];
+ [menu addItem:item];
+ [title release];
+ [item release];
+}
+
+void UIMenuShow(UIMenu *menu) {
+ [menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(ui.menuX, ui.menuY) inView:nil];
+ [menu release];
+}
+
+void UIWindowPack(UIWindow *window, int _width) {
+ int width = _width ? _width : UIElementMessage(window->e.children[0], UI_MSG_GET_WIDTH, 0, 0);
+ int height = UIElementMessage(window->e.children[0], UI_MSG_GET_HEIGHT, width, 0);
+ [window->window setContentSize:NSMakeSize(width, height)];
+}
+
+bool _UIMessageLoopSingle(int *result) {
+ // TODO Modal dialog support.
+ return false;
+}
+
+void UIWindowPostMessage(UIWindow *window, UIMessage _message, void *dp) {
+ _UIPostedMessage *message = (_UIPostedMessage *) UI_MALLOC(sizeof(_UIPostedMessage));
+ message->message = _message;
+ message->dp = dp;
+ [(UICocoaMainView*)window->view performSelectorOnMainThread:@selector(handlePostedMessage:) withObject:(id)message waitUntilDone:NO];
+}
+
+int UICocoaMain(int argc, char **argv, int (*appMain)(int, char **)) {
+ _cocoaArgc = argc, _cocoaArgv = argv, _cocoaAppMain = appMain;
+ NSApplication *application = [NSApplication sharedApplication];
+ application.delegate = [[UICocoaApplicationDelegate alloc] init];
+ return NSApplicationMain(argc, (const char **) argv);
+}
+
+#endif
+
+#endif