commit cd6065a08d5fc2b94cd2137630d4c0db5d3c692d
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Tue Aug 25 22:04:28 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Tue Aug 25 22:04:28 2026 +0200
Add custom keybindings and Ctrl+O, migrate from raylib to SDL2
- Rebindable keyboard shortcuts via key.* lines in the config file
(e.g. key.save = ctrl+shift+s), covering all Ctrl-chord actions.
- Ctrl+O opens a file, prompting to save first if there are unsaved
changes, with a typed-path modal matching the app's existing UI.
- Replace raylib with SDL2 + SDL2_ttf + SDL2_image as the rendering/
window/input backend. Fixes several keyboard-layout-dependent
shortcut bugs (SDL's event-driven keycodes are layout-aware, unlike
raylib's physical scancodes) and simplifies font handling (SDL2_ttf
re-rasterizes at the exact requested size, no atlas/mipmap workaround
needed). Zoom-in defaults to Ctrl++ rather than Ctrl+=, since "+"
sits unshifted on more keyboard layouts.
---
CMakeLists.txt | 50 ++++-
README.md | 13 +-
assets/lang/en.lang | 13 +-
assets/lang/sv.lang | 13 +-
src/app_mode.h | 11 ++
src/clay_impl.c | 3 +-
src/config.c | 27 ++-
src/config.h | 2 +
src/editor.c | 6 +-
src/editor.h | 9 +-
src/fonts.c | 78 +++-----
src/fonts.h | 26 ++-
src/hittest.c | 21 +-
src/hittest.h | 11 +-
src/keymap.c | 220 +++++++++++++++++++++
src/keymap.h | 76 ++++++++
src/lang.c | 16 +-
src/lang.h | 7 +
src/main.c | 334 ++++++++++++++++++++++++--------
src/render.c | 126 +++++++-----
src/render.h | 31 +--
src/test_main.c | 50 +++++
vendor/clay_renderer_SDL2.c | 431 ++++++++++++++++++++++++++++++++++++++++++
vendor/clay_renderer_raylib.c | 321 -------------------------------
24 files changed, 1338 insertions(+), 557 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1ac4e0e..8e09709 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,17 +12,47 @@ endif()
include(FetchContent)
include(GNUInstallDirs)
-set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
-set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
-set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE)
+set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
+set(SDL_SHARED OFF CACHE BOOL "" FORCE)
+set(SDL_STATIC ON CACHE BOOL "" FORCE)
+set(SDL_TEST OFF CACHE BOOL "" FORCE)
+set(SDL_AUDIO OFF CACHE BOOL "" FORCE)
+set(SDL_JOYSTICK OFF CACHE BOOL "" FORCE)
+set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
+set(SDL_SENSOR OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
- raylib
- GIT_REPOSITORY https://github.com/raysan5/raylib.git
- GIT_TAG 5.5
+ SDL2
+ GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
+ GIT_TAG release-2.32.10
GIT_SHALLOW TRUE
)
-FetchContent_MakeAvailable(raylib)
+FetchContent_MakeAvailable(SDL2)
+
+set(SDL2TTF_VENDORED ON CACHE BOOL "" FORCE)
+set(SDL2TTF_HARFBUZZ OFF CACHE BOOL "" FORCE)
+FetchContent_Declare(
+ SDL2_ttf
+ GIT_REPOSITORY https://github.com/libsdl-org/SDL_ttf.git
+ GIT_TAG release-2.24.0
+ GIT_SHALLOW TRUE
+)
+FetchContent_MakeAvailable(SDL2_ttf)
+
+# SDL2_image is never functionally exercised by hush (no image rendering) -- it's linked solely
+# because the vendored Clay SDL2 renderer's #include <SDL_image.h> needs the headers to resolve.
+# The heavier optional formats are turned off since nothing here ever loads an image.
+set(SDL2IMAGE_VENDORED ON CACHE BOOL "" FORCE)
+set(SDL2IMAGE_AVIF OFF CACHE BOOL "" FORCE)
+set(SDL2IMAGE_TIF OFF CACHE BOOL "" FORCE)
+set(SDL2IMAGE_WEBP OFF CACHE BOOL "" FORCE)
+FetchContent_Declare(
+ SDL2_image
+ GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git
+ GIT_TAG release-2.8.12
+ GIT_SHALLOW TRUE
+)
+FetchContent_MakeAvailable(SDL2_image)
add_executable(hush
src/clay_impl.c
@@ -35,6 +65,7 @@ add_executable(hush
src/hittest.c
src/theme.c
src/undo.c
+ src/keymap.c
src/config.c
src/lang.c
src/paths.c
@@ -45,7 +76,7 @@ add_executable(hush
)
target_include_directories(hush PRIVATE vendor)
-target_link_libraries(hush PRIVATE raylib m)
+target_link_libraries(hush PRIVATE SDL2::SDL2-static SDL2_ttf::SDL2_ttf-static SDL2_image::SDL2_image-static m)
target_compile_definitions(hush PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
if (UNIX AND NOT APPLE)
@@ -76,6 +107,7 @@ add_executable(hush_test
src/hittest.c
src/theme.c
src/undo.c
+ src/keymap.c
src/config.c
src/lang.c
src/paths.c
@@ -85,7 +117,7 @@ add_executable(hush_test
src/test_main.c
)
target_include_directories(hush_test PRIVATE vendor)
-target_link_libraries(hush_test PRIVATE raylib m)
+target_link_libraries(hush_test PRIVATE SDL2::SDL2-static SDL2_ttf::SDL2_ttf-static SDL2_image::SDL2_image-static m)
target_compile_definitions(hush_test PRIVATE ASSETS_DIR="${CMAKE_SOURCE_DIR}/assets")
if (UNIX AND NOT APPLE)
target_link_libraries(hush_test PRIVATE dl pthread)
diff --git a/README.md b/README.md
index 233307b..fe2caa8 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Markdown syntax you type is rendered as styled text as you go, rather than shown
- Text selection (Shift+arrows or click-drag) with copy/paste
- Undo/redo
- A status bar showing the document name, size, and line count
-- A config file for font, text color, and background color
+- A config file for font, text color, background color, zoom, and rebindable keyboard shortcuts
- Translatable UI (Swedish and English included; see [Language](#language) below)
- Full Swedish/Latin-1 character support (åäö, etc.)
@@ -58,12 +58,13 @@ cmake --install build --prefix ~/.local
| Shift + arrow keys | Extend the selection |
| Click + drag | Select text with the mouse |
| Ctrl+S | Save |
+| Ctrl+O | Open a file (prompts to save first if there are unsaved changes) |
| Ctrl+C / Ctrl+V | Copy / paste |
| Ctrl+Z / Ctrl+Shift+Z | Undo / redo |
| Ctrl+X | Quit (prompts to save if there are unsaved changes) |
| F1 (or Ctrl+/) | Show this shortcut list |
| Ctrl+M | View the raw markdown source (read-only; Ctrl+M or Esc to go back) |
-| Ctrl+= / Ctrl+- (or numpad +/-) | Zoom in / out |
+| Ctrl++ / Ctrl+- (or numpad +/-) | Zoom in / out |
| Ctrl+0 (or numpad 0) | Reset zoom to 100% |
| Enter | New line / new block |
| Backspace / Delete | Delete a character (merges/demotes blocks at boundaries) |
@@ -71,6 +72,8 @@ cmake --install build --prefix ~/.local
The window's close button goes through the same unsaved-changes prompt as Ctrl+X.
+Every shortcut above (except plain text-editing keys like the arrows, Enter, and Backspace) can be rebound in the config file with a `key.*` line, e.g. `key.save = ctrl+shift+s`. A binding is `modifier+modifier+key`, where modifiers are any of `ctrl`/`shift`/`alt` and key is a single letter or digit, `f1`-`f12`, punctuation (e.g. `/` or `slash`), or a name like `enter`/`escape`/`tab`/`space`/`home`/`end`/`up`/`down`/`left`/`right`/`pageup`/`pagedown`/`insert`. The numpad zoom fallback and F1 for help always keep working regardless of what's rebound, so there's no way to lock yourself out. See the generated config file (below) for the full list of `key.*` names and their defaults.
+
## Markdown syntax
| Syntax | Result |
@@ -110,9 +113,13 @@ font_mono_regular = /path/to/mono.ttf
font_mono_bold = /path/to/mono-bold.ttf
font_mono_italic = /path/to/mono-italic.ttf
font_mono_bold_italic = /path/to/mono-bold-italic.ttf
+
+key.save = ctrl+s
+key.open = ctrl+o
+key.zoom_in = ctrl++
```
-Font paths are optional per style; any left unset fall back to the bundled DejaVu fonts. `text_color` and `bg_color` accept `#RRGGBB` (the `#` is optional).
+Font paths are optional per style; any left unset fall back to the bundled DejaVu fonts. `text_color` and `bg_color` accept `#RRGGBB` (the `#` is optional). `key.*` lines rebind shortcuts (see Keyboard shortcuts above for the format and the full list of names); the generated file lists all twelve.
## Language
diff --git a/assets/lang/en.lang b/assets/lang/en.lang
index bba09b8..4a3ad0e 100644
--- a/assets/lang/en.lang
+++ b/assets/lang/en.lang
@@ -19,12 +19,23 @@ quit.save = Save and quit
quit.discard = Quit without saving
quit.cancel = Cancel
+# Ctrl+O open-file dialog, shown instead of the above when there are unsaved changes
+quit.title_open = Save changes before opening another file?
+quit.save_open = Save and open
+quit.discard_open = Open without saving
+
+# Ctrl+O open-file dialog
+open.title = Open file
+open.confirm = Open
+open.error = Could not open that file.
+
# F1 / Ctrl+/ shortcuts-help dialog
help.title = Keyboard shortcuts
help.arrows = Arrow keys — move the cursor
help.shift_arrows = Shift + arrow keys — select text
help.mouse_drag = Click + drag — select text
help.save = Ctrl+S — Save
+help.open = Ctrl+O — Open a file
help.copy_paste = Ctrl+C / Ctrl+V — Copy / paste
help.undo_redo = Ctrl+Z / Ctrl+Shift+Z — Undo / redo
help.quit = Ctrl+X — Quit
@@ -33,7 +44,7 @@ help.enter = Enter — New line / new block
help.backspace_delete = Backspace / Delete — Delete a character
help.home_end = Home / End — Start/end of line
help.source_view = Ctrl+M — View raw markdown source
-help.zoom = Ctrl+= / Ctrl+- — Zoom in / out (Ctrl+0 resets)
+help.zoom = Ctrl++ / Ctrl+- — Zoom in / out (Ctrl+0 resets)
help.close = Close
# Shown in the status bar while the raw-source preview (Ctrl+M) is open
diff --git a/assets/lang/sv.lang b/assets/lang/sv.lang
index f559fae..6f699fa 100644
--- a/assets/lang/sv.lang
+++ b/assets/lang/sv.lang
@@ -16,12 +16,23 @@ quit.save = Spara och avsluta
quit.discard = Avsluta utan att spara
quit.cancel = Avbryt
+# Ctrl+O-dialogen (öppna fil), visas istället för ovanstående om det finns osparade ändringar
+quit.title_open = Spara ändringar innan du öppnar en annan fil?
+quit.save_open = Spara och öppna
+quit.discard_open = Öppna utan att spara
+
+# Ctrl+O-dialogen (öppna fil)
+open.title = Öppna fil
+open.confirm = Öppna
+open.error = Kunde inte öppna filen.
+
# Genvägshjälp vid F1 / Ctrl+/
help.title = Kortkommandon
help.arrows = Piltangenter — flytta markören
help.shift_arrows = Shift + piltangenter — markera text
help.mouse_drag = Musklick + drag — markera text
help.save = Ctrl+S — Spara
+help.open = Ctrl+O — Öppna en fil
help.copy_paste = Ctrl+C / Ctrl+V — Kopiera / klistra in
help.undo_redo = Ctrl+Z / Ctrl+Shift+Z — Ångra / gör om
help.quit = Ctrl+X — Avsluta
@@ -30,7 +41,7 @@ help.enter = Enter — Ny rad / nytt block
help.backspace_delete = Backspace / Delete — Ta bort tecken
help.home_end = Home / End — Radens början/slut
help.source_view = Ctrl+M — Visa rå markdown-källkod
-help.zoom = Ctrl+= / Ctrl+- — Zooma in / ut (Ctrl+0 återställer)
+help.zoom = Ctrl++ / Ctrl+- — Zooma in / ut (Ctrl+0 återställer)
help.close = Stäng
# Visas i statusfältet när källkodsvyn (Ctrl+M) är öppen
diff --git a/src/app_mode.h b/src/app_mode.h
index f62ff34..72a2289 100644
--- a/src/app_mode.h
+++ b/src/app_mode.h
@@ -1,6 +1,8 @@
#ifndef HUSH_APP_MODE_H
#define HUSH_APP_MODE_H
+#include <stdbool.h>
+
/* Presentation/flow state -- deliberately kept out of EditorState, which is document-editing
state. Threaded through main.c's loop and into render_frame alongside the other per-frame
values (e.g. caretBlinkT) it already passes explicitly. */
@@ -9,6 +11,7 @@ typedef enum {
MODE_QUIT_CONFIRM,
MODE_SHORTCUTS_HELP,
MODE_SOURCE_VIEW, /* read-only raw-markdown preview, toggled with Ctrl+M */
+ MODE_OPEN_PROMPT, /* typed-path "open a file" modal, toggled with Ctrl+O */
} AppMode;
typedef enum {
@@ -17,6 +20,14 @@ typedef enum {
MODAL_ACTION_DISCARD_QUIT,
MODAL_ACTION_CANCEL,
MODAL_ACTION_CLOSE_HELP,
+ MODAL_ACTION_OPEN_CONFIRM,
} ModalAction;
+/* MODE_OPEN_PROMPT's typed path, owned by main.c and read by render.c to draw the text field. */
+typedef struct {
+ char path[512];
+ int len;
+ bool showError; /* true if the last attempt to open `path` failed */
+} OpenPromptState;
+
#endif
diff --git a/src/clay_impl.c b/src/clay_impl.c
index b22f9f3..c807b5d 100644
--- a/src/clay_impl.c
+++ b/src/clay_impl.c
@@ -1,3 +1,4 @@
+#include "fonts.h" /* SDL2_Font must be visible before the renderer needs it */
#define CLAY_IMPLEMENTATION
#include "clay.h"
-#include "clay_renderer_raylib.c"
+#include "clay_renderer_SDL2.c"
diff --git a/src/config.c b/src/config.c
index 0ed7324..c566e14 100644
--- a/src/config.c
+++ b/src/config.c
@@ -26,6 +26,7 @@ static void config_set_defaults(Config *cfg) {
for (int i = 0; i < FONT_COUNT; i++) cfg->fontPaths[i] = NULL;
cfg->language = dup_str(DEFAULT_LANGUAGE);
cfg->zoom = ZOOM_DEFAULT;
+ keymap_set_defaults(cfg->keymap);
}
void config_free(Config *cfg) {
@@ -122,6 +123,10 @@ bool config_parse_line(const char *line, int len, Config *cfg) {
}
}
+ if (keyLen > 4 && memcmp(key, "key.", 4) == 0) {
+ return keymap_parse_config_line(key, keyLen, value, valueLen, cfg->keymap);
+ }
+
return false;
}
@@ -167,7 +172,7 @@ static void write_default_config(const char *path) {
"# text_color = #%02X%02X%02X\n"
"# bg_color = #%02X%02X%02X\n"
"\n"
- "# Starting zoom level (Ctrl+=/Ctrl+- still adjust it during the session; Ctrl+0 always\n"
+ "# Starting zoom level (Ctrl++/Ctrl+- still adjust it during the session; Ctrl+0 always\n"
"# resets to 100%% regardless of this). Range 0.5-3.0.\n"
"# zoom = 1.0\n"
"\n"
@@ -180,7 +185,25 @@ static void write_default_config(const char *path) {
"# font_mono_regular = /path/to/mono.ttf\n"
"# font_mono_bold = /path/to/mono-bold.ttf\n"
"# font_mono_italic = /path/to/mono-italic.ttf\n"
- "# font_mono_bold_italic = /path/to/mono-bold-italic.ttf\n",
+ "# font_mono_bold_italic = /path/to/mono-bold-italic.ttf\n"
+ "\n"
+ "# Keyboard shortcuts. Each is \"modifier+modifier+key\", where modifiers are any of\n"
+ "# ctrl/shift/alt (in any order) and key is a single letter or digit, f1-f12, punctuation\n"
+ "# (e.g. \"/\" or \"slash\"), or a name like enter/escape/tab/space/home/end/up/down/left/\n"
+ "# right/pageup/pagedown/insert. A numpad fallback (for zoom) and F1 (for help) always\n"
+ "# work too, regardless of what's set here, so there's no way to lock yourself out.\n"
+ "# key.save = ctrl+s\n"
+ "# key.open = ctrl+o\n"
+ "# key.copy = ctrl+c\n"
+ "# key.paste = ctrl+v\n"
+ "# key.undo = ctrl+z\n"
+ "# key.redo = ctrl+shift+z\n"
+ "# key.quit = ctrl+x\n"
+ "# key.help = ctrl+/\n"
+ "# key.source_view = ctrl+m\n"
+ "# key.zoom_in = ctrl++\n"
+ "# key.zoom_out = ctrl+-\n"
+ "# key.zoom_reset = ctrl+0\n",
(unsigned)DEFAULT_COL_TEXT.r, (unsigned)DEFAULT_COL_TEXT.g, (unsigned)DEFAULT_COL_TEXT.b,
(unsigned)DEFAULT_COL_BG.r, (unsigned)DEFAULT_COL_BG.g, (unsigned)DEFAULT_COL_BG.b);
fclose(f);
diff --git a/src/config.h b/src/config.h
index 2b135b2..5d5fc97 100644
--- a/src/config.h
+++ b/src/config.h
@@ -3,6 +3,7 @@
#include "clay.h"
#include "fonts.h"
+#include "keymap.h"
#include <stdbool.h>
typedef struct {
@@ -11,6 +12,7 @@ typedef struct {
char *fontPaths[FONT_COUNT]; /* malloc'd override path per slot, or NULL = bundled default */
char *language; /* malloc'd language code (e.g. "sv"), passed to lang_load(); never NULL */
float zoom; /* starting zoom level (1.0 = 100%); Ctrl+/Ctrl- still adjust it during the session */
+ KeyBinding keymap[ACTION_COUNT]; /* defaults from keymap_set_defaults(), overridable via "key.*" lines */
} Config;
/* Fills `out` with the built-in defaults, then overlays whatever is set in the user's config
diff --git a/src/editor.c b/src/editor.c
index ac5aeb4..4c1e9e5 100644
--- a/src/editor.c
+++ b/src/editor.c
@@ -401,7 +401,7 @@ void editor_move_end(EditorState *ed, bool extend) {
reset_preferred_x(ed);
}
-void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend) {
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, int dir, bool extend) {
if (ed->preferredX < 0.0f) {
float x, y, h;
if (hittest_caret(cache, &ed->doc, fonts, ed->cursorBlock, ed->cursorOffset, &x, &y, &h)) {
@@ -418,7 +418,7 @@ void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int
snap_anchor_to_cursor(ed, extend);
}
-void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+void editor_click(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point) {
HitResult r = hittest_point(cache, &ed->doc, fonts, point);
ed->cursorBlock = r.blockIndex;
ed->cursorOffset = r.offset;
@@ -427,7 +427,7 @@ void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 poin
reset_preferred_x(ed);
}
-void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point) {
+void editor_drag_to(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point) {
HitResult r = hittest_point(cache, &ed->doc, fonts, point);
ed->cursorBlock = r.blockIndex;
ed->cursorOffset = r.offset;
diff --git a/src/editor.h b/src/editor.h
index a073bb5..9f14335 100644
--- a/src/editor.h
+++ b/src/editor.h
@@ -4,7 +4,8 @@
#include "document.h"
#include "layout_cache.h"
#include "undo.h"
-#include "raylib.h"
+#include "fonts.h"
+#include "clay.h"
#include <stdbool.h>
typedef struct {
@@ -49,13 +50,13 @@ void editor_move_left(EditorState *ed, bool extend);
void editor_move_right(EditorState *ed, bool extend);
void editor_move_home(EditorState *ed, bool extend);
void editor_move_end(EditorState *ed, bool extend);
-void editor_move_vertical(EditorState *ed, LayoutCache *cache, Font *fonts, int dir, bool extend);
+void editor_move_vertical(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, int dir, bool extend);
/* Mouse press: moves the caret and (re)establishes the selection anchor there. */
-void editor_click(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+void editor_click(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point);
/* Mouse drag (button held after the initial press): moves only the caret, extending the
selection from whatever editor_click most recently set as the anchor. */
-void editor_drag_to(EditorState *ed, LayoutCache *cache, Font *fonts, Vector2 point);
+void editor_drag_to(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, Clay_Vector2 point);
bool editor_has_selection(const EditorState *ed);
/* Normalizes the anchor/cursor pair into forward document order. */
diff --git a/src/fonts.c b/src/fonts.c
index 55ee51f..c71e482 100644
--- a/src/fonts.c
+++ b/src/fonts.c
@@ -3,66 +3,50 @@
#include <stdlib.h>
#include <stdio.h>
-#define FONT_BASE_SIZE 48
-
-/* ASCII + Latin-1 supplement, so accented characters (e.g. Swedish å ä ö) render correctly,
- plus a couple of symbols used in the UI that fall outside that range. */
-static int *build_codepoints(int *count) {
- int latin1 = 255 - 32 + 1;
- static const int extra[] = {
- 0x2022, /* bullet, used for BLOCK_BULLET markers */
- 0x2014, /* em dash, used in the status bar and modal UI text */
- 0x2610, /* ballot box, unchecked task-list marker */
- 0x2611, /* ballot box with check, checked task-list marker */
- };
- int extraCount = (int)(sizeof(extra) / sizeof(extra[0]));
- int n = latin1 + extraCount;
-
- int *cps = malloc((size_t)n * sizeof(int));
- for (int i = 0; i < latin1; i++) cps[i] = 32 + i;
- for (int i = 0; i < extraCount; i++) cps[latin1 + i] = extra[i];
-
- *count = n;
- return cps;
-}
-
-static void load_one(Font *font, const char *overridePath, const char *defaultFileName, const int *codepoints, int codepointCount) {
+/* Point size TTF_OpenFont is called with -- irrelevant beyond needing to be a valid size, since
+ every real measure/draw call re-sets the actual size via TTF_SetFontSize first (hittest.c's
+ measure_width, render.c's measure_cb and hush_measure_text). Unlike raylib's LoadFontEx (bake
+ one atlas at a large base size, then rely on GPU minification for smaller draws), SDL2_ttf
+ re-rasterizes via FreeType at the exact requested size every time, so there's no
+ baked-atlas/mipmap story here at all. */
+#define FONT_OPEN_SIZE 16
+
+static void load_one(SDL2_Font *font, int fontId, const char *overridePath, const char *defaultFileName) {
char defaultPath[1024];
snprintf(defaultPath, sizeof defaultPath, "%s/fonts/%s", paths_assets_dir(), defaultFileName);
const char *path = (overridePath && overridePath[0]) ? overridePath : defaultPath;
- *font = LoadFontEx(path, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
- if (font->texture.id == 0 && overridePath) {
+ font->fontId = (uint32_t)fontId;
+ font->font = TTF_OpenFont(path, FONT_OPEN_SIZE);
+ if (!font->font && overridePath) {
/* Bad override path (e.g. a typo) -- fall back to the bundled default rather than
leaving this style slot with no usable font. */
- *font = LoadFontEx(defaultPath, FONT_BASE_SIZE, (int *)codepoints, codepointCount);
+ font->font = TTF_OpenFont(defaultPath, FONT_OPEN_SIZE);
}
- if (font->texture.id != 0) {
- /* Glyphs are drawn well below FONT_BASE_SIZE (see theme.h's FS_* sizes), so without
- mipmaps, plain bilinear minification aliases badly ("wrong resolution" look). */
- GenTextureMipmaps(&font->texture);
- SetTextureFilter(font->texture, TEXTURE_FILTER_TRILINEAR);
+ if (!font->font) {
+ /* Only realistically reachable if assets/fonts/ itself is missing or corrupt -- already
+ an unrecoverable state today, just previously masked by raylib's built-in fallback
+ font. SDL2_ttf has no bundled font to fall back to, so fail loudly instead of handing
+ a NULL TTF_Font* to later TTF_SizeUTF8/TTF_RenderUTF8_Blended calls. */
+ fprintf(stderr, "hush: fatal: could not load bundled default font \"%s\": %s\n", defaultPath, TTF_GetError());
+ exit(1);
}
}
-void fonts_load(Font *fonts, char *const *overridePaths) {
- int codepointCount;
- int *codepoints = build_codepoints(&codepointCount);
+void fonts_load(SDL2_Font *fonts, char *const *overridePaths) {
char *none[FONT_COUNT] = {0};
char *const *ov = overridePaths ? overridePaths : none;
- load_one(&fonts[FONT_SANS_REGULAR], ov[FONT_SANS_REGULAR], "DejaVuSans.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_SANS_BOLD], ov[FONT_SANS_BOLD], "DejaVuSans-Bold.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_SANS_ITALIC], ov[FONT_SANS_ITALIC], "DejaVuSans-Oblique.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_SANS_BOLD_ITALIC], ov[FONT_SANS_BOLD_ITALIC], "DejaVuSans-BoldOblique.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_MONO_REGULAR], ov[FONT_MONO_REGULAR], "DejaVuSansMono.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_MONO_BOLD], ov[FONT_MONO_BOLD], "DejaVuSansMono-Bold.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_MONO_ITALIC], ov[FONT_MONO_ITALIC], "DejaVuSansMono-Oblique.ttf", codepoints, codepointCount);
- load_one(&fonts[FONT_MONO_BOLD_ITALIC], ov[FONT_MONO_BOLD_ITALIC], "DejaVuSansMono-BoldOblique.ttf", codepoints, codepointCount);
-
- free(codepoints);
+ load_one(&fonts[FONT_SANS_REGULAR], FONT_SANS_REGULAR, ov[FONT_SANS_REGULAR], "DejaVuSans.ttf");
+ load_one(&fonts[FONT_SANS_BOLD], FONT_SANS_BOLD, ov[FONT_SANS_BOLD], "DejaVuSans-Bold.ttf");
+ load_one(&fonts[FONT_SANS_ITALIC], FONT_SANS_ITALIC, ov[FONT_SANS_ITALIC], "DejaVuSans-Oblique.ttf");
+ load_one(&fonts[FONT_SANS_BOLD_ITALIC], FONT_SANS_BOLD_ITALIC, ov[FONT_SANS_BOLD_ITALIC], "DejaVuSans-BoldOblique.ttf");
+ load_one(&fonts[FONT_MONO_REGULAR], FONT_MONO_REGULAR, ov[FONT_MONO_REGULAR], "DejaVuSansMono.ttf");
+ load_one(&fonts[FONT_MONO_BOLD], FONT_MONO_BOLD, ov[FONT_MONO_BOLD], "DejaVuSansMono-Bold.ttf");
+ load_one(&fonts[FONT_MONO_ITALIC], FONT_MONO_ITALIC, ov[FONT_MONO_ITALIC], "DejaVuSansMono-Oblique.ttf");
+ load_one(&fonts[FONT_MONO_BOLD_ITALIC], FONT_MONO_BOLD_ITALIC, ov[FONT_MONO_BOLD_ITALIC], "DejaVuSansMono-BoldOblique.ttf");
}
-void fonts_unload(Font *fonts) {
- for (int i = 0; i < FONT_COUNT; i++) UnloadFont(fonts[i]);
+void fonts_unload(SDL2_Font *fonts) {
+ for (int i = 0; i < FONT_COUNT; i++) TTF_CloseFont(fonts[i].font);
}
diff --git a/src/fonts.h b/src/fonts.h
index 6b352f6..52b1003 100644
--- a/src/fonts.h
+++ b/src/fonts.h
@@ -1,7 +1,8 @@
#ifndef HUSH_FONTS_H
#define HUSH_FONTS_H
-#include "raylib.h"
+#include <SDL_ttf.h>
+#include <stdint.h>
#define FONT_SANS_REGULAR 0
#define FONT_SANS_BOLD 1
@@ -13,13 +14,22 @@
#define FONT_MONO_BOLD_ITALIC 7
#define FONT_COUNT 8
-/* fonts[] must have room for FONT_COUNT entries. Loaded at a large base size so that
- scaling down for on-screen font sizes stays sharp. `overridePaths[i]`, if non-NULL,
- replaces the bundled default for that slot (see font_index/FONT_* above); pass NULL for
- the whole array to use only the bundled defaults. A bad override path falls back to the
- bundled default rather than leaving an unusable font. */
-void fonts_load(Font *fonts, char *const *overridePaths);
-void fonts_unload(Font *fonts);
+/* The exact shape Clay's SDL2 renderer expects for its font-array parameter (see
+ vendor/clay_renderer_SDL2.c) -- defined here, the canonical single copy, rather than in that
+ vendored file, so fonts.c/editor.h/hittest.h/render.h can all share it. `font` is opened once
+ at an arbitrary placeholder size; every actual measure/draw call re-sets the real size via
+ TTF_SetFontSize first (see hittest.c/render.c), so the size it was opened at is irrelevant. */
+typedef struct {
+ uint32_t fontId;
+ TTF_Font *font;
+} SDL2_Font;
+
+/* fonts[] must have room for FONT_COUNT entries. `overridePaths[i]`, if non-NULL, replaces the
+ bundled default for that slot (see font_index/FONT_* above); pass NULL for the whole array to
+ use only the bundled defaults. A bad override path falls back to the bundled default rather
+ than leaving an unusable font. */
+void fonts_load(SDL2_Font *fonts, char *const *overridePaths);
+void fonts_unload(SDL2_Font *fonts);
static inline int font_index(int mono, int bold, int italic) {
return (mono ? FONT_MONO_REGULAR : FONT_SANS_REGULAR) + (bold ? 1 : 0) + (italic ? 2 : 0);
diff --git a/src/hittest.c b/src/hittest.c
index 7c732d4..2500260 100644
--- a/src/hittest.c
+++ b/src/hittest.c
@@ -6,14 +6,17 @@
#define MEASURE_BUF_SIZE 2048
-static float measure_width(Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
+static float measure_width(SDL2_Font *fonts, int fontId, int fontSize, const char *text, int start, int n) {
if (n <= 0) return 0.0f;
char buf[MEASURE_BUF_SIZE];
if (n >= MEASURE_BUF_SIZE) n = MEASURE_BUF_SIZE - 1;
memcpy(buf, text + start, (size_t)n);
buf[n] = '\0';
- Vector2 sz = MeasureTextEx(fonts[fontId], buf, (float)fontSize, 0.0f);
- return sz.x;
+ TTF_Font *font = fonts[fontId].font;
+ TTF_SetFontSize(font, fontSize);
+ int w = 0, h = 0;
+ TTF_SizeUTF8(font, buf, &w, &h);
+ return (float)w;
}
static Clay_ElementData get_line_data(int globalLineIndex) {
@@ -38,7 +41,7 @@ static int find_line_for_offset(LayoutCache *cache, int blockIndex, int offset)
}
/* Finds the byte offset within `lineIdx` whose glyph is closest to screen-space `targetX`. */
-static int find_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx, float targetX) {
+static int find_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx, float targetX) {
CachedLine *line = &cache->lines[lineIdx];
if (line->segStart == line->segEnd) return line->srcStart;
@@ -77,7 +80,7 @@ static int find_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, i
return seg->srcEnd;
}
-HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point) {
+HitResult hittest_point(LayoutCache *cache, Document *doc, SDL2_Font *fonts, Clay_Vector2 point) {
HitResult r = {0, 0};
if (cache->lineCount == 0 || doc->count == 0) return r;
@@ -102,7 +105,7 @@ HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2
return r;
}
-VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float preferredX, int dir) {
VerticalMoveResult r = {0, 0, 0};
int curLine = find_line_for_offset(cache, blockIndex, offset);
if (curLine < 0) return r;
@@ -117,7 +120,7 @@ VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fon
/* Screen-space x of byte `offset` within visual line `line` (whose Clay element data,
`lineData`, the caller already has). Shared by hittest_caret and hittest_line_range_box. */
-static float x_at_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
+static float x_at_offset_in_line(LayoutCache *cache, Document *doc, SDL2_Font *fonts, CachedLine *line, Clay_ElementData lineData, int offset) {
if (line->segStart == line->segEnd) return lineData.boundingBox.x;
int bestSeg = -1;
@@ -139,7 +142,7 @@ static float x_at_offset_in_line(LayoutCache *cache, Document *doc, Font *fonts,
return sd.boundingBox.x + w;
}
-int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
+int hittest_caret(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight) {
int lineIdx = find_line_for_offset(cache, blockIndex, offset);
if (lineIdx < 0) return 0;
CachedLine *line = &cache->lines[lineIdx];
@@ -152,7 +155,7 @@ int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex
return 1;
}
-int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+int hittest_line_range_box(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx,
int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight) {
if (lineIdx < 0 || lineIdx >= cache->lineCount) return 0;
CachedLine *line = &cache->lines[lineIdx];
diff --git a/src/hittest.h b/src/hittest.h
index 571f24e..c5a6dbb 100644
--- a/src/hittest.h
+++ b/src/hittest.h
@@ -3,7 +3,8 @@
#include "layout_cache.h"
#include "document.h"
-#include "raylib.h"
+#include "fonts.h"
+#include "clay.h"
typedef struct {
int blockIndex;
@@ -12,7 +13,7 @@ typedef struct {
/* Maps a screen-space point to the nearest (block, byte offset), using the Clay element
positions computed for the layout built during the previous frame. */
-HitResult hittest_point(LayoutCache *cache, Document *doc, Font *fonts, Vector2 point);
+HitResult hittest_point(LayoutCache *cache, Document *doc, SDL2_Font *fonts, Clay_Vector2 point);
typedef struct {
int found;
@@ -22,17 +23,17 @@ typedef struct {
/* Moves the cursor to the visual line above (dir<0) or below (dir>0), trying to land as
close as possible to `preferredX` (screen-space x). */
-VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float preferredX, int dir);
+VerticalMoveResult hittest_vertical(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float preferredX, int dir);
/* Fills the caret's screen box for (blockIndex, offset). Returns 0 if the layout has no
matching entry yet (e.g. very first frame). */
-int hittest_caret(LayoutCache *cache, Document *doc, Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight);
+int hittest_caret(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int blockIndex, int offset, float *outX, float *outY, float *outHeight);
/* Fills the screen-space box covering [rangeStart, rangeEnd) (byte offsets within the line's
block) as visible on visual line `lineIdx`, clamped to that line's own byte range. Returns 0
if the range doesn't intersect the line or the layout has no matching entry yet. Used to draw
the selection highlight, one rectangle per intersected visual line. */
-int hittest_line_range_box(LayoutCache *cache, Document *doc, Font *fonts, int lineIdx,
+int hittest_line_range_box(LayoutCache *cache, Document *doc, SDL2_Font *fonts, int lineIdx,
int rangeStart, int rangeEnd, float *outX, float *outY, float *outW, float *outHeight);
#endif
diff --git a/src/keymap.c b/src/keymap.c
new file mode 100644
index 0000000..8a195de
--- /dev/null
+++ b/src/keymap.c
@@ -0,0 +1,220 @@
+#include "keymap.h"
+#include <string.h>
+#include <ctype.h>
+#include <stddef.h>
+
+KeyBinding g_keymap[ACTION_COUNT];
+
+void keymap_set_defaults(KeyBinding *out) {
+ out[ACTION_SAVE] = (KeyBinding){ SDLK_s, true, false, false };
+ out[ACTION_OPEN] = (KeyBinding){ SDLK_o, true, false, false };
+ out[ACTION_COPY] = (KeyBinding){ SDLK_c, true, false, false };
+ out[ACTION_PASTE] = (KeyBinding){ SDLK_v, true, false, false };
+ out[ACTION_UNDO] = (KeyBinding){ SDLK_z, true, false, false };
+ out[ACTION_REDO] = (KeyBinding){ SDLK_z, true, true, false };
+ out[ACTION_QUIT] = (KeyBinding){ SDLK_x, true, false, false };
+ out[ACTION_HELP] = (KeyBinding){ SDLK_SLASH, true, false, false };
+ out[ACTION_SOURCE_VIEW] = (KeyBinding){ SDLK_m, true, false, false };
+ out[ACTION_ZOOM_IN] = (KeyBinding){ SDLK_PLUS, true, false, false };
+ out[ACTION_ZOOM_OUT] = (KeyBinding){ SDLK_MINUS, true, false, false };
+ out[ACTION_ZOOM_RESET] = (KeyBinding){ SDLK_0, true, false, false };
+}
+
+static const char *trim(const char *s, int len, int *outLen) {
+ while (len > 0 && isspace((unsigned char)s[0])) { s++; len--; }
+ while (len > 0 && isspace((unsigned char)s[len - 1])) len--;
+ *outLen = len;
+ return s;
+}
+
+static bool token_is(const char *tok, int len, const char *name) {
+ size_t n = strlen(name);
+ if ((size_t)len != n) return false;
+ for (size_t i = 0; i < n; i++) {
+ if (tolower((unsigned char)tok[i]) != (unsigned char)name[i]) return false;
+ }
+ return true;
+}
+
+static int lookup_key_token(const char *tok, int len) {
+ if (len == 1) {
+ char c = tok[0];
+ if (c >= 'a' && c <= 'z') return SDLK_a + (c - 'a');
+ if (c >= 'A' && c <= 'Z') return SDLK_a + (c - 'A');
+ if (c >= '0' && c <= '9') return SDLK_0 + (c - '0');
+ switch (c) {
+ case '/': return SDLK_SLASH;
+ case '=': return SDLK_EQUALS;
+ case '+': return SDLK_PLUS;
+ case '-': return SDLK_MINUS;
+ case ',': return SDLK_COMMA;
+ case '.': return SDLK_PERIOD;
+ case ';': return SDLK_SEMICOLON;
+ case '\'': return SDLK_QUOTE;
+ case '\\': return SDLK_BACKSLASH;
+ case '[': return SDLK_LEFTBRACKET;
+ case ']': return SDLK_RIGHTBRACKET;
+ case '`': return SDLK_BACKQUOTE;
+ }
+ return SDLK_UNKNOWN;
+ }
+
+ static const struct { const char *name; int key; } NAMED[] = {
+ { "f1", SDLK_F1 }, { "f2", SDLK_F2 }, { "f3", SDLK_F3 }, { "f4", SDLK_F4 },
+ { "f5", SDLK_F5 }, { "f6", SDLK_F6 }, { "f7", SDLK_F7 }, { "f8", SDLK_F8 },
+ { "f9", SDLK_F9 }, { "f10", SDLK_F10 }, { "f11", SDLK_F11 }, { "f12", SDLK_F12 },
+ { "slash", SDLK_SLASH }, { "equal", SDLK_EQUALS }, { "plus", SDLK_PLUS }, { "minus", SDLK_MINUS },
+ { "comma", SDLK_COMMA }, { "period", SDLK_PERIOD }, { "semicolon", SDLK_SEMICOLON },
+ { "apostrophe", SDLK_QUOTE }, { "backslash", SDLK_BACKSLASH },
+ { "leftbracket", SDLK_LEFTBRACKET }, { "rightbracket", SDLK_RIGHTBRACKET },
+ { "grave", SDLK_BACKQUOTE }, { "space", SDLK_SPACE }, { "enter", SDLK_RETURN },
+ { "escape", SDLK_ESCAPE }, { "tab", SDLK_TAB }, { "backspace", SDLK_BACKSPACE },
+ { "delete", SDLK_DELETE }, { "home", SDLK_HOME }, { "end", SDLK_END },
+ { "pageup", SDLK_PAGEUP }, { "pagedown", SDLK_PAGEDOWN },
+ { "up", SDLK_UP }, { "down", SDLK_DOWN }, { "left", SDLK_LEFT }, { "right", SDLK_RIGHT },
+ { "insert", SDLK_INSERT },
+ };
+ for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++) {
+ if (token_is(tok, len, NAMED[i].name)) return NAMED[i].key;
+ }
+ return SDLK_UNKNOWN;
+}
+
+bool keymap_parse_spec(const char *spec, int len, KeyBinding *out) {
+ int trimmedLen;
+ const char *s = trim(spec, len, &trimmedLen);
+ if (trimmedLen == 0) return false;
+
+ bool ctrl = false, shift = false, alt = false;
+ int key = SDLK_UNKNOWN;
+
+ const char *p = s;
+ int remaining = trimmedLen;
+ while (remaining > 0) {
+ const char *plus = memchr(p, '+', (size_t)remaining);
+ /* A '+' that's the very last character left can't be a separator (nothing follows it
+ to separate) -- treat it as the literal '+' key instead, so e.g. "ctrl++" parses as
+ ctrl + the '+' key, not ctrl + an empty trailing token. */
+ if (plus && (plus - p) == remaining - 1) plus = NULL;
+ int tokLen = plus ? (int)(plus - p) : remaining;
+ int tlen;
+ const char *tok = trim(p, tokLen, &tlen);
+ if (tlen == 0) return false;
+
+ if (token_is(tok, tlen, "ctrl") || token_is(tok, tlen, "control")) ctrl = true;
+ else if (token_is(tok, tlen, "shift")) shift = true;
+ else if (token_is(tok, tlen, "alt")) alt = true;
+ else {
+ int k = lookup_key_token(tok, tlen);
+ if (k == SDLK_UNKNOWN) return false;
+ key = k;
+ }
+
+ if (!plus) break;
+ remaining -= (int)(plus - p) + 1;
+ p = plus + 1;
+ }
+
+ if (key == SDLK_UNKNOWN) return false;
+ out->key = key;
+ out->ctrl = ctrl;
+ out->shift = shift;
+ out->alt = alt;
+ return true;
+}
+
+static const char *ACTION_CONFIG_NAMES[ACTION_COUNT] = {
+ [ACTION_SAVE] = "key.save",
+ [ACTION_OPEN] = "key.open",
+ [ACTION_COPY] = "key.copy",
+ [ACTION_PASTE] = "key.paste",
+ [ACTION_UNDO] = "key.undo",
+ [ACTION_REDO] = "key.redo",
+ [ACTION_QUIT] = "key.quit",
+ [ACTION_HELP] = "key.help",
+ [ACTION_SOURCE_VIEW] = "key.source_view",
+ [ACTION_ZOOM_IN] = "key.zoom_in",
+ [ACTION_ZOOM_OUT] = "key.zoom_out",
+ [ACTION_ZOOM_RESET] = "key.zoom_reset",
+};
+
+bool keymap_parse_config_line(const char *key, int keyLen, const char *value, int valueLen, KeyBinding *keymap) {
+ for (int i = 0; i < ACTION_COUNT; i++) {
+ size_t nameLen = strlen(ACTION_CONFIG_NAMES[i]);
+ if ((size_t)keyLen == nameLen && memcmp(key, ACTION_CONFIG_NAMES[i], nameLen) == 0) {
+ KeyBinding b;
+ if (!keymap_parse_spec(value, valueLen, &b)) return false;
+ keymap[i] = b;
+ return true;
+ }
+ }
+ return false;
+}
+
+#define KEYMAP_MAX_KEYDOWNS_PER_FRAME 16
+static SDL_Keycode s_keydowns[KEYMAP_MAX_KEYDOWNS_PER_FRAME];
+static int s_keydownCount;
+
+void keymap_begin_frame(void) {
+ s_keydownCount = 0;
+}
+
+void keymap_note_keydown(SDL_Keycode key) {
+ if (s_keydownCount < KEYMAP_MAX_KEYDOWNS_PER_FRAME) s_keydowns[s_keydownCount++] = key;
+}
+
+bool keymap_key_pressed_raw(SDL_Keycode key) {
+ for (int i = 0; i < s_keydownCount; i++) {
+ if (s_keydowns[i] == key) return true;
+ }
+ return false;
+}
+
+/* Unlike raylib's physical-scancode KEY_*, SDL_Keycode for a symbol/punctuation key already
+ reflects whatever the CURRENT layout needs to produce it -- e.g. "=" is Shift+0 on a Swedish
+ keyboard, so a KEYDOWN event carrying SDLK_EQUALS arrives with KMOD_SHIFT set. Requiring an
+ *additional* exact shift==false match on top of that (as stored in a "ctrl+=" binding, where
+ `shift` just means "the word 'shift' wasn't in the spec") would make such a binding permanently
+ unsatisfiable on layouts where shift is needed to type the base character at all. Letters and
+ digits don't have this problem -- SDLK_a/SDLK_0 etc. stay constant regardless of shift, so an
+ explicit shift modifier there (e.g. "ctrl+shift+z" vs "ctrl+z") is a real, independent, and
+ layout-independent distinction worth enforcing exactly. */
+static bool key_is_alnum(int key) {
+ return (key >= SDLK_a && key <= SDLK_z) || (key >= SDLK_0 && key <= SDLK_9);
+}
+
+static bool modifiers_match(const KeyBinding *b, bool ctrl, bool shift, bool alt) {
+ if (b->ctrl != ctrl || b->alt != alt) return false;
+ if (key_is_alnum(b->key)) return b->shift == shift;
+ return true;
+}
+
+bool keymap_pressed(Action a, bool ctrl, bool shift, bool alt) {
+ KeyBinding *b = &g_keymap[a];
+ if (b->key == SDLK_UNKNOWN) return false;
+ if (!modifiers_match(b, ctrl, shift, alt)) return false;
+ return keymap_key_pressed_raw(b->key);
+}
+
+bool keymap_repeat_scancode(SDL_Scancode sc, double now, double *nextRepeatAt) {
+ if (!SDL_GetKeyboardState(NULL)[sc]) { *nextRepeatAt = 0.0; return false; }
+ if (now >= *nextRepeatAt) { *nextRepeatAt = now + (*nextRepeatAt == 0.0 ? 0.4 : 0.03); return true; }
+ return false;
+}
+
+bool keymap_repeat(Action a, bool ctrl, bool shift, bool alt, double now, double *nextRepeatAt) {
+ KeyBinding *b = &g_keymap[a];
+ /* Deliberately exact-match shift here (unlike keymap_pressed above), even for non-alnum
+ keys: this resolves the binding's keycode back to a physical scancode via
+ SDL_GetScancodeFromKey, which is a static/unshifted reverse mapping -- on a layout where
+ two different keycodes share one physical key (e.g. Swedish's "0" key types "0" plain,
+ "=" with shift), SDLK_EQUALS and SDLK_0 would both resolve to the same scancode, and
+ relaxing shift here would make holding plain Ctrl+0 ambiguously also satisfy a "ctrl+="
+ binding. The numpad fallbacks main.c wires in independently cover zoom reliably regardless
+ of whether this primary binding's repeat path is reachable on a given layout. */
+ if (b->key == SDLK_UNKNOWN || b->ctrl != ctrl || b->shift != shift || b->alt != alt) {
+ *nextRepeatAt = 0.0;
+ return false;
+ }
+ return keymap_repeat_scancode(SDL_GetScancodeFromKey(b->key), now, nextRepeatAt);
+}
diff --git a/src/keymap.h b/src/keymap.h
new file mode 100644
index 0000000..68e3dca
--- /dev/null
+++ b/src/keymap.h
@@ -0,0 +1,76 @@
+#ifndef HUSH_KEYMAP_H
+#define HUSH_KEYMAP_H
+
+#include <SDL.h>
+#include <stdbool.h>
+
+/* Every user-rebindable shortcut. Plain text-editing keys (arrows, Enter, Backspace, Home/End,
+ the mouse) are deliberately not in here -- they're not "shortcuts" in the chord sense, and
+ letting them be rebound risks silently breaking normal typing. */
+typedef enum {
+ ACTION_SAVE,
+ ACTION_OPEN,
+ ACTION_COPY,
+ ACTION_PASTE,
+ ACTION_UNDO,
+ ACTION_REDO,
+ ACTION_QUIT,
+ ACTION_HELP,
+ ACTION_SOURCE_VIEW,
+ ACTION_ZOOM_IN,
+ ACTION_ZOOM_OUT,
+ ACTION_ZOOM_RESET,
+ ACTION_COUNT
+} Action;
+
+typedef struct {
+ int key; /* an SDL_Keycode (SDLK_*) -- layout-aware, not a physical scancode */
+ bool ctrl, shift, alt;
+} KeyBinding;
+
+/* Fills out[ACTION_COUNT] with hush's built-in default bindings. */
+void keymap_set_defaults(KeyBinding *out);
+
+/* Parses a binding spec like "ctrl+s", "ctrl+shift+z", "ctrl+/", or "f1" into `out`. Modifier
+ names ("ctrl", "shift", "alt") are case-insensitive and may appear in any order, joined by
+ '+'; the remaining token is the key name (single letters/digits, f1-f12, punctuation like
+ "/" or "slash", or names like "enter"/"escape"/"tab"/"space"/"home"/"end"/"up"/"down"/
+ "left"/"right"/"pageup"/"pagedown"/"insert" -- see keymap.c for the full list). Returns false
+ (leaving `out` untouched) for an empty spec or an unrecognized key name. */
+bool keymap_parse_spec(const char *spec, int len, KeyBinding *out);
+
+/* If `key` names one of the actions above (its config-file key, e.g. "key.save"), parses
+ `value` as a binding spec and stores it into keymap[that action]. Returns whether it matched
+ and parsed -- used by config.c to dispatch "key.*" lines. */
+bool keymap_parse_config_line(const char *key, int keyLen, const char *value, int valueLen, KeyBinding *keymap);
+
+/* The bindings actually used by main.c's input loop: defaulted at startup, then overlaid by
+ config_load() from the user's config file. */
+extern KeyBinding g_keymap[ACTION_COUNT];
+
+/* Clears the per-frame keydown buffer -- call once at the top of each frame, before draining
+ SDL's event queue. */
+void keymap_begin_frame(void);
+/* Records one non-repeat SDL_KEYDOWN event's keycode into the per-frame buffer -- call once per
+ such event while draining SDL_PollEvent (skip events where event.key.repeat is set, since
+ that's OS-level auto-repeat, not a fresh press). */
+void keymap_note_keydown(SDL_Keycode key);
+/* True if `key` was recorded via keymap_note_keydown this frame. Used both by keymap_pressed
+ (below) and by main.c's own ad-hoc fixed-key checks (F1, Escape, Enter, Home/End, ...) so
+ there's a single source of truth for "was this keycode just pressed" rather than two parallel
+ per-frame buffers. */
+bool keymap_key_pressed_raw(SDL_Keycode key);
+
+/* True on the frame `a`'s bound chord was just pressed (ctrl/shift/alt must match exactly). */
+bool keymap_pressed(Action a, bool ctrl, bool shift, bool alt);
+/* Same, but true again at a steady repeat rate while held. */
+bool keymap_repeat(Action a, bool ctrl, bool shift, bool alt, double now, double *nextRepeatAt);
+
+/* Shared scancode-based "is held, repeating at a steady rate" primitive: true on the frame `sc`
+ goes down, then again every ~0.03s after an initial ~0.4s delay while still held, false (and
+ resets `*nextRepeatAt`) once released. Backs keymap_repeat above; also exposed directly for
+ main.c's fixed-scancode repeat needs (arrows, backspace, delete, the numpad zoom fallback) so
+ there's one repeat-timer implementation, not two. */
+bool keymap_repeat_scancode(SDL_Scancode sc, double now, double *nextRepeatAt);
+
+#endif
diff --git a/src/lang.c b/src/lang.c
index 3268914..8da84de 100644
--- a/src/lang.c
+++ b/src/lang.c
@@ -14,11 +14,18 @@ static const char *KEY_NAMES[STR_COUNT] = {
[STR_QUIT_SAVE] = "quit.save",
[STR_QUIT_DISCARD] = "quit.discard",
[STR_QUIT_CANCEL] = "quit.cancel",
+ [STR_QUIT_TITLE_OPEN] = "quit.title_open",
+ [STR_QUIT_SAVE_OPEN] = "quit.save_open",
+ [STR_QUIT_DISCARD_OPEN] = "quit.discard_open",
+ [STR_OPEN_TITLE] = "open.title",
+ [STR_OPEN_CONFIRM] = "open.confirm",
+ [STR_OPEN_ERROR] = "open.error",
[STR_HELP_TITLE] = "help.title",
[STR_HELP_ARROWS] = "help.arrows",
[STR_HELP_SHIFT_ARROWS] = "help.shift_arrows",
[STR_HELP_MOUSE_DRAG] = "help.mouse_drag",
[STR_HELP_SAVE] = "help.save",
+ [STR_HELP_OPEN] = "help.open",
[STR_HELP_COPY_PASTE] = "help.copy_paste",
[STR_HELP_UNDO_REDO] = "help.undo_redo",
[STR_HELP_QUIT] = "help.quit",
@@ -43,11 +50,18 @@ static const char *DEFAULTS[STR_COUNT] = {
[STR_QUIT_SAVE] = "Save and quit",
[STR_QUIT_DISCARD] = "Quit without saving",
[STR_QUIT_CANCEL] = "Cancel",
+ [STR_QUIT_TITLE_OPEN] = "Save changes before opening another file?",
+ [STR_QUIT_SAVE_OPEN] = "Save and open",
+ [STR_QUIT_DISCARD_OPEN] = "Open without saving",
+ [STR_OPEN_TITLE] = "Open file",
+ [STR_OPEN_CONFIRM] = "Open",
+ [STR_OPEN_ERROR] = "Could not open that file.",
[STR_HELP_TITLE] = "Keyboard shortcuts",
[STR_HELP_ARROWS] = "Arrow keys \xE2\x80\x94 move the cursor",
[STR_HELP_SHIFT_ARROWS] = "Shift + arrow keys \xE2\x80\x94 select text",
[STR_HELP_MOUSE_DRAG] = "Click + drag \xE2\x80\x94 select text",
[STR_HELP_SAVE] = "Ctrl+S \xE2\x80\x94 Save",
+ [STR_HELP_OPEN] = "Ctrl+O \xE2\x80\x94 Open a file",
[STR_HELP_COPY_PASTE] = "Ctrl+C / Ctrl+V \xE2\x80\x94 Copy / paste",
[STR_HELP_UNDO_REDO] = "Ctrl+Z / Ctrl+Shift+Z \xE2\x80\x94 Undo / redo",
[STR_HELP_QUIT] = "Ctrl+X \xE2\x80\x94 Quit",
@@ -56,7 +70,7 @@ static const char *DEFAULTS[STR_COUNT] = {
[STR_HELP_BACKSPACE_DELETE] = "Backspace / Delete \xE2\x80\x94 Delete a character",
[STR_HELP_HOME_END] = "Home / End \xE2\x80\x94 Start/end of line",
[STR_HELP_SOURCE_VIEW] = "Ctrl+M \xE2\x80\x94 View raw markdown source",
- [STR_HELP_ZOOM] = "Ctrl+= / Ctrl+- \xE2\x80\x94 Zoom in / out (Ctrl+0 resets)",
+ [STR_HELP_ZOOM] = "Ctrl++ / Ctrl+- \xE2\x80\x94 Zoom in / out (Ctrl+0 resets)",
[STR_HELP_CLOSE] = "Close",
[STR_SOURCE_HINT] = "Raw markdown source \xE2\x80\x94 read-only \xC2\xB7 Ctrl+M or Esc to go back",
};
diff --git a/src/lang.h b/src/lang.h
index 550c30a..b751ff6 100644
--- a/src/lang.h
+++ b/src/lang.h
@@ -15,11 +15,18 @@ typedef enum {
STR_QUIT_SAVE,
STR_QUIT_DISCARD,
STR_QUIT_CANCEL,
+ STR_QUIT_TITLE_OPEN,
+ STR_QUIT_SAVE_OPEN,
+ STR_QUIT_DISCARD_OPEN,
+ STR_OPEN_TITLE,
+ STR_OPEN_CONFIRM,
+ STR_OPEN_ERROR,
STR_HELP_TITLE,
STR_HELP_ARROWS,
STR_HELP_SHIFT_ARROWS,
STR_HELP_MOUSE_DRAG,
STR_HELP_SAVE,
+ STR_HELP_OPEN,
STR_HELP_COPY_PASTE,
STR_HELP_UNDO_REDO,
STR_HELP_QUIT,
diff --git a/src/main.c b/src/main.c
index 524dcca..61e4f51 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,4 +1,5 @@
-#include "raylib.h"
+#include <SDL.h>
+#include <SDL_ttf.h>
#include "clay.h"
#include "editor.h"
#include "render.h"
@@ -7,8 +8,10 @@
#include "theme.h"
#include "app_mode.h"
#include "config.h"
+#include "keymap.h"
#include "lang.h"
#include "paths.h"
+#include "utf8.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -17,39 +20,132 @@ static void handle_clay_errors(Clay_ErrorData errorData) {
fprintf(stderr, "Clay error: %s\n", errorData.errorText.chars);
}
-/* True on the frame a key was just pressed, and then again at a steady repeat rate while held. */
-static bool key_repeat(int key, double now, double *nextRepeatAt) {
- if (IsKeyPressed(key)) { *nextRepeatAt = now + 0.4; return true; }
- if (IsKeyDown(key) && now >= *nextRepeatAt) { *nextRepeatAt = now + 0.03; return true; }
- if (!IsKeyDown(key)) *nextRepeatAt = 0.0;
- return false;
+static double now_seconds(void) {
+ return (double)SDL_GetPerformanceCounter() / (double)SDL_GetPerformanceFrequency();
}
-static void update_window_title(EditorState *ed) {
+static void update_window_title(SDL_Window *window, EditorState *ed) {
char title[512];
snprintf(title, sizeof title, "%s%s \xe2\x80\x94 hush", ed->dirty ? "*" : "", editor_display_name(ed));
- SetWindowTitle(title);
+ SDL_SetWindowTitle(window, title);
+}
+
+static char *dup_str_or_null(const char *s) {
+ if (!s) return NULL;
+ size_t n = strlen(s);
+ char *copy = malloc(n + 1);
+ memcpy(copy, s, n + 1);
+ return copy;
+}
+
+/* Tries to load OpenPromptState's typed path. On success, switches back to MODE_EDITING and
+ updates the window title (the dirty-flag-change hook that normally does this won't fire here,
+ since editor_load always resets dirty to false regardless of whether it came in already
+ false). On failure, restores the previous filePath (editor_load overwrites it unconditionally,
+ even on a failed read) so a stray Ctrl+S afterward can't silently save over the wrong name,
+ and leaves `prompt` and `mode` alone so the user can see the error and correct the path. */
+static void try_open_from_prompt(SDL_Window *window, EditorState *ed, OpenPromptState *prompt, AppMode *mode) {
+ if (prompt->len == 0) return;
+ char *oldPath = dup_str_or_null(ed->filePath);
+ if (editor_load(ed, prompt->path)) {
+ free(oldPath);
+ prompt->showError = false;
+ *mode = MODE_EDITING;
+ update_window_title(window, ed);
+ } else {
+ free(ed->filePath);
+ ed->filePath = oldPath;
+ prompt->showError = true;
+ }
+}
+
+/* This frame's SDL events, drained once at the top of the loop. Continuous "is held" state
+ (arrow/backspace repeat, modifier keys, mouse position) is read live via
+ SDL_GetKeyboardState/SDL_GetModState/SDL_GetMouseState instead of being buffered here --
+ only discrete, easy-to-miss-if-polled-later events need collecting. */
+typedef struct {
+ bool quit;
+ char textInput[256];
+ int textInputLen;
+ bool mouseLeftPressed, mouseLeftReleased;
+ float wheelX, wheelY;
+} FrameInput;
+
+static void poll_frame_input(FrameInput *fi) {
+ memset(fi, 0, sizeof *fi);
+ keymap_begin_frame();
+ SDL_Event e;
+ while (SDL_PollEvent(&e)) {
+ switch (e.type) {
+ case SDL_QUIT:
+ fi->quit = true;
+ break;
+ case SDL_KEYDOWN:
+ /* SDL's own OS-level auto-repeat would otherwise fire SDL_KEYDOWN repeatedly for
+ a held chord (e.g. Ctrl+S), which keymap_pressed's "just pressed" semantics
+ must not see -- unlike raylib's IsKeyPressed, SDL doesn't filter this for us. */
+ if (!e.key.repeat) keymap_note_keydown(e.key.keysym.sym);
+ break;
+ case SDL_TEXTINPUT: {
+ size_t n = strlen(e.text.text);
+ if (fi->textInputLen + (int)n < (int)sizeof fi->textInput) {
+ memcpy(fi->textInput + fi->textInputLen, e.text.text, n);
+ fi->textInputLen += (int)n;
+ }
+ break;
+ }
+ case SDL_MOUSEBUTTONDOWN:
+ if (e.button.button == SDL_BUTTON_LEFT) fi->mouseLeftPressed = true;
+ break;
+ case SDL_MOUSEBUTTONUP:
+ if (e.button.button == SDL_BUTTON_LEFT) fi->mouseLeftReleased = true;
+ break;
+ case SDL_MOUSEWHEEL: {
+ float mult = (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) ? -1.0f : 1.0f;
+ fi->wheelX += mult * (float)e.wheel.x;
+ fi->wheelY += mult * (float)e.wheel.y;
+ break;
+ }
+ default: break;
+ }
+ }
}
int main(int argc, char **argv) {
paths_init();
Config cfg;
- config_load(&cfg); /* pure file I/O, no raylib dependency -- safe to run before window init */
+ config_load(&cfg); /* pure file I/O -- safe to run before window init */
lang_load(cfg.language);
theme_init_defaults();
g_theme.bg = cfg.bgColor;
g_theme.text = cfg.textColor;
g_theme.cursor = cfg.textColor; /* caret always tracks the configured text color */
g_theme.zoom = cfg.zoom;
+ memcpy(g_keymap, cfg.keymap, sizeof g_keymap);
+
+ SDL_Init(SDL_INIT_VIDEO);
+ TTF_Init();
+ /* SDL_WINDOW_ALLOW_HIGHDPI deliberately omitted: raylib's equivalent flag caused a broken
+ viewport on this project's actual mixed-DPI X11 setup earlier and was removed -- don't
+ re-risk the same bug speculatively. */
+ SDL_Window *window = SDL_CreateWindow("hush", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+ 1000, 800, SDL_WINDOW_RESIZABLE);
+ SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
+ if (!renderer) renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
+ /* SDL_Renderer defaults to SDL_BLENDMODE_NONE (alpha ignored, drawn fully opaque) -- without
+ this, every semi-transparent Clay_Color (the modal scrim, selection highlight, ==highlight==
+ spans) would render fully opaque instead of blending with what's underneath. */
+ SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
+ SDL_StartTextInput();
- Clay_Raylib_Initialize(1000, 800, "hush", FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT);
- SetExitKey(KEY_NULL);
+ int winWi, winHi;
+ SDL_GetWindowSize(window, &winWi, &winHi);
uint64_t memSize = Clay_MinMemorySize();
Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(memSize, malloc(memSize));
- Clay_Initialize(arena, (Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() }, (Clay_ErrorHandler){ handle_clay_errors, 0 });
+ Clay_Initialize(arena, (Clay_Dimensions){ (float)winWi, (float)winHi }, (Clay_ErrorHandler){ handle_clay_errors, 0 });
- Font fonts[FONT_COUNT];
+ SDL2_Font fonts[FONT_COUNT];
fonts_load(fonts, cfg.fontPaths);
config_free(&cfg);
Clay_SetMeasureTextFunction(hush_measure_text, fonts);
@@ -59,20 +155,27 @@ int main(int argc, char **argv) {
if (argc >= 2) {
editor_load(&ed, argv[1]);
}
- update_window_title(&ed);
+ update_window_title(window, &ed);
LayoutCache cache;
layout_cache_init(&cache);
- double blinkResetTime = GetTime();
+ double now0 = now_seconds();
+ double blinkResetTime = now0;
+ double lastFrameNow = now0;
int lastBlock = ed.cursorBlock, lastOffset = ed.cursorOffset;
bool lastDirty = ed.dirty;
double repLeft = 0, repRight = 0, repUp = 0, repDown = 0, repBackspace = 0, repDelete = 0;
double repZoomIn = 0, repZoomOut = 0, repZoomInKp = 0, repZoomOutKp = 0;
+ double repOpenBackspace = 0;
bool windowClosing = false;
AppMode mode = MODE_EDITING;
+ OpenPromptState openPrompt = { .path = "", .len = 0, .showError = false };
+ /* True if the MODE_QUIT_CONFIRM currently showing is guarding Ctrl+O (unsaved changes
+ before opening another file) rather than Ctrl+X/window-close (quitting outright). */
+ bool confirmForOpen = false;
/* True only for a mouse-down session whose *press* frame was itself handled in
MODE_EDITING -- prevents the tail end of a click that dismissed a modal (still
physically held down for a frame or two afterward) from being replayed as a
@@ -80,87 +183,139 @@ int main(int argc, char **argv) {
bool mouseDownInEditor = false;
while (!windowClosing) {
- double now = GetTime();
- bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
- bool shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
+ double now = now_seconds();
+ float deltaTime = (float)(now - lastFrameNow);
+ lastFrameNow = now;
+
+ FrameInput fi;
+ poll_frame_input(&fi);
+
+ SDL_Keymod mods = SDL_GetModState();
+ bool ctrl = (mods & KMOD_CTRL) != 0;
+ bool shift = (mods & KMOD_SHIFT) != 0;
+ bool alt = (mods & KMOD_ALT) != 0;
+
+ int mx, my;
+ Uint32 mouseButtons = SDL_GetMouseState(&mx, &my);
+ bool mouseLeftDown = (mouseButtons & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
+ Clay_Vector2 mousePos = { (float)mx, (float)my };
if (mode == MODE_EDITING) {
- if (ctrl && IsKeyPressed(KEY_S)) {
+ if (keymap_pressed(ACTION_SAVE, ctrl, shift, alt)) {
editor_save(&ed);
- } else if (ctrl && IsKeyPressed(KEY_V)) {
- const char *clip = GetClipboardText();
- if (clip) editor_paste(&ed, clip, (int)strlen(clip));
- } else if (ctrl && IsKeyPressed(KEY_C)) {
+ } else if (keymap_pressed(ACTION_OPEN, ctrl, shift, alt)) {
+ if (ed.dirty) {
+ confirmForOpen = true;
+ mode = MODE_QUIT_CONFIRM;
+ } else {
+ openPrompt.len = 0;
+ openPrompt.path[0] = '\0';
+ openPrompt.showError = false;
+ mode = MODE_OPEN_PROMPT;
+ }
+ } else if (keymap_pressed(ACTION_PASTE, ctrl, shift, alt)) {
+ char *clip = SDL_GetClipboardText();
+ if (clip) { editor_paste(&ed, clip, (int)strlen(clip)); SDL_free(clip); }
+ } else if (keymap_pressed(ACTION_COPY, ctrl, shift, alt)) {
int len;
char *text = editor_selection_to_text(&ed, &len);
- if (text) { SetClipboardText(text); free(text); }
- } else if (ctrl && shift && IsKeyPressed(KEY_Z)) {
+ if (text) { SDL_SetClipboardText(text); free(text); }
+ } else if (keymap_pressed(ACTION_REDO, ctrl, shift, alt)) {
editor_redo(&ed);
- } else if (ctrl && IsKeyPressed(KEY_Z)) {
+ } else if (keymap_pressed(ACTION_UNDO, ctrl, shift, alt)) {
editor_undo(&ed);
- } else if (ctrl && IsKeyPressed(KEY_X)) {
+ } else if (keymap_pressed(ACTION_QUIT, ctrl, shift, alt)) {
+ confirmForOpen = false;
mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
if (!ed.dirty) windowClosing = true;
- } else if ((ctrl && IsKeyPressed(KEY_SLASH)) || IsKeyPressed(KEY_F1)) {
- /* Ctrl+/ is bound by physical scancode (matches the common editor convention,
- e.g. VS Code/Sublime's comment-toggle), which lands on a different printed
- character on non-US keyboard layouts (e.g. it's the "-/_" key on a Swedish
- layout, not "?"). F1 is a layout-independent fallback that's always reachable. */
+ } else if (keymap_pressed(ACTION_HELP, ctrl, shift, alt) || keymap_key_pressed_raw(SDLK_F1)) {
+ /* ACTION_HELP defaults to Ctrl+/. SDL_Keycode fixes the raylib-era layout problem
+ for keys whose base/unshifted level matches (letters, digits, and symbols that
+ sit unshifted on the current layout) -- but SDL2's `.sym` only ever reports a
+ key's *unshifted* level, even when shift is actually held, and SDL_TEXTINPUT
+ (which does resolve the shifted character) is suppressed entirely while Ctrl is
+ down. So on a layout where producing "/" itself requires Shift (e.g. Swedish,
+ Shift+7), there is no public SDL2 keycode this can bind to -- a real SDL2 API
+ gap (reportedly addressed by SDL3's modifier-aware key queries), not something
+ fixable here without adding a raw XKB dependency. F1 stays as a fixed,
+ always-reachable fallback for exactly this reason. */
mode = MODE_SHORTCUTS_HELP;
- } else if (ctrl && IsKeyPressed(KEY_M)) {
+ } else if (keymap_pressed(ACTION_SOURCE_VIEW, ctrl, shift, alt)) {
mode = MODE_SOURCE_VIEW;
- } else if (ctrl && (key_repeat(KEY_EQUAL, now, &repZoomIn) || key_repeat(KEY_KP_ADD, now, &repZoomInKp))) {
- /* KEY_EQUAL is bound by physical scancode, same caveat as KEY_SLASH above (e.g. it's
- a dead accent key on a Swedish layout, not "="). KEY_KP_ADD (numpad +) is a
- layout-independent fallback for keyboards with a numpad. */
+ } else if (keymap_repeat(ACTION_ZOOM_IN, ctrl, shift, alt, now, &repZoomIn) ||
+ (ctrl && keymap_repeat_scancode(SDL_SCANCODE_KP_PLUS, now, &repZoomInKp))) {
+ /* ACTION_ZOOM_IN defaults to Ctrl++ rather than Ctrl+= specifically because "+"
+ sits unshifted on this layout (Swedish: "0"/"+"/"?" share a key, "+" at the
+ base level, unlike "=" which needs Shift+0) -- so it doesn't hit the SDL2
+ keycode gap described for ACTION_HELP above. KP_PLUS (numpad +) is a fixed
+ fallback for keyboards with a numpad, regardless of what ACTION_ZOOM_IN is
+ rebound to. */
theme_zoom_in();
- } else if (ctrl && (key_repeat(KEY_MINUS, now, &repZoomOut) || key_repeat(KEY_KP_SUBTRACT, now, &repZoomOutKp))) {
+ } else if (keymap_repeat(ACTION_ZOOM_OUT, ctrl, shift, alt, now, &repZoomOut) ||
+ (ctrl && keymap_repeat_scancode(SDL_SCANCODE_KP_MINUS, now, &repZoomOutKp))) {
theme_zoom_out();
- } else if (ctrl && (IsKeyPressed(KEY_ZERO) || IsKeyPressed(KEY_KP_0))) {
+ } else if (keymap_pressed(ACTION_ZOOM_RESET, ctrl, shift, alt) || (ctrl && keymap_key_pressed_raw(SDLK_KP_0))) {
theme_zoom_reset();
} else {
- if (!ctrl) {
- int cp;
- while ((cp = GetCharPressed()) != 0) {
- int utf8Size = 0;
- const char *bytes = CodepointToUTF8(cp, &utf8Size);
- editor_insert_utf8(&ed, bytes, utf8Size);
- }
+ if (!ctrl && fi.textInputLen > 0) {
+ editor_insert_utf8(&ed, fi.textInput, fi.textInputLen);
}
- if (IsKeyPressed(KEY_ENTER) || IsKeyPressed(KEY_KP_ENTER)) editor_enter(&ed);
- if (key_repeat(KEY_BACKSPACE, now, &repBackspace)) editor_backspace(&ed);
- if (key_repeat(KEY_DELETE, now, &repDelete)) editor_delete_forward(&ed);
- if (key_repeat(KEY_LEFT, now, &repLeft)) editor_move_left(&ed, shift);
- if (key_repeat(KEY_RIGHT, now, &repRight)) editor_move_right(&ed, shift);
- if (key_repeat(KEY_UP, now, &repUp)) editor_move_vertical(&ed, &cache, fonts, -1, shift);
- if (key_repeat(KEY_DOWN, now, &repDown)) editor_move_vertical(&ed, &cache, fonts, 1, shift);
- if (IsKeyPressed(KEY_HOME)) editor_move_home(&ed, shift);
- if (IsKeyPressed(KEY_END)) editor_move_end(&ed, shift);
+ if (keymap_key_pressed_raw(SDLK_RETURN) || keymap_key_pressed_raw(SDLK_KP_ENTER)) editor_enter(&ed);
+ if (keymap_repeat_scancode(SDL_SCANCODE_BACKSPACE, now, &repBackspace)) editor_backspace(&ed);
+ if (keymap_repeat_scancode(SDL_SCANCODE_DELETE, now, &repDelete)) editor_delete_forward(&ed);
+ if (keymap_repeat_scancode(SDL_SCANCODE_LEFT, now, &repLeft)) editor_move_left(&ed, shift);
+ if (keymap_repeat_scancode(SDL_SCANCODE_RIGHT, now, &repRight)) editor_move_right(&ed, shift);
+ if (keymap_repeat_scancode(SDL_SCANCODE_UP, now, &repUp)) editor_move_vertical(&ed, &cache, fonts, -1, shift);
+ if (keymap_repeat_scancode(SDL_SCANCODE_DOWN, now, &repDown)) editor_move_vertical(&ed, &cache, fonts, 1, shift);
+ if (keymap_key_pressed_raw(SDLK_HOME)) editor_move_home(&ed, shift);
+ if (keymap_key_pressed_raw(SDLK_END)) editor_move_end(&ed, shift);
}
- if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
- editor_click(&ed, &cache, fonts, GetMousePosition());
+ if (fi.mouseLeftPressed) {
+ editor_click(&ed, &cache, fonts, mousePos);
mouseDownInEditor = true;
- } else if (mouseDownInEditor && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
- editor_drag_to(&ed, &cache, fonts, GetMousePosition());
+ } else if (mouseDownInEditor && mouseLeftDown) {
+ editor_drag_to(&ed, &cache, fonts, mousePos);
}
- /* raylib's WindowShouldClose() self-resets every frame (PollInputEvents re-derives
- it from the GLFW window-close flag and immediately clears that flag), so this is
- a clean one-shot edge per OS close-button click -- no extra latch needed. */
- if (WindowShouldClose()) {
+ /* SDL_QUIT is naturally exactly-once per actual close-button click (SDL_PollEvent
+ returns each event once) -- no self-reset workaround needed here, unlike raylib's
+ WindowShouldClose(). */
+ if (fi.quit) {
+ confirmForOpen = false;
mode = ed.dirty ? MODE_QUIT_CONFIRM : MODE_EDITING;
if (!ed.dirty) windowClosing = true;
}
} else if (mode == MODE_QUIT_CONFIRM) {
- if (IsKeyPressed(KEY_ESCAPE)) mode = MODE_EDITING;
+ if (keymap_key_pressed_raw(SDLK_ESCAPE)) mode = MODE_EDITING;
} else if (mode == MODE_SHORTCUTS_HELP) {
- if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_F1) || (ctrl && IsKeyPressed(KEY_SLASH))) mode = MODE_EDITING;
+ if (keymap_key_pressed_raw(SDLK_ESCAPE) || keymap_key_pressed_raw(SDLK_F1) || keymap_pressed(ACTION_HELP, ctrl, shift, alt)) mode = MODE_EDITING;
} else if (mode == MODE_SOURCE_VIEW) {
- if (IsKeyPressed(KEY_ESCAPE) || (ctrl && IsKeyPressed(KEY_M))) mode = MODE_EDITING;
+ if (keymap_key_pressed_raw(SDLK_ESCAPE) || keymap_pressed(ACTION_SOURCE_VIEW, ctrl, shift, alt)) mode = MODE_EDITING;
+ } else if (mode == MODE_OPEN_PROMPT) {
+ if (keymap_key_pressed_raw(SDLK_ESCAPE)) {
+ mode = MODE_EDITING;
+ } else if (keymap_key_pressed_raw(SDLK_RETURN) || keymap_key_pressed_raw(SDLK_KP_ENTER)) {
+ try_open_from_prompt(window, &ed, &openPrompt, &mode);
+ } else if (keymap_repeat_scancode(SDL_SCANCODE_BACKSPACE, now, &repOpenBackspace)) {
+ if (openPrompt.len > 0) {
+ openPrompt.len = utf8_prev_start(openPrompt.path, openPrompt.len);
+ openPrompt.path[openPrompt.len] = '\0';
+ openPrompt.showError = false;
+ }
+ } else if (fi.textInputLen > 0) {
+ int n = fi.textInputLen;
+ if (openPrompt.len + n >= (int)sizeof(openPrompt.path) - 1) n = (int)sizeof(openPrompt.path) - 1 - openPrompt.len;
+ if (n > 0) {
+ memcpy(openPrompt.path + openPrompt.len, fi.textInput, (size_t)n);
+ openPrompt.len += n;
+ openPrompt.path[openPrompt.len] = '\0';
+ openPrompt.showError = false;
+ }
+ }
}
- if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) mouseDownInEditor = false;
+ if (fi.mouseLeftReleased) mouseDownInEditor = false;
if (ed.cursorBlock != lastBlock || ed.cursorOffset != lastOffset) {
blinkResetTime = now;
@@ -169,28 +324,37 @@ int main(int argc, char **argv) {
}
if (ed.dirty != lastDirty) {
lastDirty = ed.dirty;
- update_window_title(&ed);
+ update_window_title(window, &ed);
}
- Clay_Vector2 mousePos = { GetMousePosition().x, GetMousePosition().y };
- Clay_SetPointerState(mousePos, IsMouseButtonDown(0));
- Clay_SetLayoutDimensions((Clay_Dimensions){ (float)GetScreenWidth(), (float)GetScreenHeight() });
- Vector2 wheel = GetMouseWheelMoveV();
- Clay_UpdateScrollContainers(false, (Clay_Vector2){ wheel.x, wheel.y * 40.0f }, GetFrameTime());
+ int winW, winH;
+ SDL_GetWindowSize(window, &winW, &winH);
+ Clay_SetPointerState(mousePos, mouseLeftDown);
+ Clay_SetLayoutDimensions((Clay_Dimensions){ (float)winW, (float)winH });
+ Clay_UpdateScrollContainers(false, (Clay_Vector2){ fi.wheelX, fi.wheelY * 40.0f }, deltaTime);
float caretBlinkT = (float)(now - blinkResetTime);
- BeginDrawing();
- ClearBackground(BLACK);
+ SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+ SDL_RenderClear(renderer);
int toggledTaskBlock;
- ModalAction action = render_frame(&ed, &cache, fonts, caretBlinkT, mode, &toggledTaskBlock);
- EndDrawing();
+ ModalAction action = render_frame(&ed, &cache, fonts, renderer, fi.mouseLeftPressed, (float)winW, (float)winH,
+ deltaTime, caretBlinkT, mode, &toggledTaskBlock, &openPrompt, confirmForOpen);
+ SDL_RenderPresent(renderer);
switch (action) {
- case MODAL_ACTION_SAVE_QUIT: editor_save(&ed); windowClosing = true; break;
- case MODAL_ACTION_DISCARD_QUIT: windowClosing = true; break;
- case MODAL_ACTION_CANCEL: mode = MODE_EDITING; break;
- case MODAL_ACTION_CLOSE_HELP: mode = MODE_EDITING; break;
+ case MODAL_ACTION_SAVE_QUIT:
+ editor_save(&ed);
+ if (confirmForOpen) { openPrompt.len = 0; openPrompt.path[0] = '\0'; openPrompt.showError = false; mode = MODE_OPEN_PROMPT; }
+ else windowClosing = true;
+ break;
+ case MODAL_ACTION_DISCARD_QUIT:
+ if (confirmForOpen) { openPrompt.len = 0; openPrompt.path[0] = '\0'; openPrompt.showError = false; mode = MODE_OPEN_PROMPT; }
+ else windowClosing = true;
+ break;
+ case MODAL_ACTION_CANCEL: mode = MODE_EDITING; break;
+ case MODAL_ACTION_CLOSE_HELP: mode = MODE_EDITING; break;
+ case MODAL_ACTION_OPEN_CONFIRM: try_open_from_prompt(window, &ed, &openPrompt, &mode); break;
default: break;
}
if (mode == MODE_EDITING && toggledTaskBlock >= 0) editor_toggle_task(&ed, toggledTaskBlock);
@@ -200,6 +364,10 @@ int main(int argc, char **argv) {
editor_free(&ed);
fonts_unload(fonts);
lang_free();
- Clay_Raylib_Close();
+ SDL_StopTextInput();
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroyWindow(window);
+ TTF_Quit();
+ SDL_Quit();
return 0;
}
diff --git a/src/render.c b/src/render.c
index d6fc12f..395dea6 100644
--- a/src/render.c
+++ b/src/render.c
@@ -21,12 +21,13 @@ typedef struct {
} RenderRun;
typedef struct {
- Font *fonts;
+ SDL2_Font *fonts;
LayoutCache *cache;
float contentWidth;
int cursorBlock;
int cursorOffset;
int *toggledTaskBlock; /* set to a block index this frame if its task checkbox was clicked */
+ bool mouseLeftPressed; /* this frame's left-click edge; Clay has no input polling under SDL2 */
} RenderCtx;
/* Segments that need a strikethrough line drawn through them once their final screen position
@@ -45,7 +46,7 @@ static void mark_strike(int segIdx, Clay_Color color) {
}
typedef struct {
- Font *fonts;
+ SDL2_Font *fonts;
RenderRun *runs;
bool boldBase;
int baseFontSize;
@@ -102,8 +103,11 @@ static float measure_cb(void *ctxV, int runIndex, const char *text, int start, i
if (n >= (int)sizeof(buf)) n = sizeof(buf) - 1;
memcpy(buf, text + start, (size_t)n);
buf[n] = '\0';
- Vector2 sz = MeasureTextEx(ctx->fonts[fontId], buf, (float)fontSize, 0.0f);
- return sz.x;
+ TTF_Font *font = ctx->fonts[fontId].font;
+ TTF_SetFontSize(font, fontSize);
+ int w = 0, h = 0;
+ TTF_SizeUTF8(font, buf, &w, &h);
+ return (float)w;
}
/* Expands parsed inline runs into RenderRuns: styled markup is shown as plain rendered text
@@ -357,7 +361,7 @@ static void emit_block(RenderCtx *ctx, Document *doc, int blockIndex) {
CLAY_TEXT(markerText, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = (uint16_t)baseFontSize,
.textColor = markerHovered ? COL_LINK : g_theme.text }));
}
- if (markerHovered && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && ctx->toggledTaskBlock) {
+ if (markerHovered && ctx->mouseLeftPressed && ctx->toggledTaskBlock) {
*ctx->toggledTaskBlock = blockIndex;
}
CLAY_AUTO_ID({
@@ -429,7 +433,7 @@ static void modal_text_line_bold(const char *text) {
}
/* A clickable modal button: highlights on hover, and sets *action to `thisAction` on click. */
-static void modal_button(Clay_ElementId id, const char *label, ModalAction *action, ModalAction thisAction) {
+static void modal_button(Clay_ElementId id, const char *label, ModalAction *action, ModalAction thisAction, bool mouseLeftPressed) {
bool hovered = Clay_PointerOver(id);
CLAY(id, {
.layout = {
@@ -444,16 +448,38 @@ static void modal_button(Clay_ElementId id, const char *label, ModalAction *acti
Clay_String s = { .isStaticallyAllocated = true, .length = (int)strlen(label), .chars = label };
CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_SANS_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text }));
}
- if (hovered && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) *action = thisAction;
+ if (hovered && mouseLeftPressed) *action = thisAction;
}
-/* Draws the quit-confirm or shortcuts-help modal (whichever `mode` calls for) as a floating,
- dimmed overlay on top of everything else, and returns the action the user's click resolved
- to this frame (MODAL_ACTION_NONE if none). No-op for any other mode (MODE_EDITING has
- nothing to overlay; MODE_SOURCE_VIEW is a full alternate view, not a modal). */
-static ModalAction emit_modal(AppMode mode, float winW, float winH) {
+/* The open-file text field: a bordered box showing the typed path with a trailing cursor mark. */
+static void open_path_field(const OpenPromptState *openPrompt) {
+ CLAY(CLAY_ID("OpenPathField"), {
+ .layout = {
+ .sizing = { CLAY_SIZING_GROW(0), CLAY_SIZING_FIXED(36) },
+ .padding = { 12, 12, 0, 0 },
+ .childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
+ },
+ .backgroundColor = g_theme.bg,
+ .cornerRadius = CLAY_CORNER_RADIUS(4),
+ .border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
+ }) {
+ char tmp[sizeof openPrompt->path + 8];
+ int n = snprintf(tmp, sizeof tmp, "%.*s|", openPrompt->len, openPrompt->path);
+ if (n > (int)sizeof tmp) n = (int)sizeof tmp;
+ char *scratchBuf = scratch_alloc(n);
+ memcpy(scratchBuf, tmp, (size_t)n);
+ Clay_String s = { .isStaticallyAllocated = false, .length = n, .chars = scratchBuf };
+ CLAY_TEXT(s, CLAY_TEXT_CONFIG({ .fontId = FONT_MONO_REGULAR, .fontSize = FS_MODAL, .textColor = g_theme.text, .wrapMode = CLAY_TEXT_WRAP_NONE }));
+ }
+}
+
+/* Draws the quit-confirm, shortcuts-help, or open-file modal (whichever `mode` calls for) as a
+ floating, dimmed overlay on top of everything else, and returns the action the user's click
+ resolved to this frame (MODAL_ACTION_NONE if none). No-op for any other mode (MODE_EDITING
+ has nothing to overlay; MODE_SOURCE_VIEW is a full alternate view, not a modal). */
+static ModalAction emit_modal(AppMode mode, float winW, float winH, const OpenPromptState *openPrompt, bool confirmForOpen, bool mouseLeftPressed) {
ModalAction action = MODAL_ACTION_NONE;
- if (mode != MODE_QUIT_CONFIRM && mode != MODE_SHORTCUTS_HELP) return action;
+ if (mode != MODE_QUIT_CONFIRM && mode != MODE_SHORTCUTS_HELP && mode != MODE_OPEN_PROMPT) return action;
CLAY(CLAY_ID("ModalScrim"), {
.layout = {
@@ -463,14 +489,14 @@ static ModalAction emit_modal(AppMode mode, float winW, float winH) {
.backgroundColor = COL_MODAL_SCRIM,
.floating = { .attachTo = CLAY_ATTACH_TO_ROOT, .zIndex = 10 },
}) {
- if (Clay_PointerOver(CLAY_ID("ModalScrim")) && !Clay_PointerOver(CLAY_ID("ModalBox")) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
- action = (mode == MODE_QUIT_CONFIRM) ? MODAL_ACTION_CANCEL : MODAL_ACTION_CLOSE_HELP;
+ if (Clay_PointerOver(CLAY_ID("ModalScrim")) && !Clay_PointerOver(CLAY_ID("ModalBox")) && mouseLeftPressed) {
+ action = (mode == MODE_SHORTCUTS_HELP) ? MODAL_ACTION_CLOSE_HELP : MODAL_ACTION_CANCEL;
}
CLAY(CLAY_ID("ModalBox"), {
.layout = {
.layoutDirection = CLAY_TOP_TO_BOTTOM,
- .sizing = { CLAY_SIZING_FIXED(mode == MODE_QUIT_CONFIRM ? 380 : 420), CLAY_SIZING_FIT(0) },
+ .sizing = { CLAY_SIZING_FIXED(mode == MODE_SHORTCUTS_HELP ? 420 : 380), CLAY_SIZING_FIT(0) },
.padding = CLAY_PADDING_ALL(24),
.childGap = 12,
},
@@ -479,16 +505,23 @@ static ModalAction emit_modal(AppMode mode, float winW, float winH) {
.border = { .color = COL_MODAL_BORDER, .width = CLAY_BORDER_OUTSIDE(1) },
}) {
if (mode == MODE_QUIT_CONFIRM) {
- modal_text_line_bold(lang_get(STR_QUIT_TITLE));
- modal_button(CLAY_ID("BtnSaveQuit"), lang_get(STR_QUIT_SAVE), &action, MODAL_ACTION_SAVE_QUIT);
- modal_button(CLAY_ID("BtnDiscardQuit"), lang_get(STR_QUIT_DISCARD), &action, MODAL_ACTION_DISCARD_QUIT);
- modal_button(CLAY_ID("BtnCancel"), lang_get(STR_QUIT_CANCEL), &action, MODAL_ACTION_CANCEL);
+ modal_text_line_bold(lang_get(confirmForOpen ? STR_QUIT_TITLE_OPEN : STR_QUIT_TITLE));
+ modal_button(CLAY_ID("BtnSaveQuit"), lang_get(confirmForOpen ? STR_QUIT_SAVE_OPEN : STR_QUIT_SAVE), &action, MODAL_ACTION_SAVE_QUIT, mouseLeftPressed);
+ modal_button(CLAY_ID("BtnDiscardQuit"), lang_get(confirmForOpen ? STR_QUIT_DISCARD_OPEN : STR_QUIT_DISCARD), &action, MODAL_ACTION_DISCARD_QUIT, mouseLeftPressed);
+ modal_button(CLAY_ID("BtnCancel"), lang_get(STR_QUIT_CANCEL), &action, MODAL_ACTION_CANCEL, mouseLeftPressed);
+ } else if (mode == MODE_OPEN_PROMPT) {
+ modal_text_line_bold(lang_get(STR_OPEN_TITLE));
+ open_path_field(openPrompt);
+ if (openPrompt->showError) modal_text_line(lang_get(STR_OPEN_ERROR));
+ modal_button(CLAY_ID("BtnOpenConfirm"), lang_get(STR_OPEN_CONFIRM), &action, MODAL_ACTION_OPEN_CONFIRM, mouseLeftPressed);
+ modal_button(CLAY_ID("BtnCancel"), lang_get(STR_QUIT_CANCEL), &action, MODAL_ACTION_CANCEL, mouseLeftPressed);
} else {
modal_text_line_bold(lang_get(STR_HELP_TITLE));
modal_text_line(lang_get(STR_HELP_ARROWS));
modal_text_line(lang_get(STR_HELP_SHIFT_ARROWS));
modal_text_line(lang_get(STR_HELP_MOUSE_DRAG));
modal_text_line(lang_get(STR_HELP_SAVE));
+ modal_text_line(lang_get(STR_HELP_OPEN));
modal_text_line(lang_get(STR_HELP_COPY_PASTE));
modal_text_line(lang_get(STR_HELP_UNDO_REDO));
modal_text_line(lang_get(STR_HELP_QUIT));
@@ -498,7 +531,7 @@ static ModalAction emit_modal(AppMode mode, float winW, float winH) {
modal_text_line(lang_get(STR_HELP_HOME_END));
modal_text_line(lang_get(STR_HELP_SOURCE_VIEW));
modal_text_line(lang_get(STR_HELP_ZOOM));
- modal_button(CLAY_ID("BtnCloseHelp"), lang_get(STR_HELP_CLOSE), &action, MODAL_ACTION_CLOSE_HELP);
+ modal_button(CLAY_ID("BtnCloseHelp"), lang_get(STR_HELP_CLOSE), &action, MODAL_ACTION_CLOSE_HELP, mouseLeftPressed);
}
}
}
@@ -506,9 +539,10 @@ static ModalAction emit_modal(AppMode mode, float winW, float winH) {
}
Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
- Font *fonts = (Font *)userData;
- Font font = fonts[config->fontId];
- if (font.glyphs == NULL) font = GetFontDefault();
+ SDL2_Font *fonts = (SDL2_Font *)userData;
+ TTF_Font *font = fonts[config->fontId].font;
+ if (!font) return (Clay_Dimensions){0, 0}; /* insurance only -- every slot is load-or-exit at startup */
+ TTF_SetFontSize(font, config->fontSize);
char buf[1024];
int n = text.length;
@@ -516,23 +550,24 @@ Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig
if (n > 0) memcpy(buf, text.chars, (size_t)n);
buf[n] = '\0';
- Vector2 sz = MeasureTextEx(font, buf, (float)config->fontSize, 0.0f);
- return (Clay_Dimensions){ sz.x, sz.y > 0 ? sz.y : (float)config->fontSize };
+ int w = 0, h = 0;
+ TTF_SizeUTF8(font, buf, &w, &h);
+ return (Clay_Dimensions){ (float)w, h > 0 ? (float)h : (float)config->fontSize };
}
-ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode, int *outToggledTaskBlock) {
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, SDL_Renderer *renderer,
+ bool mouseLeftPressed, float winW, float winH, float deltaTime, float caretBlinkT, AppMode mode,
+ int *outToggledTaskBlock, const OpenPromptState *openPrompt, bool confirmForOpen) {
g_scratchPos = 0;
g_strikeMarkCount = 0;
layout_cache_reset(cache);
- float winW = (float)GetScreenWidth();
- float winH = (float)GetScreenHeight();
float contentWidth = winW - 2.0f * CONTENT_SIDE_PADDING;
if (contentWidth > CONTENT_MAX_WIDTH) contentWidth = CONTENT_MAX_WIDTH;
if (contentWidth < 100.0f) contentWidth = 100.0f;
int toggledTaskBlock = -1;
- RenderCtx ctx = { fonts, cache, contentWidth, ed->cursorBlock, ed->cursorOffset, &toggledTaskBlock };
+ RenderCtx ctx = { fonts, cache, contentWidth, ed->cursorBlock, ed->cursorOffset, &toggledTaskBlock, mouseLeftPressed };
Clay_BeginLayout();
@@ -592,29 +627,25 @@ ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float
}
}
- ModalAction modalAction = emit_modal(mode, winW, winH);
+ ModalAction modalAction = emit_modal(mode, winW, winH, openPrompt, confirmForOpen, mouseLeftPressed);
- Clay_RenderCommandArray cmds = Clay_EndLayout(GetFrameTime());
- Clay_Raylib_Render(cmds, fonts);
+ Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);
+ Clay_SDL2_Render(renderer, cmds, fonts);
for (int i = 0; i < g_strikeMarkCount; i++) {
Clay_ElementData sd = Clay_GetElementData(CLAY_IDI("Seg", g_strikeMarks[i].segIdx));
if (!sd.found) continue;
- Color lineColor = {
- (unsigned char)roundf(g_strikeMarks[i].color.r), (unsigned char)roundf(g_strikeMarks[i].color.g),
- (unsigned char)roundf(g_strikeMarks[i].color.b), (unsigned char)roundf(g_strikeMarks[i].color.a),
- };
+ Clay_Color c = g_strikeMarks[i].color;
+ SDL_SetRenderDrawColor(renderer, (Uint8)c.r, (Uint8)c.g, (Uint8)c.b, (Uint8)c.a);
int midY = (int)(sd.boundingBox.y + sd.boundingBox.height * 0.5f);
- DrawRectangle((int)sd.boundingBox.x, midY, (int)sd.boundingBox.width, 1, lineColor);
+ SDL_Rect strikeRect = { (int)sd.boundingBox.x, midY, (int)sd.boundingBox.width, 1 };
+ SDL_RenderFillRect(renderer, &strikeRect);
}
if (mode == MODE_EDITING && editor_has_selection(ed)) {
int sb, so, eb, eo;
editor_selection_range(ed, &sb, &so, &eb, &eo);
- Color selColor = {
- (unsigned char)roundf(COL_SELECTION.r), (unsigned char)roundf(COL_SELECTION.g),
- (unsigned char)roundf(COL_SELECTION.b), (unsigned char)roundf(COL_SELECTION.a),
- };
+ SDL_SetRenderDrawColor(renderer, (Uint8)COL_SELECTION.r, (Uint8)COL_SELECTION.g, (Uint8)COL_SELECTION.b, (Uint8)COL_SELECTION.a);
for (int i = 0; i < cache->lineCount; i++) {
CachedLine *line = &cache->lines[i];
if (line->blockIndex < sb || line->blockIndex > eb) continue;
@@ -622,7 +653,8 @@ ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float
int rangeEnd = (line->blockIndex == eb) ? eo : ed->doc.blocks[line->blockIndex].text.len;
float x, y, w, h;
if (hittest_line_range_box(cache, &ed->doc, fonts, i, rangeStart, rangeEnd, &x, &y, &w, &h)) {
- DrawRectangle((int)x, (int)y, (int)w, (int)h, selColor);
+ SDL_Rect selRect = { (int)x, (int)y, (int)w, (int)h };
+ SDL_RenderFillRect(renderer, &selRect);
}
}
}
@@ -646,11 +678,9 @@ ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float
}
}
if (fmodf(caretBlinkT, 1.0f) < 0.5f) {
- Color cursorColor = {
- (unsigned char)roundf(g_theme.cursor.r), (unsigned char)roundf(g_theme.cursor.g),
- (unsigned char)roundf(g_theme.cursor.b), (unsigned char)roundf(g_theme.cursor.a),
- };
- DrawRectangle((int)cx, (int)cy, 2, (int)ch, cursorColor);
+ SDL_SetRenderDrawColor(renderer, (Uint8)g_theme.cursor.r, (Uint8)g_theme.cursor.g, (Uint8)g_theme.cursor.b, (Uint8)g_theme.cursor.a);
+ SDL_Rect caretRect = { (int)cx, (int)cy, 2, (int)ch };
+ SDL_RenderFillRect(renderer, &caretRect);
}
}
}
diff --git a/src/render.h b/src/render.h
index 8d6e131..fc3a082 100644
--- a/src/render.h
+++ b/src/render.h
@@ -4,28 +4,37 @@
#include "editor.h"
#include "layout_cache.h"
#include "app_mode.h"
-#include "raylib.h"
+#include "fonts.h"
#include "clay.h"
+#include <SDL.h>
-/* Declared (not defined) here because vendor/clay_renderer_raylib.c has no header of its own;
+/* Declared (not defined) here because vendor/clay_renderer_SDL2.c has no header of its own;
it is compiled once, into src/clay_impl.c. */
-void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags);
-void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font *fonts);
-void Clay_Raylib_Close(void);
+void Clay_SDL2_Render(SDL_Renderer *renderer, Clay_RenderCommandArray renderCommands, SDL2_Font *fonts);
-/* Registered with Clay_SetMeasureTextFunction (userData = the Font[FONT_COUNT] array).
- Uses raylib's own UTF-8 aware MeasureTextEx, unlike the byte-indexed helper that ships
- with Clay's raylib renderer, so accented characters (e.g. Swedish å ä ö) measure correctly. */
+/* Registered with Clay_SetMeasureTextFunction (userData = the SDL2_Font[FONT_COUNT] array).
+ UTF-8 aware (TTF_SizeUTF8), so accented characters (e.g. Swedish å ä ö) measure correctly. */
Clay_Dimensions hush_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
/* Builds the Clay tree for the current editor state, populates `cache` fresh (consumed by
hit-testing on the *next* frame's input), issues the Clay render commands, and draws the
- blinking caret on top. Call between BeginDrawing()/EndDrawing(), after
+ blinking caret on top. Call between SDL_RenderClear/SDL_RenderPresent, after
Clay_SetPointerState/Clay_SetLayoutDimensions/Clay_UpdateScrollContainers.
+ `renderer` is used both for Clay_SDL2_Render and for the raw post-Clay overlay draws (caret,
+ selection highlight, strikethrough). `mouseLeftPressed` is this frame's left-click edge (Clay
+ itself has no input polling under SDL2, so render.c's internal click checks -- task
+ checkboxes, modal buttons, click-outside-to-cancel -- need it passed in). `winW`/`winH` are
+ the current window size in pixels (main.c already computes these for Clay_SetLayoutDimensions
+ that same frame).
When `mode` is not MODE_EDITING, also draws the corresponding modal on top and returns
whichever action the user's click resolved to this frame (MODAL_ACTION_NONE otherwise).
`*outToggledTaskBlock` is set to the block index of a task-list checkbox clicked this frame,
- or -1 if none (ignored if NULL) -- caller should follow up with editor_toggle_task. */
-ModalAction render_frame(EditorState *ed, LayoutCache *cache, Font *fonts, float caretBlinkT, AppMode mode, int *outToggledTaskBlock);
+ or -1 if none (ignored if NULL) -- caller should follow up with editor_toggle_task.
+ `openPrompt` is read (never NULL) for MODE_OPEN_PROMPT's typed-path text field; ignored
+ otherwise. `confirmForOpen` disambiguates MODE_QUIT_CONFIRM's wording: true if it's guarding
+ Ctrl+O (unsaved changes before opening another file) rather than Ctrl+X (quitting). */
+ModalAction render_frame(EditorState *ed, LayoutCache *cache, SDL2_Font *fonts, SDL_Renderer *renderer,
+ bool mouseLeftPressed, float winW, float winH, float deltaTime, float caretBlinkT, AppMode mode,
+ int *outToggledTaskBlock, const OpenPromptState *openPrompt, bool confirmForOpen);
#endif
diff --git a/src/test_main.c b/src/test_main.c
index 8f62836..d84331b 100644
--- a/src/test_main.c
+++ b/src/test_main.c
@@ -8,6 +8,8 @@
#include "config.h"
#include "lang.h"
#include "theme.h"
+#include "keymap.h"
+#include <SDL.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -425,9 +427,56 @@ static void test_config_parse_line(void) {
CHECK(!config_parse_line(l9, (int)strlen(l9), &cfg), "a non-numeric zoom value returns false");
CHECK(cfg.zoom == zoomBefore, "an invalid zoom line leaves the previous value untouched");
+ const char *l10 = "key.open = ctrl+shift+o";
+ CHECK(config_parse_line(l10, (int)strlen(l10), &cfg), "a valid key.* line returns true");
+ CHECK(cfg.keymap[ACTION_OPEN].key == SDLK_o && cfg.keymap[ACTION_OPEN].ctrl && cfg.keymap[ACTION_OPEN].shift && !cfg.keymap[ACTION_OPEN].alt,
+ "key.open rebinds ACTION_OPEN's KeyBinding");
+
+ const char *l11 = "key.open = not+a+real+key";
+ CHECK(!config_parse_line(l11, (int)strlen(l11), &cfg), "a key.* line with an unrecognized key name returns false");
+ CHECK(cfg.keymap[ACTION_OPEN].key == SDLK_o && cfg.keymap[ACTION_OPEN].shift, "an invalid key.* line leaves the previous binding untouched");
+
+ const char *l12 = "key.unknown_action = ctrl+q";
+ CHECK(!config_parse_line(l12, (int)strlen(l12), &cfg), "a key.* line for an unknown action name returns false");
+
config_free(&cfg);
}
+static void test_keymap(void) {
+ KeyBinding defaults[ACTION_COUNT];
+ keymap_set_defaults(defaults);
+ CHECK(defaults[ACTION_SAVE].key == SDLK_s && defaults[ACTION_SAVE].ctrl, "default ACTION_SAVE is Ctrl+S");
+ CHECK(defaults[ACTION_OPEN].key == SDLK_o && defaults[ACTION_OPEN].ctrl, "default ACTION_OPEN is Ctrl+O");
+ CHECK(defaults[ACTION_REDO].key == SDLK_z && defaults[ACTION_REDO].ctrl && defaults[ACTION_REDO].shift,
+ "default ACTION_REDO is Ctrl+Shift+Z, distinct from ACTION_UNDO's Ctrl+Z");
+ CHECK(defaults[ACTION_ZOOM_IN].key == SDLK_PLUS && defaults[ACTION_ZOOM_IN].ctrl,
+ "default ACTION_ZOOM_IN is Ctrl++, not Ctrl+= -- \"+\" sits unshifted on more layouts (e.g. Swedish)");
+
+ KeyBinding b;
+ CHECK(keymap_parse_spec("ctrl+s", 6, &b) && b.key == SDLK_s && b.ctrl && !b.shift && !b.alt, "\"ctrl+s\" parses");
+ CHECK(keymap_parse_spec("Ctrl+Shift+Z", 12, &b) && b.key == SDLK_z && b.ctrl && b.shift, "modifier names are case-insensitive");
+ CHECK(keymap_parse_spec("ctrl+/", 6, &b) && b.key == SDLK_SLASH, "a single punctuation character (\"/\") is recognized");
+ CHECK(keymap_parse_spec("ctrl+slash", 10, &b) && b.key == SDLK_SLASH, "its named form (\"slash\") parses to the same key");
+ CHECK(keymap_parse_spec("ctrl++", 6, &b) && b.key == SDLK_PLUS && b.ctrl,
+ "\"ctrl++\" parses the trailing '+' as the literal plus key, not an empty token after the '+' separator");
+ CHECK(keymap_parse_spec("ctrl+plus", 9, &b) && b.key == SDLK_PLUS && b.ctrl, "its named form (\"plus\") parses to the same key");
+ CHECK(keymap_parse_spec("f1", 2, &b) && b.key == SDLK_F1 && !b.ctrl, "a bare key with no modifiers parses");
+ CHECK(keymap_parse_spec("alt+shift+9", 11, &b) && b.key == SDLK_9 && b.alt && b.shift, "digits and \"alt\" parse");
+ CHECK(keymap_parse_spec(" ctrl + s ", 10, &b) && b.key == SDLK_s && b.ctrl, "stray whitespace around tokens is trimmed");
+
+ CHECK(!keymap_parse_spec("", 0, &b), "an empty spec is rejected");
+ CHECK(!keymap_parse_spec("ctrl+", 5, &b), "a spec with no key token is rejected");
+ CHECK(!keymap_parse_spec("ctrl+nonsense", 13, &b), "an unrecognized key name is rejected");
+ CHECK(!keymap_parse_spec("ctrl", 4, &b), "a spec that's only a modifier is rejected");
+
+ KeyBinding km[ACTION_COUNT];
+ keymap_set_defaults(km);
+ CHECK(keymap_parse_config_line("key.zoom_in", 11, "alt+equal", 9, km), "keymap_parse_config_line matches \"key.zoom_in\"");
+ CHECK(km[ACTION_ZOOM_IN].key == SDLK_EQUALS && km[ACTION_ZOOM_IN].alt && !km[ACTION_ZOOM_IN].ctrl, "and rebinds only that action");
+ CHECK(km[ACTION_ZOOM_OUT].key == SDLK_MINUS && km[ACTION_ZOOM_OUT].ctrl, "leaving every other action's default untouched");
+ CHECK(!keymap_parse_config_line("not_a_key_line", 14, "ctrl+s", 6, km), "a non-\"key.*\" name doesn't match");
+}
+
static void test_lang_parse_line(void) {
CHECK(strcmp(lang_get(STR_HELP_CLOSE), "Close") == 0, "before loading anything, lang_get returns the built-in English default");
@@ -685,6 +734,7 @@ int main(void) {
test_config_parse_line();
test_lang_parse_line();
test_theme_zoom();
+ test_keymap();
test_heading_levels_4_5_6();
test_heading_levels_round_trip();
test_horizontal_rule();
diff --git a/vendor/clay_renderer_SDL2.c b/vendor/clay_renderer_SDL2.c
new file mode 100644
index 0000000..a6b0fa7
--- /dev/null
+++ b/vendor/clay_renderer_SDL2.c
@@ -0,0 +1,431 @@
+/* Vendored from renderers/SDL2/clay_renderer_SDL2.c in github.com/nicbarker/clay (MIT), with two
+ deliberate patches from upstream -- keep these if re-vendoring a newer version:
+ 1. Clay_SDL2_Render's `static` qualifier is stripped so render.c (a separate translation unit)
+ can call it, matching how Clay_Raylib_Render was callable before this migration.
+ 2. The local SDL2_Font typedef below is removed; the canonical definition now lives in
+ src/fonts.h (included by src/clay_impl.c before this file), so there's exactly one
+ definition instead of two hand-synced copies. */
+/* clay.h is already included by src/clay_impl.c before this file, same as the raylib renderer. */
+#include <SDL.h>
+#include <SDL_ttf.h>
+#include <SDL_image.h>
+#include <stdio.h>
+#include <math.h>
+
+#ifndef M_PI
+ #define M_PI 3.14159
+#endif
+
+#define CLAY_COLOR_TO_SDL_COLOR_ARGS(color) color.r, color.g, color.b, color.a
+
+static Clay_Dimensions SDL2_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData)
+{
+ SDL2_Font *fonts = (SDL2_Font*)userData;
+
+ TTF_Font *font = fonts[config->fontId].font;
+ TTF_SetFontSize(font, config->fontSize);
+ char *chars = (char *)calloc(text.length + 1, 1);
+ memcpy(chars, text.chars, text.length);
+ int width = 0;
+ int height = 0;
+ if (TTF_SizeUTF8(font, chars, &width, &height) < 0) {
+ fprintf(stderr, "Error: could not measure text: %s\n", TTF_GetError());
+ exit(1);
+ }
+ free(chars);
+ return (Clay_Dimensions) {
+ .width = (float)width,
+ .height = (float)height,
+ };
+}
+
+/* Global for convenience. Even in 4K this is enough for smooth curves (low radius or rect size coupled with
+ * no AA or low resolution might make it appear as jagged curves) */
+static int NUM_CIRCLE_SEGMENTS = 16;
+
+//all rendering is performed by a single SDL call, avoiding multiple RenderRect + plumbing choice for circles.
+static void SDL_RenderFillRoundedRect(SDL_Renderer* renderer, const SDL_FRect rect, const float cornerRadius, const Clay_Color _color) {
+ const SDL_Color color = (SDL_Color) {
+ .r = (Uint8)_color.r,
+ .g = (Uint8)_color.g,
+ .b = (Uint8)_color.b,
+ .a = (Uint8)_color.a,
+ };
+
+ int indexCount = 0, vertexCount = 0;
+
+ const float maxRadius = SDL_min(rect.w, rect.h) / 2.0f;
+ const float clampedRadius = SDL_min(cornerRadius, maxRadius);
+
+ const int numCircleSegments = SDL_max(NUM_CIRCLE_SEGMENTS, (int)clampedRadius * 0.5f);
+
+ SDL_Vertex vertices[512];
+ int indices[512];
+
+ //define center rectangle
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + clampedRadius}, color, {0, 0} }; //0 center TL
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + clampedRadius}, color, {1, 0} }; //1 center TR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //2 center BR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //3 center BL
+
+ indices[indexCount++] = 0;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 3;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 2;
+ indices[indexCount++] = 3;
+
+ //define rounded corners as triangle fans
+ const float step = (M_PI / 2) / numCircleSegments;
+ for (int i = 0; i < numCircleSegments; i++) {
+ const float angle1 = (float)i * step;
+ const float angle2 = ((float)i + 1.0f) * step;
+
+ for (int j = 0; j < 4; j++) { // Iterate over four corners
+ float cx, cy, signX, signY;
+
+ switch (j) {
+ case 0: cx = rect.x + clampedRadius; cy = rect.y + clampedRadius; signX = -1; signY = -1; break; // Top-left
+ case 1: cx = rect.x + rect.w - clampedRadius; cy = rect.y + clampedRadius; signX = 1; signY = -1; break; // Top-right
+ case 2: cx = rect.x + rect.w - clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = 1; signY = 1; break; // Bottom-right
+ case 3: cx = rect.x + clampedRadius; cy = rect.y + rect.h - clampedRadius; signX = -1; signY = 1; break; // Bottom-left
+ default: return;
+ }
+
+ vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle1) * clampedRadius * signX, cy + SDL_sinf(angle1) * clampedRadius * signY}, color, {0, 0} };
+ vertices[vertexCount++] = (SDL_Vertex){ {cx + SDL_cosf(angle2) * clampedRadius * signX, cy + SDL_sinf(angle2) * clampedRadius * signY}, color, {0, 0} };
+
+ indices[indexCount++] = j; // Connect to corresponding central rectangle vertex
+ indices[indexCount++] = vertexCount - 2;
+ indices[indexCount++] = vertexCount - 1;
+ }
+ }
+
+ //Define edge rectangles
+ // Top edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y}, color, {0, 0} }; //TL
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y}, color, {1, 0} }; //TR
+
+ indices[indexCount++] = 0;
+ indices[indexCount++] = vertexCount - 2; //TL
+ indices[indexCount++] = vertexCount - 1; //TR
+ indices[indexCount++] = 1;
+ indices[indexCount++] = 0;
+ indices[indexCount++] = vertexCount - 1; //TR
+ // Right edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + clampedRadius}, color, {1, 0} }; //RT
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w, rect.y + rect.h - clampedRadius}, color, {1, 1} }; //RB
+
+ indices[indexCount++] = 1;
+ indices[indexCount++] = vertexCount - 2; //RT
+ indices[indexCount++] = vertexCount - 1; //RB
+ indices[indexCount++] = 2;
+ indices[indexCount++] = 1;
+ indices[indexCount++] = vertexCount - 1; //RB
+ // Bottom edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + rect.w - clampedRadius, rect.y + rect.h}, color, {1, 1} }; //BR
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x + clampedRadius, rect.y + rect.h}, color, {0, 1} }; //BL
+
+ indices[indexCount++] = 2;
+ indices[indexCount++] = vertexCount - 2; //BR
+ indices[indexCount++] = vertexCount - 1; //BL
+ indices[indexCount++] = 3;
+ indices[indexCount++] = 2;
+ indices[indexCount++] = vertexCount - 1; //BL
+ // Left edge
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + rect.h - clampedRadius}, color, {0, 1} }; //LB
+ vertices[vertexCount++] = (SDL_Vertex){ {rect.x, rect.y + clampedRadius}, color, {0, 0} }; //LT
+
+ indices[indexCount++] = 3;
+ indices[indexCount++] = vertexCount - 2; //LB
+ indices[indexCount++] = vertexCount - 1; //LT
+ indices[indexCount++] = 0;
+ indices[indexCount++] = 3;
+ indices[indexCount++] = vertexCount - 1; //LT
+
+ // Render everything
+ SDL_RenderGeometry(renderer, NULL, vertices, vertexCount, indices, indexCount);
+}
+
+//all rendering is performed by a single SDL call, using twi sets of arcing triangles, inner and outer, that fit together; along with two tringles to fill the end gaps.
+static void SDL_RenderCornerBorder(SDL_Renderer *renderer, Clay_BoundingBox* boundingBox, Clay_BorderRenderData* config, int cornerIndex, Clay_Color _color){
+ /////////////////////////////////
+ //The arc is constructed of outer triangles and inner triangles (if needed).
+ //First three vertices are first outer triangle's vertices
+ //Each two vertices after that are the inner-middle and second-outer vertex of
+ //each outer triangle after the first, because there first-outer vertex is equal to the
+ //second-outer vertex of the previous triangle. Indices set accordingly.
+ //The final two vertices are the missing vertices for the first and last inner triangles (if needed)
+ //Everything is in clockwise order (CW).
+ /////////////////////////////////
+ const SDL_Color color = (SDL_Color) {
+ .r = (Uint8)_color.r,
+ .g = (Uint8)_color.g,
+ .b = (Uint8)_color.b,
+ .a = (Uint8)_color.a,
+ };
+
+ float centerX, centerY, outerRadius, clampedRadius, startAngle, borderWidth;
+ const float maxRadius = SDL_min(boundingBox->width, boundingBox->height) / 2.0f;
+
+ SDL_Vertex vertices[512];
+ int indices[512];
+ int indexCount = 0, vertexCount = 0;
+
+ switch (cornerIndex) {
+ case(0):
+ startAngle = M_PI;
+ outerRadius = SDL_min(config->cornerRadius.topLeft, maxRadius);
+ centerX = boundingBox->x + outerRadius;
+ centerY = boundingBox->y + outerRadius;
+ borderWidth = config->width.top;
+ break;
+ case(1):
+ startAngle = 3*M_PI/2;
+ outerRadius = SDL_min(config->cornerRadius.topRight, maxRadius);
+ centerX = boundingBox->x + boundingBox->width - outerRadius;
+ centerY = boundingBox->y + outerRadius;
+ borderWidth = config->width.top;
+ break;
+ case(2):
+ startAngle = 0;
+ outerRadius = SDL_min(config->cornerRadius.bottomRight, maxRadius);
+ centerX = boundingBox->x + boundingBox->width - outerRadius;
+ centerY = boundingBox->y + boundingBox->height - outerRadius;
+ borderWidth = config->width.bottom;
+ break;
+ case(3):
+ startAngle = M_PI/2;
+ outerRadius = SDL_min(config->cornerRadius.bottomLeft, maxRadius);
+ centerX = boundingBox->x + outerRadius;
+ centerY = boundingBox->y + boundingBox->height - outerRadius;
+ borderWidth = config->width.bottom;
+ break;
+ default: break;
+ }
+
+ const float innerRadius = outerRadius - borderWidth;
+ const int minNumOuterTriangles = NUM_CIRCLE_SEGMENTS;
+ const int numOuterTriangles = SDL_max(minNumOuterTriangles, ceilf(outerRadius * 0.5f));
+ const float angleStep = M_PI / (2.0*(float)numOuterTriangles);
+
+ //outer triangles, in CW order
+ for (int i = 0; i < numOuterTriangles; i++) {
+ float angle1 = startAngle + i*angleStep; //first-outer vertex angle
+ float angle2 = startAngle + ((float)i + 0.5) * angleStep; //inner-middle vertex angle
+ float angle3 = startAngle + (i+1)*angleStep; // second-outer vertex angle
+
+ if( i == 0){ //first outer triangle
+ vertices[vertexCount++] = (SDL_Vertex){ {centerX + SDL_cosf(angle1) * outerRadius, centerY + SDL_sinf(angle1) * outerRadius}, color, {0, 0} }; //vertex index = 0
+ }
+ indices[indexCount++] = vertexCount - 1; //will be second-outer vertex of last outer triangle if not first outer triangle.
+
+ vertices[vertexCount++] = (innerRadius > 0)?
+ (SDL_Vertex){ {centerX + SDL_cosf(angle2) * (innerRadius), centerY + SDL_sinf(angle2) * (innerRadius)}, color, {0, 0}}:
+ (SDL_Vertex){ {centerX, centerY }, color, {0, 0}};
+ indices[indexCount++] = vertexCount - 1;
+
+ vertices[vertexCount++] = (SDL_Vertex){ {centerX + SDL_cosf(angle3) * outerRadius, centerY + SDL_sinf(angle3) * outerRadius}, color, {0, 0} };
+ indices[indexCount++] = vertexCount - 1;
+ }
+
+ if(innerRadius > 0){
+ // inner triangles in CW order (except the first and last)
+ for (int i = 0; i < numOuterTriangles - 1; i++){ //skip the last outer triangle
+ if(i==0){ //first outer triangle -> second inner triangle
+ indices[indexCount++] = 1; //inner-middle vertex of first outer triangle
+ indices[indexCount++] = 2; //second-outer vertex of first outer triangle
+ indices[indexCount++] = 3; //innder-middle vertex of second-outer triangle
+ }else{
+ int baseIndex = 3; //skip first outer triangle
+ indices[indexCount++] = baseIndex + (i-1)*2; // inner-middle vertex of current outer triangle
+ indices[indexCount++] = baseIndex + (i-1)*2 + 1; // second-outer vertex of current outer triangle
+ indices[indexCount++] = baseIndex + (i-1)*2 + 2; // inner-middle vertex of next outer triangle
+ }
+ }
+
+ float endAngle = startAngle + M_PI/2.0;
+
+ //last inner triangle
+ indices[indexCount++] = vertexCount - 2; //inner-middle vertex of last outer triangle
+ indices[indexCount++] = vertexCount - 1; //second-outer vertex of last outer triangle
+ vertices[vertexCount++] = (SDL_Vertex){ {centerX + SDL_cosf(endAngle) * innerRadius, centerY + SDL_sinf(endAngle) * innerRadius}, color, {0, 0} }; //missing vertex
+ indices[indexCount++] = vertexCount - 1;
+
+ // //first inner triangle
+ indices[indexCount++] = 0; //first-outer vertex of first outer triangle
+ indices[indexCount++] = 1; //inner-middle vertex of first outer triangle
+ vertices[vertexCount++] = (SDL_Vertex){ {centerX + SDL_cosf(startAngle) * innerRadius, centerY + SDL_sinf(startAngle) * innerRadius}, color, {0, 0} }; //missing vertex
+ indices[indexCount++] = vertexCount - 1;
+ }
+
+ SDL_RenderGeometry(renderer, NULL, vertices, vertexCount, indices, indexCount);
+}
+
+SDL_Rect currentClippingRectangle;
+
+void Clay_SDL2_Render(SDL_Renderer *renderer, Clay_RenderCommandArray renderCommands, SDL2_Font *fonts)
+{
+ for (uint32_t i = 0; i < renderCommands.length; i++)
+ {
+ Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, i);
+ Clay_BoundingBox boundingBox = renderCommand->boundingBox;
+ switch (renderCommand->commandType)
+ {
+ case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
+ Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle;
+ Clay_Color color = config->backgroundColor;
+ SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
+ SDL_FRect rect = (SDL_FRect) {
+ .x = boundingBox.x,
+ .y = boundingBox.y,
+ .w = boundingBox.width,
+ .h = boundingBox.height,
+ };
+ if (config->cornerRadius.topLeft > 0) {
+ SDL_RenderFillRoundedRect(renderer, rect, config->cornerRadius.topLeft, color);
+ }
+ else {
+ SDL_RenderFillRectF(renderer, &rect);
+ }
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_TEXT: {
+ Clay_TextRenderData *config = &renderCommand->renderData.text;
+ char *cloned = (char *)calloc(config->stringContents.length + 1, 1);
+ memcpy(cloned, config->stringContents.chars, config->stringContents.length);
+ TTF_Font* font = fonts[config->fontId].font;
+ TTF_SetFontSize(font, config->fontSize);
+ SDL_Surface *surface = TTF_RenderUTF8_Blended(font, cloned, (SDL_Color) {
+ .r = (Uint8)config->textColor.r,
+ .g = (Uint8)config->textColor.g,
+ .b = (Uint8)config->textColor.b,
+ .a = (Uint8)config->textColor.a,
+ });
+ SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
+
+ SDL_Rect destination = (SDL_Rect){
+ .x = boundingBox.x,
+ .y = boundingBox.y,
+ .w = boundingBox.width,
+ .h = boundingBox.height,
+ };
+ SDL_RenderCopy(renderer, texture, NULL, &destination);
+
+ SDL_DestroyTexture(texture);
+ SDL_FreeSurface(surface);
+ free(cloned);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
+ currentClippingRectangle = (SDL_Rect) {
+ .x = boundingBox.x,
+ .y = boundingBox.y,
+ .w = boundingBox.width,
+ .h = boundingBox.height,
+ };
+ SDL_RenderSetClipRect(renderer, ¤tClippingRectangle);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
+ SDL_RenderSetClipRect(renderer, NULL);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
+ Clay_ImageRenderData *config = &renderCommand->renderData.image;
+
+ SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, config->imageData);
+
+ SDL_Rect destination = (SDL_Rect){
+ .x = boundingBox.x,
+ .y = boundingBox.y,
+ .w = boundingBox.width,
+ .h = boundingBox.height,
+ };
+
+ SDL_RenderCopy(renderer, texture, NULL, &destination);
+
+ SDL_DestroyTexture(texture);
+ break;
+ }
+ case CLAY_RENDER_COMMAND_TYPE_BORDER: {
+ Clay_BorderRenderData *config = &renderCommand->renderData.border;
+ SDL_SetRenderDrawColor(renderer, CLAY_COLOR_TO_SDL_COLOR_ARGS(config->color));
+
+ if(boundingBox.width > 0 & boundingBox.height > 0){
+ const float maxRadius = SDL_min(boundingBox.width, boundingBox.height) / 2.0f;
+
+ if (config->width.left > 0) {
+ const float clampedRadiusTop = SDL_min((float)config->cornerRadius.topLeft, maxRadius);
+ const float clampedRadiusBottom = SDL_min((float)config->cornerRadius.bottomLeft, maxRadius);
+ SDL_FRect rect = {
+ boundingBox.x,
+ boundingBox.y + clampedRadiusTop,
+ (float)config->width.left,
+ (float)boundingBox.height - clampedRadiusTop - clampedRadiusBottom
+ };
+ SDL_RenderFillRectF(renderer, &rect);
+ }
+
+ if (config->width.right > 0) {
+ const float clampedRadiusTop = SDL_min((float)config->cornerRadius.topRight, maxRadius);
+ const float clampedRadiusBottom = SDL_min((float)config->cornerRadius.bottomRight, maxRadius);
+ SDL_FRect rect = {
+ boundingBox.x + boundingBox.width - config->width.right,
+ boundingBox.y + clampedRadiusTop,
+ (float)config->width.right,
+ (float)boundingBox.height - clampedRadiusTop - clampedRadiusBottom
+ };
+ SDL_RenderFillRectF(renderer, &rect);
+ }
+
+ if (config->width.top > 0) {
+ const float clampedRadiusLeft = SDL_min((float)config->cornerRadius.topLeft, maxRadius);
+ const float clampedRadiusRight = SDL_min((float)config->cornerRadius.topRight, maxRadius);
+ SDL_FRect rect = {
+ boundingBox.x + clampedRadiusLeft,
+ boundingBox.y,
+ boundingBox.width - clampedRadiusLeft - clampedRadiusRight,
+ (float)config->width.top };
+ SDL_RenderFillRectF(renderer, &rect);
+ }
+
+ if (config->width.bottom > 0) {
+ const float clampedRadiusLeft = SDL_min((float)config->cornerRadius.bottomLeft, maxRadius);
+ const float clampedRadiusRight = SDL_min((float)config->cornerRadius.bottomRight, maxRadius);
+ SDL_FRect rect = {
+ boundingBox.x + clampedRadiusLeft,
+ boundingBox.y + boundingBox.height - config->width.bottom,
+ boundingBox.width - clampedRadiusLeft - clampedRadiusRight,
+ (float)config->width.bottom
+ };
+ SDL_RenderFillRectF(renderer, &rect);
+ }
+
+ //corner index: 0->3 topLeft -> CW -> bottonLeft
+ if (config->width.top > 0 & config->cornerRadius.topLeft > 0) {
+ SDL_RenderCornerBorder(renderer, &boundingBox, config, 0, config->color);
+ }
+
+ if (config->width.top > 0 & config->cornerRadius.topRight> 0) {
+ SDL_RenderCornerBorder(renderer, &boundingBox, config, 1, config->color);
+ }
+
+ if (config->width.bottom > 0 & config->cornerRadius.bottomRight > 0) {
+ SDL_RenderCornerBorder(renderer, &boundingBox, config, 2, config->color);
+ }
+
+ if (config->width.bottom > 0 & config->cornerRadius.bottomLeft > 0) {
+ SDL_RenderCornerBorder(renderer, &boundingBox, config, 3, config->color);
+ }
+ }
+
+ break;
+ }
+ default: {
+ fprintf(stderr, "Error: unhandled render command: %d\n", renderCommand->commandType);
+ exit(1);
+ }
+ }
+ }
+}
diff --git a/vendor/clay_renderer_raylib.c b/vendor/clay_renderer_raylib.c
deleted file mode 100644
index 7fbc2c7..0000000
--- a/vendor/clay_renderer_raylib.c
+++ /dev/null
@@ -1,321 +0,0 @@
-#include "raylib.h"
-#include "raymath.h"
-#include "stdint.h"
-#include "string.h"
-#include "stdio.h"
-#include "stdlib.h"
-
-#define CLAY_RECTANGLE_TO_RAYLIB_RECTANGLE(rectangle) (Rectangle) { .x = rectangle.x, .y = rectangle.y, .width = rectangle.width, .height = rectangle.height }
-#define CLAY_COLOR_TO_RAYLIB_COLOR(color) (Color) { .r = (unsigned char)roundf(color.r), .g = (unsigned char)roundf(color.g), .b = (unsigned char)roundf(color.b), .a = (unsigned char)roundf(color.a) }
-
-Camera Raylib_camera;
-
-typedef enum
-{
- CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL
-} CustomLayoutElementType;
-
-typedef struct
-{
- Model model;
- float scale;
- Vector3 position;
- Matrix rotation;
-} CustomLayoutElement_3DModel;
-
-typedef struct
-{
- CustomLayoutElementType type;
- union {
- CustomLayoutElement_3DModel model;
- } customData;
-} CustomLayoutElement;
-
-const char* overlayShaderCode = "#version 330\n"
- "\n"
- "in vec2 fragTexCoord;\n"
- "in vec4 fragColor;\n"
- "\n"
- "uniform sampler2D texture0;\n"
- "uniform vec4 overlayColor;\n"
- "\n"
- "out vec4 finalColor;\n"
- "\n"
- "void main()\n"
- "{\n"
- " vec4 texelColor = texture(texture0, fragTexCoord) * fragColor;\n"
- "\n"
- " vec3 blendedRGB = mix(texelColor.rgb, overlayColor.rgb, overlayColor.a);\n"
- "\n"
- " finalColor = vec4(blendedRGB, texelColor.a);\n"
- "}";
-
-Shader overlayShader;
-int colorLoc;
-bool overlayEnabled = false;
-
-void InitOverlay() {
- overlayShader = LoadShaderFromMemory(0, overlayShaderCode);
- colorLoc = GetShaderLocation(overlayShader, "overlayColor");
-}
-
-void SetColorOverlay(Color color) {
- overlayEnabled = true;
- float colorFloat[4] = {
- (float)color.r/255.0f,
- (float)color.g/255.0f,
- (float)color.b/255.0f,
- (float)color.a/255.0f,
- };
-
- SetShaderValue(overlayShader, colorLoc, colorFloat, SHADER_UNIFORM_VEC4);
- BeginShaderMode(overlayShader);
-}
-
-void DisableColorOverlay() {
- if (overlayEnabled) {
- EndShaderMode();
- overlayEnabled = false;
- }
-}
-
-// Get a ray trace from the screen position (i.e mouse) within a specific section of the screen
-Ray GetScreenToWorldPointWithZDistance(Vector2 position, Camera camera, int screenWidth, int screenHeight, float zDistance)
-{
- Ray ray = { 0 };
-
- // Calculate normalized device coordinates
- // NOTE: y value is negative
- float x = (2.0f*position.x)/(float)screenWidth - 1.0f;
- float y = 1.0f - (2.0f*position.y)/(float)screenHeight;
- float z = 1.0f;
-
- // Store values in a vector
- Vector3 deviceCoords = { x, y, z };
-
- // Calculate view matrix from camera look at
- Matrix matView = MatrixLookAt(camera.position, camera.target, camera.up);
-
- Matrix matProj = MatrixIdentity();
-
- if (camera.projection == CAMERA_PERSPECTIVE)
- {
- // Calculate projection matrix from perspective
- matProj = MatrixPerspective(camera.fovy*DEG2RAD, ((double)screenWidth/(double)screenHeight), 0.01f, zDistance);
- }
- else if (camera.projection == CAMERA_ORTHOGRAPHIC)
- {
- double aspect = (double)screenWidth/(double)screenHeight;
- double top = camera.fovy/2.0;
- double right = top*aspect;
-
- // Calculate projection matrix from orthographic
- matProj = MatrixOrtho(-right, right, -top, top, 0.01, 1000.0);
- }
-
- // Unproject far/near points
- Vector3 nearPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 0.0f }, matProj, matView);
- Vector3 farPoint = Vector3Unproject((Vector3){ deviceCoords.x, deviceCoords.y, 1.0f }, matProj, matView);
-
- // Calculate normalized direction vector
- Vector3 direction = Vector3Normalize(Vector3Subtract(farPoint, nearPoint));
-
- ray.position = farPoint;
-
- // Apply calculated vectors to ray
- ray.direction = direction;
-
- return ray;
-}
-
-
-static inline Clay_Dimensions Raylib_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
- // Measure string size for Font
- Clay_Dimensions textSize = { 0 };
-
- float maxTextWidth = 0.0f;
- float lineTextWidth = 0;
- int maxLineCharCount = 0;
- int lineCharCount = 0;
-
- float textHeight = config->fontSize;
- Font* fonts = (Font*)userData;
- Font fontToUse = fonts[config->fontId];
- // Font failed to load, likely the fonts are in the wrong place relative to the execution dir.
- // RayLib ships with a default font, so we can continue with that built in one.
- if (!fontToUse.glyphs) {
- fontToUse = GetFontDefault();
- }
-
- float scaleFactor = config->fontSize/(float)fontToUse.baseSize;
-
- for (int i = 0; i < text.length; ++i, lineCharCount++)
- {
- if (text.chars[i] == '\n') {
- maxTextWidth = fmax(maxTextWidth, lineTextWidth);
- maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
- lineTextWidth = 0;
- lineCharCount = 0;
- continue;
- }
- int index = text.chars[i] - 32;
- if (fontToUse.glyphs[index].advanceX != 0) lineTextWidth += fontToUse.glyphs[index].advanceX;
- else lineTextWidth += (fontToUse.recs[index].width + fontToUse.glyphs[index].offsetX);
- }
-
- maxTextWidth = fmax(maxTextWidth, lineTextWidth);
- maxLineCharCount = CLAY__MAX(maxLineCharCount, lineCharCount);
-
- textSize.width = maxTextWidth * scaleFactor + (lineCharCount * config->letterSpacing);
- textSize.height = textHeight;
-
- return textSize;
-}
-
-void Clay_Raylib_Initialize(int width, int height, const char *title, unsigned int flags) {
- SetConfigFlags(flags);
- InitWindow(width, height, title);
- InitOverlay();
-// EnableEventWaiting();
-}
-
-// A MALLOC'd buffer, that we keep modifying inorder to save from so many Malloc and Free Calls.
-// Call Clay_Raylib_Close() to free
-static char *temp_render_buffer = NULL;
-static int temp_render_buffer_len = 0;
-
-// Call after closing the window to clean up the render buffer
-void Clay_Raylib_Close()
-{
- if(temp_render_buffer) free(temp_render_buffer);
- temp_render_buffer_len = 0;
-
- CloseWindow();
-}
-
-
-void Clay_Raylib_Render(Clay_RenderCommandArray renderCommands, Font* fonts)
-{
- for (int j = 0; j < renderCommands.length; j++)
- {
- Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, j);
- Clay_BoundingBox boundingBox = {renderCommand->boundingBox.x, renderCommand->boundingBox.y, renderCommand->boundingBox.width, renderCommand->boundingBox.height};
- switch (renderCommand->commandType)
- {
- case CLAY_RENDER_COMMAND_TYPE_TEXT: {
- Clay_TextRenderData *textData = &renderCommand->renderData.text;
- Font fontToUse = fonts[textData->fontId];
-
- int strlen = textData->stringContents.length + 1;
-
- if(strlen > temp_render_buffer_len) {
- // Grow the temp buffer if we need a larger string
- if(temp_render_buffer) free(temp_render_buffer);
- temp_render_buffer = (char *) malloc(strlen);
- temp_render_buffer_len = strlen;
- }
-
- // Raylib uses standard C strings so isn't compatible with cheap slices, we need to clone the string to append null terminator
- memcpy(temp_render_buffer, textData->stringContents.chars, textData->stringContents.length);
- temp_render_buffer[textData->stringContents.length] = '\0';
- DrawTextEx(fontToUse, temp_render_buffer, (Vector2){boundingBox.x, boundingBox.y}, (float)textData->fontSize, (float)textData->letterSpacing, CLAY_COLOR_TO_RAYLIB_COLOR(textData->textColor));
-
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
- Texture2D imageTexture = *(Texture2D *)renderCommand->renderData.image.imageData;
- Clay_Color tintColor = renderCommand->renderData.image.backgroundColor;
- if (tintColor.r == 0 && tintColor.g == 0 && tintColor.b == 0 && tintColor.a == 0) {
- tintColor = (Clay_Color) { 255, 255, 255, 255 };
- }
- DrawTexturePro(
- imageTexture,
- (Rectangle) { 0, 0, imageTexture.width, imageTexture.height },
- (Rectangle){boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height},
- (Vector2) {},
- 0,
- CLAY_COLOR_TO_RAYLIB_COLOR(tintColor));
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
- BeginScissorMode((int)roundf(boundingBox.x), (int)roundf(boundingBox.y), (int)roundf(boundingBox.width), (int)roundf(boundingBox.height));
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
- EndScissorMode();
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_START: {
- SetColorOverlay(CLAY_COLOR_TO_RAYLIB_COLOR(renderCommand->renderData.overlayColor.color));
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_OVERLAY_COLOR_END: {
- DisableColorOverlay();
- }
- case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
- Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle;
- if (config->cornerRadius.topLeft > 0) {
- float radius = (config->cornerRadius.topLeft * 2) / (float)((boundingBox.width > boundingBox.height) ? boundingBox.height : boundingBox.width);
- DrawRectangleRounded((Rectangle) { boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height }, radius, 8, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
- } else {
- DrawRectangle(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height, CLAY_COLOR_TO_RAYLIB_COLOR(config->backgroundColor));
- }
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_BORDER: {
- Clay_BorderRenderData *config = &renderCommand->renderData.border;
- // Left border
- if (config->width.left > 0) {
- DrawRectangleV((Vector2) { boundingBox.x, boundingBox.y + config->cornerRadius.topLeft }, (Vector2) { config->width.left, boundingBox.height - config->cornerRadius.topLeft - config->cornerRadius.bottomLeft }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- // Right border
- if (config->width.right > 0) {
- DrawRectangleV((Vector2) { boundingBox.x + boundingBox.width - config->width.right, boundingBox.y + config->cornerRadius.topRight }, (Vector2) { config->width.right, boundingBox.height - config->cornerRadius.topRight - config->cornerRadius.bottomRight }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- // Top border
- if (config->width.top > 0) {
- DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.topLeft, boundingBox.y }, (Vector2) { boundingBox.width - config->cornerRadius.topLeft - config->cornerRadius.topRight, (int)config->width.top }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- // Bottom border
- if (config->width.bottom > 0) {
- DrawRectangleV((Vector2) { boundingBox.x + config->cornerRadius.bottomLeft, boundingBox.y + boundingBox.height - config->width.bottom }, (Vector2) { boundingBox.width - config->cornerRadius.bottomLeft - config->cornerRadius.bottomRight, (int)config->width.bottom }, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- if (config->cornerRadius.topLeft > 0) {
- DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.topLeft), roundf(boundingBox.y + config->cornerRadius.topLeft) }, roundf(config->cornerRadius.topLeft - config->width.top), config->cornerRadius.topLeft, 180, 270, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- if (config->cornerRadius.topRight > 0) {
- DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.topRight), roundf(boundingBox.y + config->cornerRadius.topRight) }, roundf(config->cornerRadius.topRight - config->width.top), config->cornerRadius.topRight, 270, 360, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- if (config->cornerRadius.bottomLeft > 0) {
- DrawRing((Vector2) { roundf(boundingBox.x + config->cornerRadius.bottomLeft), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomLeft) }, roundf(config->cornerRadius.bottomLeft - config->width.bottom), config->cornerRadius.bottomLeft, 90, 180, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- if (config->cornerRadius.bottomRight > 0) {
- DrawRing((Vector2) { roundf(boundingBox.x + boundingBox.width - config->cornerRadius.bottomRight), roundf(boundingBox.y + boundingBox.height - config->cornerRadius.bottomRight) }, roundf(config->cornerRadius.bottomRight - config->width.bottom), config->cornerRadius.bottomRight, 0.1, 90, 10, CLAY_COLOR_TO_RAYLIB_COLOR(config->color));
- }
- break;
- }
- case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
- Clay_CustomRenderData *config = &renderCommand->renderData.custom;
- CustomLayoutElement *customElement = (CustomLayoutElement *)config->customData;
- if (!customElement) continue;
- switch (customElement->type) {
- case CUSTOM_LAYOUT_ELEMENT_TYPE_3D_MODEL: {
- Clay_BoundingBox rootBox = renderCommands.internalArray[0].boundingBox;
- float scaleValue = CLAY__MIN(CLAY__MIN(1, 768 / rootBox.height) * CLAY__MAX(1, rootBox.width / 1024), 1.5f);
- Ray positionRay = GetScreenToWorldPointWithZDistance((Vector2) { renderCommand->boundingBox.x + renderCommand->boundingBox.width / 2, renderCommand->boundingBox.y + (renderCommand->boundingBox.height / 2) + 20 }, Raylib_camera, (int)roundf(rootBox.width), (int)roundf(rootBox.height), 140);
- BeginMode3D(Raylib_camera);
- DrawModel(customElement->customData.model.model, positionRay.position, customElement->customData.model.scale * scaleValue, WHITE); // Draw 3d model with texture
- EndMode3D();
- break;
- }
- default: break;
- }
- break;
- }
- default: {
- printf("Error: unhandled render command.");
- exit(1);
- }
- }
- }
-}