raven/libraven.h
2023-05-24 23:03:05 +03:00

2613 lines
83 KiB
C

// This is an automatically generated bundle of the Raven library. Do not edit this file directly.
// Please see the end of the file for licensing information.
//
// This is a header-only bundle of the Raven library. Add this file to your project and
// use `#define RAVEN_IMPLEMENTATION` before including to create the implementation.
// Raven depends on these libraries, please make sure you include them before building:
// xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11
// Compile command example:
// gcc -Wall -Wextra -std=gnu99 $(pkg-config --cflags --libs xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11) -lm main.c -o main
//
// For more information, please see: https://git.hippoz.xyz/hippoz/raven
//
// ----- header files -----
// --- str.h ---
#ifndef _UI__STR_H
#define _UI__STR_H
#include <stdlib.h>
typedef struct UIString {
size_t size;
size_t capacity;
char *data;
} UIString;
int ui_string_insert(UIString *str, const size_t where, const char *d, const size_t dsize);
int ui_string_delete(UIString *str, const size_t where, const size_t amount);
#endif
// --- defs.h ---
#ifndef _UI__DEFS_H
#define _UI__DEFS_H
#ifndef NDEBUG
#define _UI_DEBUG
#endif
#define UI_ENABLE_BRANCH_HINTS
#ifdef UI_ENABLE_BRANCH_HINTS
# ifdef __has_builtin
# if __has_builtin(__builtin_expect)
# define __UI_UTIL_CAN_USE_BRANCH_HINTS
# endif
# endif
#endif
#ifdef __UI_UTIL_CAN_USE_BRANCH_HINTS
# define unlikely(M_expr) __builtin_expect(!!(M_expr), 0)
# define likely(M_expr) __builtin_expect(!!(M_expr), 1)
#else
# define unlikely(M_expr) (M_expr)
# define likely(M_expr) (M_expr)
#endif
#endif // _UI__DEFS_H
// --- color.h ---
#ifndef _UI__COLOR_H
#define _UI__COLOR_H
typedef struct UIRGBA {
double r, g, b, a;
} UIRGBA;
#define UI_HEX_TO_COLOR_NORMAL(hex) ((((hex) & 0xFF0000) >> 16) / 255.0), ((((hex) & 0xFF00) >> 8) / 255.0), (((hex) & 0xFF) / 255.0)
#define UI_HEX_TO_RGBA(hex) (UIRGBA){UI_HEX_TO_COLOR_NORMAL(hex), 1.0}
#endif // _UI__COLOR_H
// --- rect.h ---
#ifndef _UI__RECT_H
#define _UI__RECT_H
#include <stdbool.h>
typedef struct UIRect {
double x, y, w, h;
} UIRect;
bool ui_rect_null(UIRect *r);
bool ui_rect_contains_point(UIRect *r, double x, double y);
bool ui_rect_overlap_rect(UIRect *a, UIRect *b);
UIRect ui_rect_united(UIRect *a, UIRect *b);
bool ui_rect_equals(UIRect *a, UIRect *b);
#endif // _UI__RECT_H
// --- node.h ---
#ifndef _UI__NODE_H
#define _UI__NODE_H
#include <stdint.h>
#include <unistd.h>
// #include "rect.h" // -- commented because of single-header bundle
#include <cairo.h>
#define UI_NODE_COMPUTED_EXTENTS_OK (1)
#define UI_NODE_COMPUTED_EXTENTS_CACHED (2)
enum UIEvent {
UI_EVENT_NONE = 0,
UI_EVENT_ATTACHED,
UI_EVENT_DESTROY,
UI_EVENT_REPAINT,
UI_EVENT_RELAYOUT,
UI_EVENT_GET_EXTENTS,
UI_EVENT_HOVERED,
UI_EVENT_UNHOVERED,
UI_EVENT_PRESSED,
UI_EVENT_UNPRESSED,
UI_EVENT_TIMER_END,
UI_EVENT_BUTTON_LEFT_UPDATE, // d == 1 if pressed, d == 0 if released
UI_EVENT_BUTTON_RIGHT_UPDATE, // d == 1 if pressed, d == 0 if released
UI_EVENT_SCROLL, // p (double*) = delta
UI_EVENT_KEY_DOWN, // d (unsigned int) = keycode, p (struct xkb_state*) = xkb state
UI_EVENT__LAST,
};
static const int UI_EVENT_BUCKET_INDEX[] = {
[UI_EVENT_NONE] = 0,
[UI_EVENT_ATTACHED] = 0,
[UI_EVENT_DESTROY] = 0,
[UI_EVENT_TIMER_END] = 0,
[UI_EVENT_RELAYOUT] = 1,
[UI_EVENT_GET_EXTENTS] = 2,
[UI_EVENT_REPAINT] = 3,
[UI_EVENT_HOVERED] = 4,
[UI_EVENT_UNHOVERED] = 4,
[UI_EVENT_PRESSED] = 5,
[UI_EVENT_UNPRESSED] = 5,
[UI_EVENT_SCROLL] = 5,
[UI_EVENT_BUTTON_LEFT_UPDATE] = 6,
[UI_EVENT_BUTTON_RIGHT_UPDATE] = 6,
[UI_EVENT_KEY_DOWN] = 6,
};
enum UISizePolicy {
UI_SIZE_POLICY_COMPUTED,
UI_SIZE_POLICY_GROW,
UI_SIZE_POLICY_FIXED,
UI_SIZE_POLICY_STATIC
};
enum UIDirection {
UI_DIRECTION_HORIZONTAL,
UI_DIRECTION_VERTICAL
};
typedef struct UINodeDispatchBucket {
int capacity, count;
struct UINode **elements;
} UINodeDispatchBucket;
#define UI_NODE_COMPONENT (1 << 0)
#define UI_NODE_RETAINS_PRESS (1 << 1)
#define UI_NODE_DISABLED (1 << 2)
#define UI_NODE_DISPATCH_BUCKETS (7)
typedef struct UINode {
const char *text;
UIRect rect;
double window_rel_x, window_rel_y;
struct UINode *parent;
struct UINode **nodes;
struct UIWindow *window;
cairo_t *drw;
int nodes_count, nodes_capacity;
uint32_t flags;
enum UISizePolicy width_policy;
enum UISizePolicy height_policy;
bool cached_computed_extents;
UINodeDispatchBucket dispatch_buckets[UI_NODE_DISPATCH_BUCKETS];
int (*handle)(struct UINode *node, enum UIEvent ev, size_t d, void *p);
int (*handle_proto)(struct UINode *node, enum UIEvent ev, size_t d, void *p);
} UINode;
UIRect node_get_computed_rect(UINode *node);
int node_send(UINode *node, enum UIEvent ev, size_t d, void *p);
int node_dispatch(UINode *node, enum UIEvent ev, size_t d, void *p);
UINode *node_by_point(UINode *root, double x, double y);
void node_init(UINode *node);
void node_free(UINode *node);
void node_repaint(UINode *node, UIRect *rect, bool do_group, double window_rel_x, double window_rel_y);
UINode *node_attach(UINode *parent, UINode *node);
UINode *node_new(UINode *parent, const char *text);
void node_dump(UINode *node, int depth);
void node_request_relayout(UINode *node);
void node_notify_subscribe(UINode *node, UINode *target, enum UIEvent *events);
#endif // _UI__NODE_H
// --- background-node.h ---
#ifndef _UI__BACKGROUND_NODE_H
#define _UI__BACKGROUND_NODE_H
// #include "color.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
typedef struct UIBackgroundNode {
UINode node;
UIRGBA normal, hovered, pressed;
double border_radius;
bool update_on_status;
} UIBackgroundNode;
int background_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
UIBackgroundNode *state_background_node_new(UINode *parent, UIRGBA normal, UIRGBA hovered, UIRGBA pressed, double border_radius);
UIBackgroundNode *background_node_new(UINode *parent, UIRGBA normal, double border_radius);
#endif // _UI__BACKGROUND_NODE_H
// --- window.h ---
#ifndef _UI__WINDOW_H
#define _UI__WINDOW_H
// #include "color.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include <cairo/cairo.h>
#include <cairo/cairo-xcb.h>
#include <pango/pangocairo.h>
#include <pango/pango-layout.h>
typedef struct UIWindowTimer {
bool present;
int64_t started_at_ms;
int64_t duration_ms;
UINode *target; // TODO: what if the node gets deleted while the timer is still running?
} UIWindowTimer;
typedef struct UIWindowXKBKeyboard {
uint8_t first_xkb_event;
struct xkb_context *ctx;
struct xkb_keymap *keymap;
struct xkb_state *state;
int32_t device_id;
} UIWindowXKBKeyboard;
#define UI_WINDOW_MAX_TIMERS 4
#define UI_WINDOW_MAX_POLL (1)
typedef struct UIWindow {
struct UINode *root, *hovered, *pressed;
UIRect invalid_region;
xcb_connection_t *_xcb_connection;
xcb_window_t _xcb_window_id;
cairo_surface_t *_cairo_surface;
cairo_t *drw;
UIWindowTimer timers[UI_WINDOW_MAX_TIMERS];
UIWindowXKBKeyboard *xkb_core_keyboard;
} UIWindow;
int window_invalidate_node(UIWindow *window, UINode *node);
void window_free(UIWindow *window);
UIWindow *window_new(int width, int height);
bool window_flush_invalidated(UIWindow *window);
int window_attach_root(UIWindow *window, UINode *root);
int window_turn(UIWindow *window);
UIWindowTimer *window_sched_timer(UIWindow *window, UINode *target, int64_t duration_ms);
#endif // _UI__WINDOW_H
// --- colors.h ---
#ifndef _UI__COLORS_H
#define _UI__COLORS_H
// #include "color.h" // -- commented because of single-header bundle
#define colordef(hex) {UI_HEX_TO_COLOR_NORMAL(hex), 1.0}
// Source: https://tailwindcss.com/docs/customizing-colors
const UIRGBA UISlate50 = colordef(0xf8fafc);
const UIRGBA UISlate100 = colordef(0xf1f5f9);
const UIRGBA UISlate200 = colordef(0xe2e8f0);
const UIRGBA UISlate300 = colordef(0xcbd5e1);
const UIRGBA UISlate400 = colordef(0x94a3b8);
const UIRGBA UISlate500 = colordef(0x64748b);
const UIRGBA UISlate600 = colordef(0x475569);
const UIRGBA UISlate700 = colordef(0x334155);
const UIRGBA UISlate800 = colordef(0x1e293b);
const UIRGBA UISlate900 = colordef(0x0f172a);
const UIRGBA UISlate950 = colordef(0x020617);
const UIRGBA UIGray50 = colordef(0xf9fafb);
const UIRGBA UIGray100 = colordef(0xf3f4f6);
const UIRGBA UIGray200 = colordef(0xe5e7eb);
const UIRGBA UIGray300 = colordef(0xd1d5db);
const UIRGBA UIGray400 = colordef(0x9ca3af);
const UIRGBA UIGray500 = colordef(0x6b7280);
const UIRGBA UIGray600 = colordef(0x4b5563);
const UIRGBA UIGray700 = colordef(0x374151);
const UIRGBA UIGray800 = colordef(0x1f2937);
const UIRGBA UIGray900 = colordef(0x111827);
const UIRGBA UIGray950 = colordef(0x030712);
const UIRGBA UIZinc50 = colordef(0xfafafa);
const UIRGBA UIZinc100 = colordef(0xf4f4f5);
const UIRGBA UIZinc200 = colordef(0xe4e4e7);
const UIRGBA UIZinc300 = colordef(0xd4d4d8);
const UIRGBA UIZinc400 = colordef(0xa1a1aa);
const UIRGBA UIZinc500 = colordef(0x71717a);
const UIRGBA UIZinc600 = colordef(0x52525b);
const UIRGBA UIZinc700 = colordef(0x3f3f46);
const UIRGBA UIZinc800 = colordef(0x27272a);
const UIRGBA UIZinc900 = colordef(0x18181b);
const UIRGBA UIZinc950 = colordef(0x09090b);
const UIRGBA UINeutral50 = colordef(0xfafafa);
const UIRGBA UINeutral100 = colordef(0xf5f5f5);
const UIRGBA UINeutral200 = colordef(0xe5e5e5);
const UIRGBA UINeutral300 = colordef(0xd4d4d4);
const UIRGBA UINeutral400 = colordef(0xa3a3a3);
const UIRGBA UINeutral500 = colordef(0x737373);
const UIRGBA UINeutral600 = colordef(0x525252);
const UIRGBA UINeutral700 = colordef(0x404040);
const UIRGBA UINeutral800 = colordef(0x262626);
const UIRGBA UINeutral900 = colordef(0x171717);
const UIRGBA UINeutral950 = colordef(0x0a0a0a);
const UIRGBA UIStone50 = colordef(0xfafaf9);
const UIRGBA UIStone100 = colordef(0xf5f5f4);
const UIRGBA UIStone200 = colordef(0xe7e5e4);
const UIRGBA UIStone300 = colordef(0xd6d3d1);
const UIRGBA UIStone400 = colordef(0xa8a29e);
const UIRGBA UIStone500 = colordef(0x78716c);
const UIRGBA UIStone600 = colordef(0x57534e);
const UIRGBA UIStone700 = colordef(0x44403c);
const UIRGBA UIStone800 = colordef(0x292524);
const UIRGBA UIStone900 = colordef(0x1c1917);
const UIRGBA UIStone950 = colordef(0x0c0a09);
const UIRGBA UIRed50 = colordef(0xfef2f2);
const UIRGBA UIRed100 = colordef(0xfee2e2);
const UIRGBA UIRed200 = colordef(0xfecaca);
const UIRGBA UIRed300 = colordef(0xfca5a5);
const UIRGBA UIRed400 = colordef(0xf87171);
const UIRGBA UIRed500 = colordef(0xef4444);
const UIRGBA UIRed600 = colordef(0xdc2626);
const UIRGBA UIRed700 = colordef(0xb91c1c);
const UIRGBA UIRed800 = colordef(0x991b1b);
const UIRGBA UIRed900 = colordef(0x7f1d1d);
const UIRGBA UIRed950 = colordef(0x450a0a);
const UIRGBA UIOrange50 = colordef(0xfff7ed);
const UIRGBA UIOrange100 = colordef(0xffedd5);
const UIRGBA UIOrange200 = colordef(0xfed7aa);
const UIRGBA UIOrange300 = colordef(0xfdba74);
const UIRGBA UIOrange400 = colordef(0xfb923c);
const UIRGBA UIOrange500 = colordef(0xf97316);
const UIRGBA UIOrange600 = colordef(0xea580c);
const UIRGBA UIOrange700 = colordef(0xc2410c);
const UIRGBA UIOrange800 = colordef(0x9a3412);
const UIRGBA UIOrange900 = colordef(0x7c2d12);
const UIRGBA UIOrange950 = colordef(0x431407);
const UIRGBA UIAmber50 = colordef(0xfffbeb);
const UIRGBA UIAmber100 = colordef(0xfef3c7);
const UIRGBA UIAmber200 = colordef(0xfde68a);
const UIRGBA UIAmber300 = colordef(0xfcd34d);
const UIRGBA UIAmber400 = colordef(0xfbbf24);
const UIRGBA UIAmber500 = colordef(0xf59e0b);
const UIRGBA UIAmber600 = colordef(0xd97706);
const UIRGBA UIAmber700 = colordef(0xb45309);
const UIRGBA UIAmber800 = colordef(0x92400e);
const UIRGBA UIAmber900 = colordef(0x78350f);
const UIRGBA UIAmber950 = colordef(0x451a03);
const UIRGBA UIYellow50 = colordef(0xfefce8);
const UIRGBA UIYellow100 = colordef(0xfef9c3);
const UIRGBA UIYellow200 = colordef(0xfef08a);
const UIRGBA UIYellow300 = colordef(0xfde047);
const UIRGBA UIYellow400 = colordef(0xfacc15);
const UIRGBA UIYellow500 = colordef(0xeab308);
const UIRGBA UIYellow600 = colordef(0xca8a04);
const UIRGBA UIYellow700 = colordef(0xa16207);
const UIRGBA UIYellow800 = colordef(0x854d0e);
const UIRGBA UIYellow900 = colordef(0x713f12);
const UIRGBA UIYellow950 = colordef(0x422006);
const UIRGBA UILime50 = colordef(0xf7fee7);
const UIRGBA UILime100 = colordef(0xecfccb);
const UIRGBA UILime200 = colordef(0xd9f99d);
const UIRGBA UILime300 = colordef(0xbef264);
const UIRGBA UILime400 = colordef(0xa3e635);
const UIRGBA UILime500 = colordef(0x84cc16);
const UIRGBA UILime600 = colordef(0x65a30d);
const UIRGBA UILime700 = colordef(0x4d7c0f);
const UIRGBA UILime800 = colordef(0x3f6212);
const UIRGBA UILime900 = colordef(0x365314);
const UIRGBA UILime950 = colordef(0x1a2e05);
const UIRGBA UIGreen50 = colordef(0xf0fdf4);
const UIRGBA UIGreen100 = colordef(0xdcfce7);
const UIRGBA UIGreen200 = colordef(0xbbf7d0);
const UIRGBA UIGreen300 = colordef(0x86efac);
const UIRGBA UIGreen400 = colordef(0x4ade80);
const UIRGBA UIGreen500 = colordef(0x22c55e);
const UIRGBA UIGreen600 = colordef(0x16a34a);
const UIRGBA UIGreen700 = colordef(0x15803d);
const UIRGBA UIGreen800 = colordef(0x166534);
const UIRGBA UIGreen900 = colordef(0x14532d);
const UIRGBA UIGreen950 = colordef(0x052e16);
const UIRGBA UIEmerald50 = colordef(0xecfdf5);
const UIRGBA UIEmerald100 = colordef(0xd1fae5);
const UIRGBA UIEmerald200 = colordef(0xa7f3d0);
const UIRGBA UIEmerald300 = colordef(0x6ee7b7);
const UIRGBA UIEmerald400 = colordef(0x34d399);
const UIRGBA UIEmerald500 = colordef(0x10b981);
const UIRGBA UIEmerald600 = colordef(0x059669);
const UIRGBA UIEmerald700 = colordef(0x047857);
const UIRGBA UIEmerald800 = colordef(0x065f46);
const UIRGBA UIEmerald900 = colordef(0x064e3b);
const UIRGBA UIEmerald950 = colordef(0x022c22);
const UIRGBA UITeal50 = colordef(0xf0fdfa);
const UIRGBA UITeal100 = colordef(0xccfbf1);
const UIRGBA UITeal200 = colordef(0x99f6e4);
const UIRGBA UITeal300 = colordef(0x5eead4);
const UIRGBA UITeal400 = colordef(0x2dd4bf);
const UIRGBA UITeal500 = colordef(0x14b8a6);
const UIRGBA UITeal600 = colordef(0x0d9488);
const UIRGBA UITeal700 = colordef(0x0f766e);
const UIRGBA UITeal800 = colordef(0x115e59);
const UIRGBA UITeal900 = colordef(0x134e4a);
const UIRGBA UITeal950 = colordef(0x042f2e);
const UIRGBA UICyan50 = colordef(0xecfeff);
const UIRGBA UICyan100 = colordef(0xcffafe);
const UIRGBA UICyan200 = colordef(0xa5f3fc);
const UIRGBA UICyan300 = colordef(0x67e8f9);
const UIRGBA UICyan400 = colordef(0x22d3ee);
const UIRGBA UICyan500 = colordef(0x06b6d4);
const UIRGBA UICyan600 = colordef(0x0891b2);
const UIRGBA UICyan700 = colordef(0x0e7490);
const UIRGBA UICyan800 = colordef(0x155e75);
const UIRGBA UICyan900 = colordef(0x164e63);
const UIRGBA UICyan950 = colordef(0x083344);
const UIRGBA UISky50 = colordef(0xf0f9ff);
const UIRGBA UISky100 = colordef(0xe0f2fe);
const UIRGBA UISky200 = colordef(0xbae6fd);
const UIRGBA UISky300 = colordef(0x7dd3fc);
const UIRGBA UISky400 = colordef(0x38bdf8);
const UIRGBA UISky500 = colordef(0x0ea5e9);
const UIRGBA UISky600 = colordef(0x0284c7);
const UIRGBA UISky700 = colordef(0x0369a1);
const UIRGBA UISky800 = colordef(0x075985);
const UIRGBA UISky900 = colordef(0x0c4a6e);
const UIRGBA UISky950 = colordef(0x082f49);
const UIRGBA UIBlue50 = colordef(0xeff6ff);
const UIRGBA UIBlue100 = colordef(0xdbeafe);
const UIRGBA UIBlue200 = colordef(0xbfdbfe);
const UIRGBA UIBlue300 = colordef(0x93c5fd);
const UIRGBA UIBlue400 = colordef(0x60a5fa);
const UIRGBA UIBlue500 = colordef(0x3b82f6);
const UIRGBA UIBlue600 = colordef(0x2563eb);
const UIRGBA UIBlue700 = colordef(0x1d4ed8);
const UIRGBA UIBlue800 = colordef(0x1e40af);
const UIRGBA UIBlue900 = colordef(0x1e3a8a);
const UIRGBA UIBlue950 = colordef(0x172554);
const UIRGBA UIIndigo50 = colordef(0xeef2ff);
const UIRGBA UIIndigo100 = colordef(0xe0e7ff);
const UIRGBA UIIndigo200 = colordef(0xc7d2fe);
const UIRGBA UIIndigo300 = colordef(0xa5b4fc);
const UIRGBA UIIndigo400 = colordef(0x818cf8);
const UIRGBA UIIndigo500 = colordef(0x6366f1);
const UIRGBA UIIndigo600 = colordef(0x4f46e5);
const UIRGBA UIIndigo700 = colordef(0x4338ca);
const UIRGBA UIIndigo800 = colordef(0x3730a3);
const UIRGBA UIIndigo900 = colordef(0x312e81);
const UIRGBA UIIndigo950 = colordef(0x1e1b4b);
const UIRGBA UIViolet50 = colordef(0xf5f3ff);
const UIRGBA UIViolet100 = colordef(0xede9fe);
const UIRGBA UIViolet200 = colordef(0xddd6fe);
const UIRGBA UIViolet300 = colordef(0xc4b5fd);
const UIRGBA UIViolet400 = colordef(0xa78bfa);
const UIRGBA UIViolet500 = colordef(0x8b5cf6);
const UIRGBA UIViolet600 = colordef(0x7c3aed);
const UIRGBA UIViolet700 = colordef(0x6d28d9);
const UIRGBA UIViolet800 = colordef(0x5b21b6);
const UIRGBA UIViolet900 = colordef(0x4c1d95);
const UIRGBA UIViolet950 = colordef(0x2e1065);
const UIRGBA UIPurple50 = colordef(0xfaf5ff);
const UIRGBA UIPurple100 = colordef(0xf3e8ff);
const UIRGBA UIPurple200 = colordef(0xe9d5ff);
const UIRGBA UIPurple300 = colordef(0xd8b4fe);
const UIRGBA UIPurple400 = colordef(0xc084fc);
const UIRGBA UIPurple500 = colordef(0xa855f7);
const UIRGBA UIPurple600 = colordef(0x9333ea);
const UIRGBA UIPurple700 = colordef(0x7e22ce);
const UIRGBA UIPurple800 = colordef(0x6b21a8);
const UIRGBA UIPurple900 = colordef(0x581c87);
const UIRGBA UIPurple950 = colordef(0x3b0764);
const UIRGBA UIFuchsia50 = colordef(0xfdf4ff);
const UIRGBA UIFuchsia100 = colordef(0xfae8ff);
const UIRGBA UIFuchsia200 = colordef(0xf5d0fe);
const UIRGBA UIFuchsia300 = colordef(0xf0abfc);
const UIRGBA UIFuchsia400 = colordef(0xe879f9);
const UIRGBA UIFuchsia500 = colordef(0xd946ef);
const UIRGBA UIFuchsia600 = colordef(0xc026d3);
const UIRGBA UIFuchsia700 = colordef(0xa21caf);
const UIRGBA UIFuchsia800 = colordef(0x86198f);
const UIRGBA UIFuchsia900 = colordef(0x701a75);
const UIRGBA UIFuchsia950 = colordef(0x4a044e);
const UIRGBA UIPink50 = colordef(0xfdf2f8);
const UIRGBA UIPink100 = colordef(0xfce7f3);
const UIRGBA UIPink200 = colordef(0xfbcfe8);
const UIRGBA UIPink300 = colordef(0xf9a8d4);
const UIRGBA UIPink400 = colordef(0xf472b6);
const UIRGBA UIPink500 = colordef(0xec4899);
const UIRGBA UIPink600 = colordef(0xdb2777);
const UIRGBA UIPink700 = colordef(0xbe185d);
const UIRGBA UIPink800 = colordef(0x9d174d);
const UIRGBA UIPink900 = colordef(0x831843);
const UIRGBA UIPink950 = colordef(0x500724);
const UIRGBA UIRose50 = colordef(0xfff1f2);
const UIRGBA UIRose100 = colordef(0xffe4e6);
const UIRGBA UIRose200 = colordef(0xfecdd3);
const UIRGBA UIRose300 = colordef(0xfda4af);
const UIRGBA UIRose400 = colordef(0xfb7185);
const UIRGBA UIRose500 = colordef(0xf43f5e);
const UIRGBA UIRose600 = colordef(0xe11d48);
const UIRGBA UIRose700 = colordef(0xbe123c);
const UIRGBA UIRose800 = colordef(0x9f1239);
const UIRGBA UIRose900 = colordef(0x881337);
const UIRGBA UIRose950 = colordef(0x4c0519);
#endif // _UI__COLORS_H
// --- timeutil.h ---
#ifndef _UI__TIMEUTIL_H
#define _UI__TIMEUTIL_H
#include <stdint.h>
int64_t time_current_ms(void);
#endif // _UI__TIMEUTIL_H
// --- scrollable-node.h ---
#ifndef _UI__SCROLL_CONTAINER_NODE_H
#define _UI__SCROLL_CONTAINER_NODE_H
// #include "node.h" // -- commented because of single-header bundle
typedef struct UIScrollableNode {
UINode node;
UINode *target;
double x_scroll, y_scroll;
} UIScrollableNode;
UIScrollableNode *scrollable_new(UINode *parent, UINode *target);
int scrollable_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
#endif // _UI__SCROLL_CONTAINER_NODE_H
// --- dispatcher-node.h ---
#ifndef _UI__DISPATCHER_NODE_H
#define _UI__DISPATCHER_NODE_H
// #include "node.h" // -- commented because of single-header bundle
typedef struct UIDispatcherNode {
UINode node;
enum UIEvent node_event_type;
int event_type;
void *data;
int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p);
} UIDispatcherNode;
UIDispatcherNode *dispatcher_new(UINode *parent, enum UIEvent node_event_type, int event_type, void *data, int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p));
int dispatcher_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
#endif // _UI__DISPATCHER_NODE_H
// --- box-layout-node.h ---
#ifndef _UI__BOX_LAYOUT_NODE_H
#define _UI__BOX_LAYOUT_NODE_H
// #include "node.h" // -- commented because of single-header bundle
enum UIBoxLayoutJustify {
UI_BOX_LAYOUT_JUSTIFY_START,
UI_BOX_LAYOUT_JUSTIFY_CENTER
};
typedef struct UIBoxLayoutNode {
UINode node;
enum UIDirection direction;
enum UIBoxLayoutJustify justify_secondary_dimension;
double margin_top, margin_left, margin_bottom, margin_right, gap;
} UIBoxLayoutNode;
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction);
void box_layout_set_margins(UIBoxLayoutNode *layout, double margin);
int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p);
#endif // _UI__BOX_LAYOUT_NODE_H
// --- text-node.h ---
#ifndef _UI__TEXT_NODE_H
#define _UI__TEXT_NODE_H
// #include "node.h" // -- commented because of single-header bundle
// #include "color.h" // -- commented because of single-header bundle
#include <pango/pango-layout.h>
#include <pango/pango-font.h>
#include <pango/pango-types.h>
typedef struct UITextNode {
UINode node;
PangoFontDescription *desc;
PangoLayout *layout;
char *_pending_text;
UIRGBA color;
ssize_t caret_index;
UINode *caret_node;
bool wrap;
} UITextNode;
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
void text_node_set_text(UITextNode *text_node, char *text);
UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text);
#endif // _UI__TEXT_NODE_H
// --- text-input-node.h ---
#ifndef _UI__TEXT_INPUT_NODE_H
#define _UI__TEXT_INPUT_NODE_H
// #include "node.h" // -- commented because of single-header bundle
// #include "text-node.h" // -- commented because of single-header bundle
// #include "str.h" // -- commented because of single-header bundle
typedef struct UITextInputNode {
UINode node;
UITextNode *text_node;
UIString text;
size_t text_cursor_index;
} UITextInputNode;
UITextInputNode *text_input_new(UINode *parent);
int text_input_handle(UINode *node, enum UIEvent ev, size_t d, void *p);
#endif // _UI__TEXT_INPUT_NODE_H
// ----- end header files -----
// ----- source files -----
#ifdef RAVEN_IMPLEMENTATION
#define RAVEN_IMPLEMENTATION
// --- prof.c ---
// Source: https://github.com/tsoding/wang-tiles/blob/0aebe3bb5d4ded4a163ed597b0cc7790177333e9/src/prof.c
// Licensed under MIT. https://github.com/tsoding/wang-tiles/blob/0aebe3bb5d4ded4a163ed597b0cc7790177333e9/LICENSE
// TODO: document how to use the profiler module @doc
#include <assert.h>
#include <time.h>
#include <stddef.h>
#ifdef PROF
typedef struct {
const char *label;
double elapsed;
size_t size;
} Entry;
typedef struct {
struct timespec begin;
Entry *entry;
} Clock;
// TODO: several active contexts
#define CLOCK_STACK_CAP 256
Clock clock_stack[CLOCK_STACK_CAP];
size_t clock_stack_count = 0;
#define SUMMARY_CAP 1024
Entry summary[SUMMARY_CAP];
size_t summary_count = 0;
// TODO: Profiler: several passes over the same path and compute the average elapsed time
// It would be interesting to explore how to do that. If you do several passes over the
// same path without cleaning the summary you will just pile more entries to the summary.
// We need a way to "rewind" the summary_count so the pass hits the same entries again and
// computes the average time on their elapsed values.
//
// This would be also super useful when we integrate our renderer with a display window. This
// will enable us to display the average renderer performance in real time.
void begin_clock(const char *label)
{
assert(clock_stack_count < CLOCK_STACK_CAP);
assert(summary_count < SUMMARY_CAP);
Entry *e = &summary[summary_count++];
e->label = label;
e->size = 1;
e->elapsed = 0.0;
Clock *c = &clock_stack[clock_stack_count++];
if (clock_gettime(CLOCK_MONOTONIC, &c->begin) < 0) {
fprintf(stderr, "ERROR: could not get current monotonic time: %s\n",
strerror(errno));
exit(1);
}
c->entry = e;
}
void end_clock(void)
{
assert(clock_stack_count > 0);
Clock *c = &clock_stack[--clock_stack_count];
struct timespec end;
if (clock_gettime(CLOCK_MONOTONIC, &end) < 0) {
fprintf(stderr, "ERROR: could not get current monotonic time: %s\n",
strerror(errno));
exit(1);
}
c->entry->elapsed = (end.tv_sec - c->begin.tv_sec) * 1000 + (end.tv_nsec - c->begin.tv_nsec) / 1e+6;
if (clock_stack_count > 0) {
Clock *pc = &clock_stack[clock_stack_count - 1];
pc->entry->size += c->entry->size;
}
}
void render_entry(FILE *stream, ptrdiff_t root, size_t level, size_t line_width)
{
fprintf(stream, "%*s%-*s%.9lfms\n",
(int) level * 2, "",
(int) line_width - (int) level * 2, summary[root].label,
summary[root].elapsed);
size_t size = summary[root].size - 1;
ptrdiff_t child = root + 1;
while (size > 0) {
render_entry(stream, child, level + 1, line_width);
size -= summary[child].size;
child += summary[child].size;
}
}
void render_summary(FILE *stream, size_t line_width)
{
ptrdiff_t root = 0;
while ((size_t) root < summary_count) {
render_entry(stream, root, 0, line_width);
root += summary[root].size;
}
}
size_t estimate_entry_line_width(ptrdiff_t root, size_t level)
{
size_t line_width = 2 * level + strlen(summary[root].label);
size_t size = summary[root].size - 1;
ptrdiff_t child = root + 1;
while (size > 0) {
size_t entry_line_width = estimate_entry_line_width( child, level + 1);
if (entry_line_width > line_width) {
line_width = entry_line_width;
}
size -= summary[child].size;
child += summary[child].size;
}
return line_width;
}
size_t estimate_line_width(void)
{
size_t line_width = 0;
ptrdiff_t root = 0;
while ((size_t) root < summary_count) {
size_t entry_line_width = estimate_entry_line_width(root, 0);
if (entry_line_width > line_width) {
line_width = entry_line_width;
}
root += summary[root].size;
}
return line_width;
}
void clear_summary()
{
clock_stack_count = 0;
summary_count = 0;
}
void dump_summary(FILE *stream)
{
size_t line_width = estimate_line_width();
render_summary(stream, line_width + 2);
clear_summary();
}
#else
#define begin_clock(...)
#define end_clock(...)
#define dump_summary(...)
#define clear_summary(...)
#endif
// --- str.c ---
// #include "str.h" // -- commented because of single-header bundle
#include <string.h>
int ui_string_insert(UIString *str, const size_t where, const char *d, const size_t dsize)
{
size_t required_capacity = str->size + dsize;
if (required_capacity > str->capacity) {
str->data = realloc(str->data, required_capacity + 1);
str->data[required_capacity] = '\0';
str->capacity = required_capacity;
}
memmove(str->data + where + dsize, str->data + where, str->capacity - where - dsize);
memcpy(str->data + where, d, dsize);
str->size += dsize;
return 0;
}
int ui_string_delete(UIString *str, const size_t where, const size_t amount)
{
if (str->data + where + amount > str->data + str->capacity) {
return -1;
}
memmove(str->data + where, str->data + where + amount, str->capacity - where - amount);
str->size -= amount;
str->data[str->size] = '\0';
return 0;
}
// --- rect.c ---
// #include "rect.h" // -- commented because of single-header bundle
#include <stdbool.h>
#define max(a,b) __extension__({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a > _b ? _a : _b; })
#define min(a,b) __extension__({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; })
bool ui_rect_null(UIRect *r)
{
return !(r->x || r->y || r->w || r->h);
}
bool ui_rect_contains_point(UIRect *r, double x, double y)
{
return r->x <= x && r->x + r->w >= x && r->y <= y && r->y + r->h >= y;
}
bool ui_rect_overlap_rect(UIRect *a, UIRect *b)
{
// https://stackoverflow.com/a/306332
return (a->x < b->x + b->w && a->x + a->w > b->x && a->y < b->y + b->h && a->y + a->h > b->y);
}
UIRect ui_rect_united(UIRect *a, UIRect *b)
{
if (ui_rect_null(a)) return *b;
if (ui_rect_null(b)) return *a;
return (UIRect){
.x = min(a->x, b->x),
.y = min(a->y, b->y),
.w = max(a->x + a->w, b->x + b->w),
.h = max(a->y + a->h, b->y + b->h)
};
}
bool ui_rect_equals(UIRect *a, UIRect *b)
{
return a->x == b->x && a->y == b->y && a->w == b->w && a->h == b->h;
}
// --- node.c ---
#include <stdlib.h>
#include <stdio.h>
// #include "defs.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
typedef struct UIWindow UIWindow;
int window_invalidate_node(UIWindow *window, UINode *node);
static inline uint32_t hash_ui_event(enum UIEvent ev, int count)
{
if (ev < UI_EVENT__LAST) {
return UI_EVENT_BUCKET_INDEX[ev];
}
// FNV-1a
uint32_t hash = 0x811c9dc5;
for (size_t i = 0; i < sizeof(enum UIEvent); i++) {
hash ^= ((char*)&ev)[i];
hash *= 0x01000193;
}
return hash % count;
}
UIRect node_get_computed_rect(UINode *node) {
UIRect rect = node->rect;
UIRect extents = node->rect;
if (!node->cached_computed_extents && (node->width_policy == UI_SIZE_POLICY_COMPUTED || node->height_policy == UI_SIZE_POLICY_COMPUTED)) {
if (node_dispatch(node, UI_EVENT_GET_EXTENTS, 0, &extents) == UI_NODE_COMPUTED_EXTENTS_CACHED) {
// Whoever returned UI_NODE_COMPUTED_EXTENTS_CACHED has the responsibility to set
// cached_computed_extents back to false when the cache is no longer valid.
node->cached_computed_extents = true;
}
}
rect.w = node->width_policy == UI_SIZE_POLICY_COMPUTED ? extents.w : rect.w;
rect.h = node->height_policy == UI_SIZE_POLICY_COMPUTED ? extents.h : rect.h;
return rect;
}
int node_send(UINode *node, enum UIEvent ev, size_t d, void *p)
{
if (unlikely(node->flags & UI_NODE_DISABLED)) {
return 0;
}
if (node->handle) {
int result = (node->handle)(node, ev, d, p);
if (result) return result;
}
if (node->handle_proto) {
return (node->handle_proto)(node, ev, d, p);
}
return 0;
}
int node_dispatch(UINode *node, enum UIEvent ev, size_t d, void *p)
{
if (unlikely(!node || node->flags & UI_NODE_DISABLED)) {
return 0;
}
int result = 0;
if (node->handle) {
result = (node->handle)(node, ev, d, p);
if (result) return result;
}
if (node->handle_proto) {
result = (node->handle_proto)(node, ev, d, p);
if (result) return result;
}
uint32_t hash = hash_ui_event(ev, UI_NODE_DISPATCH_BUCKETS);
UINodeDispatchBucket *bucket = &node->dispatch_buckets[hash];
for (int i = 0; i < bucket->count; i++) {
UINode *n = bucket->elements[i];
// We prioritize handle_proto for components
if (n->handle_proto) {
result = (n->handle_proto)(n, ev, d, p);
if (result) return result;
}
if (n->handle) {
result = (n->handle)(n, ev, d, p);
if (result) return result;
}
}
return 0;
}
UINode *node_by_point(UINode *root, double x, double y)
{
for (int i = 0; i < root->nodes_count; i++) {
UINode *node = root->nodes[i];
if (ui_rect_contains_point(&node->rect, x - node->window_rel_x, y - node->window_rel_y) && !(node->flags & UI_NODE_DISABLED)) {
return node_by_point(node, x, y);
}
}
return root;
}
void node_init(UINode *node)
{
node->rect = (UIRect){0, 0, 0, 0};
node->window_rel_x = 0;
node->window_rel_y = 0;
node->parent = NULL;
node->nodes = NULL;
node->window = NULL;
node->drw = NULL;
node->nodes_count = 0;
node->nodes_capacity = 0;
node->flags = 0;
node->width_policy = UI_SIZE_POLICY_COMPUTED;
node->height_policy = UI_SIZE_POLICY_COMPUTED;
node->handle = NULL;
node->handle_proto = NULL;
node->cached_computed_extents = false;
for (int i = 0; i < UI_NODE_DISPATCH_BUCKETS; i++) {
node->dispatch_buckets[i].capacity = 0;
node->dispatch_buckets[i].count = 0;
node->dispatch_buckets[i].elements = NULL;
}
}
static inline void node_add_dispatch_target(UINode *node, enum UIEvent ev, UINode *target)
{
uint32_t index = hash_ui_event(ev, UI_NODE_DISPATCH_BUCKETS);
UINodeDispatchBucket *bucket = &node->dispatch_buckets[index];
if (bucket->count >= bucket->capacity) {
bucket->capacity += 2;
bucket->elements = realloc(bucket->elements, bucket->capacity * sizeof(bucket->elements));
if (!bucket->elements) {
return;
}
}
int node_index = bucket->count++;
bucket->elements[node_index] = target;
}
void node_notify_subscribe(UINode *node, UINode *target, enum UIEvent *event)
{
for (; *event; event++) {
node_add_dispatch_target(node, *event, target);
}
}
void node_free(UINode *node)
{
if (!node) return;
// TODO: properly handle removing this node from its parent
node_dispatch(node, UI_EVENT_DESTROY, 0, NULL);
for (int i = 0; i < node->nodes_count; i++) {
node_free(node->nodes[i]);
}
free(node->nodes);
free(node);
}
void node_repaint(UINode *node, UIRect *rect, bool do_group, double window_rel_x, double window_rel_y)
{
node->window_rel_x = window_rel_x;
node->window_rel_y = window_rel_y;
if (!ui_rect_overlap_rect(rect, &node->rect) && !(node->flags & UI_NODE_COMPONENT)) {
return;
}
if (do_group) {
cairo_rectangle(node->drw, rect->x, rect->y, rect->w, rect->h);
cairo_clip(node->drw);
}
cairo_save(node->drw);
cairo_translate(node->drw, node->rect.x, node->rect.y);
node_send(node, UI_EVENT_REPAINT, 0, 0);
UIRect local_rect = { rect->x - node->rect.x, rect->y - node->rect.y, rect->w, rect->h };
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_DISABLED) {
continue;
}
node_repaint(current, &local_rect, false, window_rel_x + node->rect.x, window_rel_y + node->rect.y);
}
cairo_restore(node->drw);
}
UINode *node_attach(UINode *parent, UINode *node)
{
if (node->parent) {
return NULL;
}
if (parent) {
node->parent = parent;
node->drw = parent->drw;
node->window = parent->window;
if (parent->nodes_count >= parent->nodes_capacity) {
parent->nodes_capacity += 2;
parent->nodes = realloc(parent->nodes, parent->nodes_capacity * sizeof(**parent->nodes));
if (!parent->nodes) {
return NULL;
}
}
int index = parent->nodes_count++;
parent->nodes[index] = node;
node_send(node, UI_EVENT_ATTACHED, 0, 0);
}
return node;
}
UINode *node_new(UINode *parent, const char *text)
{
UINode *node = malloc(sizeof(UINode));
node_init(node);
node->text = text;
node_attach(parent, (UINode*)node);
return node;
}
void node_dump(UINode *node, int depth)
{
printf("%*s%s [%g, %g %gx%g]\n", depth * 2, "", node->text, node->rect.x, node->rect.y, node->rect.w, node->rect.h);
for (int i = 0; i < node->nodes_count; i++) {
node_dump(node->nodes[i], depth + 1);
}
}
void node_request_relayout(UINode *node)
{
UINode *current = NULL;
for (current = node; current->parent && current->width_policy != UI_SIZE_POLICY_STATIC && current->height_policy != UI_SIZE_POLICY_STATIC; current = current->parent);
if (current) {
node_dispatch(current, UI_EVENT_RELAYOUT, 0, NULL);
window_invalidate_node(current->window, current);
}
}
// --- background-node.c ---
// #include "background-node.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "window.h" // -- commented because of single-header bundle
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
int background_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
if (!node->parent) {
return -1;
}
UIBackgroundNode *n = (UIBackgroundNode*)node;
switch (ev) {
case UI_EVENT_ATTACHED: {
node_notify_subscribe(node->parent, node, (enum UIEvent[]){ UI_EVENT_REPAINT, UI_EVENT_HOVERED, UI_EVENT_UNHOVERED, UI_EVENT_PRESSED, UI_EVENT_UNPRESSED, 0 });
break;
}
case UI_EVENT_REPAINT: {
UIRGBA *color = &n->normal;
if (n->update_on_status) {
if (node->window->pressed == node->parent) {
color = &n->pressed;
} else if (node->window->hovered == node->parent) {
color = &n->hovered;
}
}
cairo_set_source_rgba(node->drw, color->r, color->g, color->b, color->a);
if (n->border_radius) {
double w = node->parent->rect.w;
double h = node->parent->rect.h;
double r = n->border_radius;
double deg = M_PI / 180.0;
cairo_new_sub_path(node->drw);
cairo_arc(node->drw, w - r, r, r, -90 * deg, 0 * deg);
cairo_arc(node->drw, w - r, h - r, r, 0 * deg, 90 * deg);
cairo_arc(node->drw, r, h - r, r, 90 * deg, 180 * deg);
cairo_arc(node->drw, r, r, r, 180 * deg, 270 * deg);
cairo_close_path(node->drw);
} else {
cairo_rectangle(node->drw, 0, 0, node->parent->rect.w, node->parent->rect.h);
}
cairo_fill(node->drw);
break;
}
case UI_EVENT_HOVERED: /* through */
case UI_EVENT_UNHOVERED: /* through */
case UI_EVENT_PRESSED: /* through */
case UI_EVENT_UNPRESSED: {
if (n->update_on_status) {
window_invalidate_node(node->window, node);
}
break;
}
default: {
return 0;
}
}
return 0;
}
UIBackgroundNode *state_background_node_new(UINode *parent, UIRGBA normal, UIRGBA hovered, UIRGBA pressed, double border_radius)
{
UIBackgroundNode *n = malloc(sizeof(UIBackgroundNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "background";
n->node.handle_proto = background_node_handle;
n->normal = normal;
n->hovered = hovered;
n->pressed = pressed;
n->update_on_status = true;
n->border_radius = border_radius;
node_attach(parent, (UINode*)n);
return n;
}
UIBackgroundNode *background_node_new(UINode *parent, UIRGBA normal, double border_radius)
{
UIBackgroundNode *n = malloc(sizeof(UIBackgroundNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "background";
n->node.handle_proto = background_node_handle;
n->normal = normal;
n->hovered = normal;
n->pressed = normal;
n->update_on_status = false;
n->border_radius = border_radius;
node_attach(parent, (UINode*)n);
return n;
}
// --- window.c ---
// #include "cairo.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
// #include "timeutil.h" // -- commented because of single-header bundle
// #include "window.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "defs.h" // -- commented because of single-header bundle
#include <xkbcommon/xkbcommon.h>
#ifdef _UI_DEBUG
#define PROF
#endif
// #include "prof.c" // -- commented because of single-header bundle
// #include "time.h" // -- commented because of single-header bundle
#include <stdint.h>
#include <stdlib.h>
#include <sys/poll.h>
#include <time.h>
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#include <xcb/xkb.h>
#include <xkbcommon/xkbcommon-x11.h>
static void window_process_xcb_event(UIWindow *window, xcb_generic_event_t *event);
static int window_select_xkb_events_for_device(UIWindow *window, int32_t device_id);
static int window_keyboard_update_keymap(UIWindow *window, UIWindowXKBKeyboard *kb);
static int window_keyboard_init(UIWindow *window, UIWindowXKBKeyboard *kb, uint8_t first_xkb_event, int32_t device_id, struct xkb_context *ctx);
static int window_init_core_xkb_keyboard(UIWindow *window);
static void window_keyboard_process_event(UIWindow *window, UIWindowXKBKeyboard *kb, xcb_generic_event_t *gevent);
static void window_keyboard_deinit(UIWindowXKBKeyboard *kb);
int window_invalidate_node(UIWindow *window, UINode *node)
{
if (node->flags & UI_NODE_COMPONENT) {
if (!node->parent) {
return -1;
}
node = node->parent;
}
UIRect local_rect = (UIRect){
.x = node->rect.x + node->window_rel_x,
.y = node->rect.y + node->window_rel_y,
.w = node->rect.w,
.h = node->rect.h
};
window->invalid_region = ui_rect_united(&window->invalid_region, &local_rect);
return 0;
}
void window_free(UIWindow *window)
{
if (!window) return;
if (window->_cairo_surface) cairo_surface_destroy(window->_cairo_surface);
if (window->drw) cairo_destroy(window->drw);
if (window->_xcb_connection) xcb_disconnect(window->_xcb_connection);
window_keyboard_deinit(window->xkb_core_keyboard);
node_free(window->root);
free(window);
}
UIWindow *window_new(int width, int height)
{
bool fail = false;
UIWindow *window = NULL;
window = calloc(1, sizeof(UIWindow));
if (!window) {
fail = true;
goto done;
}
window->hovered = NULL;
window->pressed = NULL;
window->root = NULL;
window->invalid_region = (UIRect){ 0, 0, 0, 0 };
window->_cairo_surface = NULL;
window->_xcb_connection = NULL;
window->_xcb_window_id = -1;
window->drw = NULL;
window->_xcb_connection = xcb_connect(NULL, NULL);
if (xcb_connection_has_error(window->_xcb_connection) != 0) {
fail = true;
goto done;
}
const xcb_setup_t *setup = xcb_get_setup(window->_xcb_connection);
xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
xcb_screen_t *screen = iter.data;
xcb_visualtype_t *visualtype = NULL;
for (xcb_depth_iterator_t iter_depth = xcb_screen_allowed_depths_iterator(screen); iter_depth.rem; xcb_depth_next(&iter_depth)) {
for (xcb_visualtype_iterator_t iter_visualtype = xcb_depth_visuals_iterator(iter_depth.data); iter_visualtype.rem; xcb_visualtype_next(&iter_visualtype)) {
if (iter_visualtype.data->visual_id == screen->root_visual) {
visualtype = iter_visualtype.data;
break;
}
}
if (visualtype) break;
}
if (!visualtype) {
fail = true;
goto done;
}
window->_xcb_window_id = xcb_generate_id(window->_xcb_connection);
if (window->_xcb_window_id == (unsigned)-1) {
fail = true;
goto done;
}
xcb_create_window(
window->_xcb_connection, // connection
XCB_COPY_FROM_PARENT, // depth
window->_xcb_window_id, // window id
screen->root, // parent window
0, // x
0, // y
width, // width
height, // height
0, // border_width
XCB_WINDOW_CLASS_INPUT_OUTPUT, // class
screen->root_visual, // visual
XCB_CW_EVENT_MASK, // value mask
(uint32_t[]){ XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_KEY_PRESS } // value list
);
xcb_map_window(window->_xcb_connection, window->_xcb_window_id);
if (xcb_flush(window->_xcb_connection) <= 0) {
fail = true;
goto done;
}
window->_cairo_surface = cairo_xcb_surface_create(window->_xcb_connection, window->_xcb_window_id, visualtype, width, height);
if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS) {
fail = true;
goto done;
}
window->drw = cairo_create(window->_cairo_surface);
if (cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) {
fail = true;
goto done;
}
if (window_init_core_xkb_keyboard(window) < 0) {
fprintf(stderr, "err: window_new: failed to initialize keyboard (_window_init_core_xkb_keyboard failed). Keyboard input may not be available.\n");
}
done:
if (fail) {
window_free(window);
return NULL;
}
return window;
}
bool window_flush_invalidated(UIWindow *window)
{
if (ui_rect_null(&window->invalid_region)) {
return false;
}
begin_clock("Flush invalidated widgets");
cairo_push_group(window->drw);
node_repaint(window->root, &window->invalid_region, true, 0, 0);
window->invalid_region = (UIRect){ 0, 0, 0, 0 };
cairo_pop_group_to_source(window->drw);
cairo_paint(window->drw);
cairo_surface_flush(window->_cairo_surface);
end_clock();
return true;
}
int window_attach_root(UIWindow *window, UINode *root)
{
if (!window || !root || window->root || !window->drw) {
return -1;
}
root->window = window;
root->drw = window->drw;
root->width_policy = UI_SIZE_POLICY_STATIC;
root->height_policy = UI_SIZE_POLICY_STATIC;
window->root = root;
return 0;
}
// Thanks: https://github.com/xkbcommon/libxkbcommon/blob/003fdee1378382d4fef77089f5f4652cd2422c6a/tools/interactive-x11.c#L67
static int window_select_xkb_events_for_device(UIWindow *window, int32_t device_id)
{
if (!window->_xcb_connection) {
return -1;
}
enum {
required_events =
(XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
XCB_XKB_EVENT_TYPE_MAP_NOTIFY |
XCB_XKB_EVENT_TYPE_STATE_NOTIFY),
required_nkn_details =
(XCB_XKB_NKN_DETAIL_KEYCODES),
required_map_parts =
(XCB_XKB_MAP_PART_KEY_TYPES |
XCB_XKB_MAP_PART_KEY_SYMS |
XCB_XKB_MAP_PART_MODIFIER_MAP |
XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
XCB_XKB_MAP_PART_KEY_ACTIONS |
XCB_XKB_MAP_PART_VIRTUAL_MODS |
XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP),
required_state_details =
(XCB_XKB_STATE_PART_MODIFIER_BASE |
XCB_XKB_STATE_PART_MODIFIER_LATCH |
XCB_XKB_STATE_PART_MODIFIER_LOCK |
XCB_XKB_STATE_PART_GROUP_BASE |
XCB_XKB_STATE_PART_GROUP_LATCH |
XCB_XKB_STATE_PART_GROUP_LOCK),
};
static const xcb_xkb_select_events_details_t details = {
.affectNewKeyboard = required_nkn_details,
.newKeyboardDetails = required_nkn_details,
.affectState = required_state_details,
.stateDetails = required_state_details,
};
xcb_void_cookie_t cookie = xcb_xkb_select_events_aux_checked(
window->_xcb_connection,
device_id,
required_events, /* affectWhich */
0, /* clear */
0, /* selectAll */
required_map_parts, /* affectMap */
required_map_parts, /* map */
&details /* details */
);
xcb_generic_error_t *error = xcb_request_check(window->_xcb_connection, cookie);
if (error) {
free(error);
return -1;
}
return 0;
}
static int window_keyboard_update_keymap(UIWindow *window, UIWindowXKBKeyboard *kb)
{
struct xkb_keymap *keymap = xkb_x11_keymap_new_from_device(kb->ctx, window->_xcb_connection, kb->device_id, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (!keymap) {
return -1;
}
struct xkb_state *state = xkb_x11_state_new_from_device(keymap, window->_xcb_connection, kb->device_id);
if (!state) {
xkb_keymap_unref(keymap);
return -1;
}
xkb_state_unref(kb->state);
xkb_keymap_unref(kb->keymap);
kb->state = state;
kb->keymap = keymap;
return 0;
}
static int window_keyboard_init(UIWindow *window, UIWindowXKBKeyboard *kb, uint8_t first_xkb_event, int32_t device_id, struct xkb_context *ctx)
{
kb->first_xkb_event = first_xkb_event;
kb->device_id = device_id;
kb->ctx = ctx;
kb->keymap = NULL;
kb->state = NULL;
if (window_keyboard_update_keymap(window, kb) < 0) {
return -1;
}
if (window_select_xkb_events_for_device(window, device_id) < 0) {
xkb_state_unref(kb->state);
xkb_keymap_unref(kb->keymap);
return -1;
}
return 0;
}
static int window_init_core_xkb_keyboard(UIWindow *window)
{
uint8_t first_xkb_event;
int ret = 0;
UIWindowXKBKeyboard *kb = NULL;
struct xkb_context *ctx = NULL;
if (!xkb_x11_setup_xkb_extension(
window->_xcb_connection,
XKB_X11_MIN_MAJOR_XKB_VERSION,
XKB_X11_MIN_MINOR_XKB_VERSION,
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS,
NULL, NULL, &first_xkb_event, NULL
)) {
fprintf(stderr, "err: _window_init_core_xkb_keyboard: failed to set up xkb extension (xkb_x11_setup_xkb_extension)\n");
return -1;
}
kb = malloc(sizeof(UIWindowXKBKeyboard));
if (!kb) {
fprintf(stderr, "err: _window_init_core_xkb_keyboard: failed to malloc keyboard struct\n");
ret = -1;
goto defer_error;
}
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if (!ctx) {
fprintf(stderr, "err: _window_init_core_xkb_keyboard: failed to create xkb context\n");
ret = -1;
goto defer_error;
}
int32_t core_kbd_device_id = xkb_x11_get_core_keyboard_device_id(window->_xcb_connection);
if (core_kbd_device_id < 0) {
fprintf(stderr, "err: _window_init_core_xkb_keyboard: failed to find xkb core keyboard device_id\n");
ret = -1;
goto defer_error;
}
if (window_keyboard_init(window, kb, first_xkb_event, core_kbd_device_id, ctx) < 0) {
fprintf(stderr, "err: _window_init_core_xkb_keyboard: failed to initialize keyboard (_window_keyboard_init)\n");
ret = -1;
goto defer_error;
}
goto success;
defer_error:
free(kb);
xkb_context_unref(ctx);
return ret;
success:
window->xkb_core_keyboard = kb;
return ret;
}
static void window_keyboard_deinit(UIWindowXKBKeyboard *kb)
{
if (!kb) {
return;
}
xkb_state_unref(kb->state);
xkb_keymap_unref(kb->keymap);
free(kb);
}
static void window_keyboard_process_event(UIWindow *window, UIWindowXKBKeyboard *kb, xcb_generic_event_t *gevent)
{
union xkb_event {
struct {
uint8_t response_type;
uint8_t xkbType;
uint16_t sequence;
xcb_timestamp_t time;
uint8_t deviceID;
} any;
xcb_xkb_new_keyboard_notify_event_t new_keyboard_notify;
xcb_xkb_map_notify_event_t map_notify;
xcb_xkb_state_notify_event_t state_notify;
} *event = (union xkb_event *) gevent;
if (event->any.deviceID != kb->device_id) {
return;
}
switch (event->any.xkbType) {
case XCB_XKB_NEW_KEYBOARD_NOTIFY: {
if (event->new_keyboard_notify.changed && XCB_XKB_NKN_DETAIL_KEYCODES) {
window_keyboard_update_keymap(window, kb);
}
break;
}
case XCB_XKB_MAP_NOTIFY: {
window_keyboard_update_keymap(window, kb);
break;
}
case XCB_XKB_STATE_NOTIFY: {
xkb_state_update_mask(
kb->state,
event->state_notify.baseMods,
event->state_notify.latchedMods,
event->state_notify.lockedMods,
event->state_notify.baseGroup,
event->state_notify.latchedGroup,
event->state_notify.lockedGroup
);
break;
}
}
}
static void window_process_xcb_event(UIWindow *window, xcb_generic_event_t *event)
{
begin_clock("Process XCB event");
uint8_t event_type = event->response_type & ~0x80;
switch (event_type) {
case XCB_EXPOSE: {
begin_clock("XCB_EXPOSE");
xcb_expose_event_t *ev = (xcb_expose_event_t*)event;
UIRect region = { ev->x, ev->y, ev->width, ev->height };
window->invalid_region = ui_rect_united(&window->invalid_region, &region);
window_invalidate_node(window, window->root);
end_clock();
break;
}
case XCB_CONFIGURE_NOTIFY: {
begin_clock("XCB_CONFIGURE_NOTIFY");
xcb_configure_notify_event_t *ev = (xcb_configure_notify_event_t*)event;
UIRect new_rect = { 0, 0, ev->width, ev->height };
if (!ui_rect_equals(&window->root->rect, &new_rect)) {
cairo_xcb_surface_set_size(window->_cairo_surface, ev->width, ev->height);
window->root->rect.x = 0;
window->root->rect.y = 0;
window->root->rect.w = ev->width;
window->root->rect.h = ev->height;
node_dispatch(window->root, UI_EVENT_RELAYOUT, 0, NULL);
window_invalidate_node(window, window->root);
#ifdef _UI_DEBUG
//node_dump(window->root, 0);
#endif
}
end_clock();
break;
}
case XCB_MOTION_NOTIFY: {
begin_clock("XCB_MOTION_NOTIFY");
xcb_motion_notify_event_t *ev = (xcb_motion_notify_event_t*)event;
UINode *node = node_by_point(window->root, ev->event_x, ev->event_y);
if (node && node != window->hovered) {
UINode *previously_hovered = window->hovered;
window->hovered = node;
if (previously_hovered) {
node_dispatch(previously_hovered, UI_EVENT_UNHOVERED, 0, NULL);
}
node_dispatch(node, UI_EVENT_HOVERED, 0, NULL);
}
end_clock();
break;
}
case XCB_KEY_PRESS: {
begin_clock("XCB_KEY_PRESS");
xcb_key_press_event_t *ev = (xcb_key_press_event_t*)event;
if (window->xkb_core_keyboard) {
UINode *n = window->pressed;
while (n) {
if (node_dispatch(n, UI_EVENT_KEY_DOWN, ev->detail, window->xkb_core_keyboard->state) > 0) {
break;
}
n = n->parent;
}
}
end_clock();
break;
}
case XCB_BUTTON_RELEASE: /* through */
case XCB_BUTTON_PRESS: {
begin_clock("XCB_BUTTON_PRESS");
xcb_button_press_event_t *ev = (xcb_button_press_event_t*)event;
UINode *node = node_by_point(window->root, ev->event_x, ev->event_y);
if (node) {
enum UIEvent ui_event = UI_EVENT_BUTTON_LEFT_UPDATE;
bool state = event_type == XCB_BUTTON_PRESS ? true : false;
double delta = 0.0;
switch (ev->detail) {
case XCB_BUTTON_INDEX_3: {
ui_event = UI_EVENT_BUTTON_RIGHT_UPDATE;
break;
}
case XCB_BUTTON_INDEX_4: {
// scroll up
ui_event = UI_EVENT_SCROLL;
delta = 20.0;
break;
}
case XCB_BUTTON_INDEX_5: {
// scroll down
ui_event = UI_EVENT_SCROLL;
delta = -20.0;
break;
}
}
if (!(ui_event == UI_EVENT_SCROLL && !state)) {
UINode *n = node;
while (n) {
if (node_dispatch(n, ui_event, state, &delta) > 0) {
break;
}
n = n->parent;
}
}
if (ui_event == UI_EVENT_BUTTON_LEFT_UPDATE) {
if (state) {
UINode *previously_pressed = window->pressed;
window->pressed = node;
if (previously_pressed) {
node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL);
}
node_dispatch(node, UI_EVENT_PRESSED, 0, NULL);
} else {
UINode *previously_pressed = window->pressed;
if (previously_pressed && !(previously_pressed->flags & UI_NODE_RETAINS_PRESS)) {
window->pressed = NULL;
node_dispatch(previously_pressed, UI_EVENT_UNPRESSED, 0, NULL);
}
}
}
}
end_clock();
break;
}
default: {
if (window->xkb_core_keyboard && event->response_type == window->xkb_core_keyboard->first_xkb_event) {
window_keyboard_process_event(window, window->xkb_core_keyboard, event);
}
break;
}
}
end_clock();
}
int window_turn(UIWindow *window)
{
if (!window || !window->root || !window->_xcb_connection || !window->_cairo_surface || !window->drw) {
return -1;
}
if (xcb_connection_has_error(window->_xcb_connection) != 0) {
return -1;
}
if (cairo_surface_status(window->_cairo_surface) != CAIRO_STATUS_SUCCESS || cairo_status(window->drw) != CAIRO_STATUS_SUCCESS) {
return -1;
}
bool has_looped_once = false;
struct pollfd fds[UI_WINDOW_MAX_POLL] = {0};
for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) {
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
fds[0].fd = xcb_get_file_descriptor(window->_xcb_connection);
fds[0].events = POLLIN;
int64_t frame_peak_ms = 0;
int64_t frame_peak_last_measurement_ms = 0;
for (;;) {
int64_t poll_timeout;
/* compute `poll_timeout` based on active timers */
{
int64_t now = time_current_ms();
int64_t lowest = INT64_MAX;
bool has_timer = false;
for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) {
UIWindowTimer *timer = &window->timers[i];
if (timer->present) {
has_timer = true;
int64_t distance = (timer->started_at_ms + timer->duration_ms) - now;
if (distance < lowest) {
lowest = distance;
}
}
}
if (lowest < 0) {
lowest = 0;
}
poll_timeout = has_timer ? lowest : -1;
if (!has_looped_once) {
// Makes sure that we handle any pending events before entering the loop for the first time
poll_timeout = 0;
}
}
{
if (poll(fds, UI_WINDOW_MAX_POLL, poll_timeout) < 0) {
fprintf(stderr, "err: window_turn(): poll() failed\n");
return -1;
}
}
clear_summary();
begin_clock("Frame");
int64_t frame_start_ms = time_current_ms();
for (int i = 0; i < UI_WINDOW_MAX_POLL; i++) {
if (fds[i].revents & POLLNVAL) {
fprintf(stderr, "err: window_turn(): poll got POLLNVAL\n");
return -1;
}
if (fds[i].revents & POLLERR) {
fprintf(stderr, "err: window_turn(): poll got POLLERR\n");
return -1;
}
if (fds[i].revents & POLLHUP) {
fprintf(stderr, "err: window_turn(): poll got POLLHUP\n");
return -1;
}
if ((i == 0 && fds[i].revents & POLLIN) || !has_looped_once) {
xcb_generic_event_t *xcb_event;
while ((xcb_event = xcb_poll_for_event(window->_xcb_connection))) {
window_process_xcb_event(window, xcb_event);
free(xcb_event);
}
}
}
/* process finished timers */
{
for (int j = 0; j < UI_WINDOW_MAX_TIMERS; j++) {
UIWindowTimer *timer = &window->timers[j];
if (timer->present) {
int64_t now = time_current_ms();
int64_t distance = (timer->started_at_ms + timer->duration_ms) - now;
if (distance <= 0) {
int64_t dt = now - timer->started_at_ms;
#ifdef _UI_DEBUG
printf("timer jitter: %ld\n", dt - timer->duration_ms);
#endif
UINode *target = timer->target;
timer->present = false;
timer->started_at_ms = 0;
timer->duration_ms = 0;
timer->target = NULL;
if (target) {
node_dispatch(target, UI_EVENT_TIMER_END, 0, &dt);
}
}
}
}
}
if (window_flush_invalidated(window)) {
begin_clock("Flush painting results to XCB");
xcb_flush(window->_xcb_connection);
end_clock();
}
has_looped_once = true;
#ifdef _UI_DEBUG
int64_t frame_end_ms = time_current_ms();
int64_t frame_delta_ms = frame_end_ms - frame_start_ms;
if (frame_delta_ms > frame_peak_ms || frame_end_ms - frame_peak_last_measurement_ms >= 3000) {
frame_peak_last_measurement_ms = frame_end_ms;
frame_peak_ms = frame_delta_ms;
printf("peak frametime: %ldms\n", frame_peak_ms);
}
#else
(void)frame_peak_last_measurement_ms;
(void)frame_start_ms;
(void)frame_peak_ms;
#endif
end_clock();
dump_summary(stdout);
}
return 0;
}
UIWindowTimer *window_sched_timer(UIWindow *window, UINode *target, int64_t duration_ms)
{
for (int i = 0; i < UI_WINDOW_MAX_TIMERS; i++) {
if (!window->timers[i].present) {
UIWindowTimer *timer = &window->timers[i];
timer->present = true;
timer->duration_ms = duration_ms;
timer->target = target;
timer->started_at_ms = time_current_ms();
return timer;
}
}
return NULL;
}
// --- timeutil.c ---
// #include "timeutil.h" // -- commented because of single-header bundle
#include <time.h>
int64_t time_current_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
}
// --- scrollable-node.c ---
// #include "scrollable-node.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
// #include "window.h" // -- commented because of single-header bundle
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
UIScrollableNode *scrollable_new(UINode *parent, UINode *target)
{
UIScrollableNode *n = malloc(sizeof(UIScrollableNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "scrollable";
n->node.handle_proto = scrollable_handle;
n->target = target;
n->x_scroll = 0;
n->y_scroll = 0;
node_attach(parent, (UINode*)n);
return n;
}
int scrollable_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
UIScrollableNode *n = (UIScrollableNode *)node;
UINode *target = n->target;
if (!node->parent) {
return 0;
}
// TODO: horizontal scroll
switch (ev) {
case UI_EVENT_ATTACHED: {
node_notify_subscribe(node->parent, node, (enum UIEvent[]){ UI_EVENT_SCROLL, UI_EVENT_RELAYOUT, 0 });
break;
}
case UI_EVENT_SCROLL: {
if (!target) return 0;
double scroll_end = target->rect.h - node->parent->rect.h;
if (scroll_end < 0) scroll_end = 0;
n->y_scroll -= *(double*)p;
if (n->y_scroll < 0) {
n->y_scroll = 0;
}
if (n->y_scroll > scroll_end) {
n->y_scroll = scroll_end;
}
target->rect.x = n->x_scroll ? -n->x_scroll : 0;
target->rect.y = n->y_scroll ? -n->y_scroll : 0;
window_invalidate_node(node->window, node);
break;
}
case UI_EVENT_RELAYOUT: {
if (!target) return 0;
UIRect rect = {0};
node_dispatch(target, UI_EVENT_GET_EXTENTS, 0, &rect);
target->rect.w = target->width_policy == UI_SIZE_POLICY_GROW ? node->parent->rect.w : rect.w;
target->rect.h = target->height_policy == UI_SIZE_POLICY_GROW ? node->parent->rect.h : rect.h;
node_dispatch(target, UI_EVENT_RELAYOUT, 0, NULL);
break;
}
default: {}
}
return 0;
}
// --- dispatcher-node.c ---
#include <stdlib.h>
// #include "dispatcher-node.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
int dispatcher_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
if (!node->parent) {
return -1;
}
UIDispatcherNode *n = (UIDispatcherNode*)node;
if (ev == UI_EVENT_ATTACHED) {
node_notify_subscribe(node->parent, node, (enum UIEvent[]){ n->node_event_type, 0 });
}
if (ev == n->node_event_type) {
(n->handle)(node->parent, n->data, n->event_type, d, p);
}
return 0;
}
UIDispatcherNode *dispatcher_new(UINode *parent, enum UIEvent node_event_type, int event_type, void *data, int (*handle)(struct UINode *node, void *data, int event_type, size_t d, void *p))
{
UIDispatcherNode *n = malloc(sizeof(UIDispatcherNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "dispatcher";
n->node.handle_proto = dispatcher_handle;
n->node_event_type = node_event_type;
n->event_type = event_type;
n->data = data;
n->handle = handle;
node_attach(parent, (UINode*)n);
return n;
}
// --- box-layout-node.c ---
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// #include "box-layout-node.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
int box_layout_handle(UINode *component, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
UIBoxLayoutNode *box_layout_node = (UIBoxLayoutNode*)component;
enum UIDirection direction = box_layout_node->direction;
bool is_horizontal = direction == UI_DIRECTION_HORIZONTAL;
bool is_vertical = !is_horizontal;
double margin_top = box_layout_node->margin_top;
double margin_bottom = box_layout_node->margin_bottom;
double margin_left = box_layout_node->margin_left;
double margin_right = box_layout_node->margin_right;
double gap = box_layout_node->gap;
if (!component->parent) return 0;
UINode *node = component->parent;
double size = 0;
double maximum_secondary_position = 0;
int growing_widgets = 0;
int current_index = 0;
if (ev == UI_EVENT_ATTACHED) {
node_notify_subscribe(component->parent, component, (enum UIEvent[]){ UI_EVENT_RELAYOUT, UI_EVENT_GET_EXTENTS, 0 });
return 0;
}
if (ev == UI_EVENT_RELAYOUT || ev == UI_EVENT_GET_EXTENTS) {
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_COMPONENT) continue;
UIRect current_rect = node_get_computed_rect(current);
current->rect.w = current_rect.w;
current->rect.h = current_rect.h;
if (is_horizontal) {
if (current_rect.h > maximum_secondary_position) {
maximum_secondary_position = current_rect.h;
}
if (current->width_policy == UI_SIZE_POLICY_GROW) {
growing_widgets++;
} else {
size += current_rect.w;
}
} else {
if (current_rect.w > maximum_secondary_position) {
maximum_secondary_position = current_rect.w;
}
if (current->height_policy == UI_SIZE_POLICY_GROW) {
growing_widgets++;
} else {
size += current_rect.h;
}
}
if (current_index) {
size += gap;
}
current_index++;
}
maximum_secondary_position += is_horizontal ? margin_top + margin_bottom : margin_left + margin_right;
size += is_vertical ? margin_top + margin_bottom : margin_left + margin_right;
if (ev == UI_EVENT_GET_EXTENTS) {
((UIRect*)p)->w = is_horizontal ? size : maximum_secondary_position;
((UIRect*)p)->h = is_vertical ? size : maximum_secondary_position;
return UI_NODE_COMPUTED_EXTENTS_OK;
}
// we can distribute growing layout children only if we have a fixed sized policy in the layout primary direction
bool is_fixed_for_primary_direction = (is_horizontal && node->width_policy != UI_SIZE_POLICY_COMPUTED) || (is_vertical && node->height_policy != UI_SIZE_POLICY_COMPUTED);
double primary_direction = is_horizontal ? node->rect.w - margin_left - margin_right : node->rect.h - margin_top - margin_bottom;
bool distributes_growing_widgets = growing_widgets && is_fixed_for_primary_direction;
double size_per_growing_widget = 0;
// growing widgets are evenly sized among the remaining free space
if (distributes_growing_widgets) {
double total_size_for_growing_widgets = primary_direction - size;
if (total_size_for_growing_widgets > 0) {
size_per_growing_widget = floor(total_size_for_growing_widgets / growing_widgets);
} else {
distributes_growing_widgets = false;
}
}
bool is_fixed_for_secondary_direction = (is_horizontal && node->height_policy != UI_SIZE_POLICY_COMPUTED) || (is_vertical && node->width_policy != UI_SIZE_POLICY_COMPUTED);
double secondary_direction = is_horizontal ? node->rect.h - margin_top - margin_bottom : node->rect.w - margin_left - margin_right;
// if our layout secondary direction is fixed, we set the maximum known secondary position to it
if (is_fixed_for_secondary_direction) {
maximum_secondary_position = secondary_direction;
}
double x = margin_left;
double y = margin_top;
current_index = 0;
for (int i = 0; i < node->nodes_count; i++) {
UINode *current = node->nodes[i];
if (current->flags & UI_NODE_COMPONENT) continue;
if (is_horizontal) {
if (current->width_policy == UI_SIZE_POLICY_GROW) {
current->rect.w = distributes_growing_widgets ? size_per_growing_widget : 0;
}
if (current->height_policy == UI_SIZE_POLICY_GROW) {
current->rect.h = maximum_secondary_position;
}
if (current_index) {
x += gap;
}
current->rect.x = x;
if (box_layout_node->justify_secondary_dimension == UI_BOX_LAYOUT_JUSTIFY_CENTER) {
current->rect.y = margin_top + (maximum_secondary_position - current->rect.h) / 2;
} else {
current->rect.y = y;
}
x += current->rect.w;
} else {
if (current->height_policy == UI_SIZE_POLICY_GROW) {
current->rect.h = distributes_growing_widgets ? size_per_growing_widget : 0;
}
if (current->width_policy == UI_SIZE_POLICY_GROW) {
current->rect.w = maximum_secondary_position;
}
if (current_index) {
y += gap;
}
if (box_layout_node->justify_secondary_dimension == UI_BOX_LAYOUT_JUSTIFY_CENTER) {
current->rect.x = margin_left + (maximum_secondary_position - current->rect.w) / 2;
} else {
current->rect.x = x;
}
current->rect.y = y;
y += current->rect.h;
}
node_dispatch(current, UI_EVENT_RELAYOUT, 0, NULL);
current_index++;
}
return 0;
}
return 0;
}
void box_layout_set_margins(UIBoxLayoutNode *layout, double margin)
{
layout->margin_top = margin;
layout->margin_left = margin;
layout->margin_bottom = margin;
layout->margin_right = margin;
}
UIBoxLayoutNode *box_layout_new(UINode *parent, enum UIDirection direction)
{
UIBoxLayoutNode *n = malloc(sizeof(UIBoxLayoutNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->node.text = "box_layout";
n->node.handle_proto = box_layout_handle;
n->direction = direction;
n->margin_top = 0;
n->margin_left = 0;
n->margin_bottom = 0;
n->margin_right = 0;
n->gap = 0;
node_attach(parent, (UINode*)n);
return n;
}
// --- text-node.c ---
#include <pango/pango.h>
#include <pango/pangocairo.h>
// #include "background-node.h" // -- commented because of single-header bundle
// #include "node.h" // -- commented because of single-header bundle
// #include "pango/pango-layout.h" // -- commented because of single-header bundle
// #include "pango/pango-types.h" // -- commented because of single-header bundle
// #include "rect.h" // -- commented because of single-header bundle
// #include "text-node.h" // -- commented because of single-header bundle
UITextNode *text_node_new(UINode *parent, PangoFontDescription *desc, UIRGBA color, char *text)
{
UITextNode *n = malloc(sizeof(UITextNode));
node_init(&n->node);
n->node.flags = UI_NODE_COMPONENT;
n->_pending_text = text;
n->desc = desc;
n->layout = NULL;
n->color = color;
n->caret_index = -1;
n->caret_node = NULL;
n->wrap = false;
n->node.text = "text";
n->node.handle_proto = text_node_handle;
node_attach(parent, (UINode*)n);
return n;
}
void text_node_set_text(UITextNode *text_node, char *text)
{
text_node->_pending_text = text;
text_node->node.cached_computed_extents = false;
}
int text_node_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
(void)d;
(void)p;
if (!node->parent) {
return -1;
}
UITextNode *n = (UITextNode*)node;
if (n->_pending_text) {
if (n->layout) {
pango_layout_set_text(n->layout, n->_pending_text, -1);
pango_cairo_update_layout(node->drw, n->layout);
n->_pending_text = NULL;
} else {
PangoLayout *layout = pango_cairo_create_layout(n->node.drw);
pango_layout_set_font_description(layout, n->desc);
if (n->wrap) {
pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
} else {
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
}
pango_layout_set_text(layout, n->_pending_text, -1);
n->layout = layout;
n->_pending_text = NULL;
}
}
switch (ev) {
case UI_EVENT_ATTACHED: {
node_notify_subscribe(node->parent, node, (enum UIEvent[]){ UI_EVENT_REPAINT, UI_EVENT_GET_EXTENTS, UI_EVENT_RELAYOUT, UI_EVENT_DESTROY, 0 });
break;
}
case UI_EVENT_REPAINT: {
if (!n->layout) {
break;
}
cairo_set_source_rgba(n->node.drw, n->color.r, n->color.g, n->color.b, n->color.a);
pango_cairo_show_layout(n->node.drw, n->layout);
cairo_fill(n->node.drw);
break;
}
case UI_EVENT_GET_EXTENTS: {
if (!n->layout) {
break;
}
int w, h = 0;
pango_layout_get_size(n->layout, &w, &h);
((UIRect*)p)->w = pango_units_to_double(w);
((UIRect*)p)->h = pango_units_to_double(h);
return UI_NODE_COMPUTED_EXTENTS_CACHED;
}
case UI_EVENT_RELAYOUT: {
if (!n->layout) {
break;
}
if (node->parent->width_policy != UI_SIZE_POLICY_COMPUTED) {
pango_layout_set_width(n->layout, pango_units_from_double(node->parent->rect.w));
}
if (node->parent->height_policy != UI_SIZE_POLICY_COMPUTED) {
pango_layout_set_height(n->layout, pango_units_from_double(node->parent->rect.h));
}
if (n->caret_index >= 0) {
if (!n->caret_node) {
n->caret_node = node_new(&n->node, "caret");
background_node_new(n->caret_node, n->color, 0.0);
}
PangoRectangle rect;
pango_layout_get_cursor_pos(n->layout, n->caret_index, &rect, NULL);
n->caret_node->rect = (UIRect){
.x = pango_units_to_double(rect.x),
.y = pango_units_to_double(rect.y),
.w = 1.0,
.h = pango_units_to_double(rect.height),
};
n->caret_node->flags &= ~UI_NODE_DISABLED;
node_dispatch(n->caret_node, UI_EVENT_RELAYOUT, 0, 0);
} else {
if (n->caret_node) {
n->caret_node->flags |= UI_NODE_DISABLED;
}
}
break;
}
case UI_EVENT_DESTROY: {
if (n->layout) {
g_object_unref(n->layout);
}
n->layout = NULL;
n->_pending_text = NULL;
break;
}
default: {
return 0;
}
}
return 0;
}
// --- text-input-node.c ---
#include <stdlib.h>
#include <string.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h>
// #include "node.h" // -- commented because of single-header bundle
// #include "text-node.h" // -- commented because of single-header bundle
// #include "text-input-node.h" // -- commented because of single-header bundle
// #include "str.h" // -- commented because of single-header bundle
UITextInputNode *text_input_new(UINode *parent)
{
UITextInputNode *n = malloc(sizeof(UITextInputNode));
node_init(&n->node);
n->text_node = NULL;
n->node.flags = UI_NODE_RETAINS_PRESS;
n->node.text = "text_input";
n->node.handle_proto = text_input_handle;
n->text = (UIString){0};
n->text_cursor_index = 0;
node_attach(parent, (UINode*)n);
return n;
}
int text_input_handle(UINode *node, enum UIEvent ev, size_t d, void *p)
{
UITextInputNode *n = (UITextInputNode *)node;
switch (ev) {
case UI_EVENT_KEY_DOWN: {
xkb_keycode_t keycode = d;
struct xkb_state *xkb_state = (struct xkb_state*)p;
xkb_keysym_t keysym = xkb_state_key_get_one_sym(xkb_state, keycode);
switch (keysym) {
case XKB_KEY_BackSpace: {
if (n->text_cursor_index > 0) {
ui_string_delete(&n->text, --n->text_cursor_index, 1);
}
break;
}
case XKB_KEY_Delete: {
if (n->text.size > 0 && n->text_cursor_index < n->text.size) {
ui_string_delete(&n->text, n->text_cursor_index, 1);
}
if (n->text_cursor_index > 0 && n->text_cursor_index > n->text.size) {
n->text_cursor_index = n->text.size - 1;
}
break;
}
case XKB_KEY_Home: {
n->text_cursor_index = 0;
break;
}
case XKB_KEY_End: {
n->text_cursor_index = n->text.size;
break;
}
case XKB_KEY_Left: {
if (n->text_cursor_index > 0) {
n->text_cursor_index--;
}
break;
}
case XKB_KEY_Right: {
if (n->text_cursor_index < n->text.size) {
n->text_cursor_index++;
}
break;
}
default: {
size_t input_size = xkb_state_key_get_utf8(xkb_state, keycode, NULL, 0) + 1;
if (input_size <= 1) {
break;
}
char *input_buf = calloc(1, input_size);
xkb_state_key_get_utf8(xkb_state, keycode, input_buf, input_size);
ui_string_insert(&n->text, n->text_cursor_index, input_buf, input_size - 1);
n->text_cursor_index++;
free(input_buf);
break;
}
}
if (n->text_node) {
text_node_set_text(n->text_node, n->text.data);
n->text_node->caret_index = n->text_cursor_index;
n->text_node->wrap = true;
node_request_relayout(&n->text_node->node);
}
return 1;
}
case UI_EVENT_PRESSED: {
if (n->text_node) {
n->text_node->caret_index = n->text_cursor_index;
node_request_relayout(&n->text_node->node);
}
break;
}
case UI_EVENT_UNPRESSED: {
if (n->text_node) {
n->text_node->caret_index = -1;
node_request_relayout(&n->text_node->node);
}
break;
}
default: {
break;
}
}
return 0;
}
#endif
// ----- end source files -----
// MIT License
// Copyright (c) 2023 hippoz
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.