#include <SDL.h>
#include <SDL_ttf.h>
#include "clay.h"
#include "editor.h"
#include "render.h"
#include "layout_cache.h"
#include "image_cache.h"
#include "fonts.h"
#include "theme.h"
#include "app_mode.h"
#include "block_type_menu.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>
static void handle_clay_errors(Clay_ErrorData errorData) {
fprintf(stderr, "Clay error: %s\n", errorData.errorText.chars);
}
static double now_seconds(void) {
return (double)SDL_GetPerformanceCounter() / (double)SDL_GetPerformanceFrequency();
}
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));
SDL_SetWindowTitle(window, title);
}
/* Whether `point` (current mouse position) falls inside the element `id`'s last-known bounding
box. Queried before this frame's render_frame/Clay_BeginLayout call, so it reflects the
previous frame's layout -- fine in practice since these floating popups don't move frame to
frame absent user action, the same assumption Clay_PointerOver itself relies on internally. */
static bool point_in_element(Clay_ElementId id, Clay_Vector2 point) {
Clay_ElementData d = Clay_GetElementData(id);
if (!d.found) return false;
return point.x >= d.boundingBox.x && point.x <= d.boundingBox.x + d.boundingBox.width
&& point.y >= d.boundingBox.y && point.y <= d.boundingBox.y + d.boundingBox.height;
}
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 -- 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();
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)winWi, (float)winHi }, (Clay_ErrorHandler){ handle_clay_errors, 0 });
SDL2_Font fonts[FONT_COUNT];
fonts_load(fonts, cfg.fontPaths);
config_free(&cfg);
Clay_SetMeasureTextFunction(hush_measure_text, fonts);
EditorState ed;
editor_init(&ed);
if (argc >= 2) {
editor_load(&ed, argv[1]);
}
update_window_title(window, &ed);
LayoutCache cache;
layout_cache_init(&cache);
ImageCache *imageCache;
image_cache_init(&imageCache);
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
document drag-select once mode flips back to MODE_EDITING. */
bool mouseDownInEditor = false;
/* The left-margin "Turn Into" popup: whether it's open, and which block it was opened for
(so moving the cursor to a different block auto-closes it rather than silently retargeting
a menu the user can no longer see is misaligned). */
bool turnIntoOpen = false;
int turnIntoAnchorBlock = -1;
/* The "@" quick-insert popup. `dismissed` is a per-trigger latch set by Escape or a
conversion, cleared again the moment the trigger condition re-edges true (a fresh "@" or a
move to a different block) -- see the reset logic below for why this can't just be "open
whenever the trigger condition holds". */
bool quickInsertOpen = false;
bool quickInsertDismissed = false;
int qiSelectedIndex = 0;
int qiLastCursorBlock = -1;
bool qiWasOpenLastFrame = false;
while (!windowClosing) {
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 (keymap_pressed(ACTION_SAVE, ctrl, shift, alt)) {
editor_save(&ed);
} 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) { SDL_SetClipboardText(text); free(text); }
} else if (keymap_pressed(ACTION_REDO, ctrl, shift, alt)) {
editor_redo(&ed);
} else if (keymap_pressed(ACTION_UNDO, ctrl, shift, alt)) {
editor_undo(&ed);
} 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 (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 (keymap_pressed(ACTION_SOURCE_VIEW, ctrl, shift, alt)) {
mode = MODE_SOURCE_VIEW;
} 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 (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 (keymap_pressed(ACTION_ZOOM_RESET, ctrl, shift, alt) || (ctrl && keymap_key_pressed_raw(SDLK_KP_0))) {
theme_zoom_reset();
} else if (turnIntoOpen && keymap_key_pressed_raw(SDLK_ESCAPE)) {
turnIntoOpen = false;
} else {
if (!ctrl && fi.textInputLen > 0) {
editor_insert_utf8(&ed, fi.textInput, fi.textInputLen);
}
/* Re-derive the "@" quick-insert popup's open state from the document itself
(see block_type_menu_quick_insert_trigger_active) rather than tracking it as
independent input-capture state -- the popup's filter text IS the block's own
"@word" content. `quickInsertDismissed` is a latch (Escape or a conversion) that
must NOT re-trigger just because the trigger condition still holds -- it only
resets on a genuinely fresh trigger edge (a new "@" typed, or the cursor landing
in a different block that also happens to read "@word"). */
bool qiShouldOpen = block_type_menu_quick_insert_trigger_active(&ed);
if (qiShouldOpen && (!qiWasOpenLastFrame || ed.cursorBlock != qiLastCursorBlock)) {
quickInsertDismissed = false;
qiSelectedIndex = 0;
}
qiLastCursorBlock = ed.cursorBlock;
qiWasOpenLastFrame = qiShouldOpen;
quickInsertOpen = qiShouldOpen && !quickInsertDismissed && !turnIntoOpen;
if (quickInsertOpen) {
Block *qb = &ed.doc.blocks[ed.cursorBlock];
const char *filter = qb->text.data + 1;
int filterLen = qb->text.len - 1;
int filteredCount = 0;
for (int i = 0; i < BLOCK_TYPE_MENU_ITEM_COUNT; i++) {
if (block_type_menu_item_matches(&BLOCK_TYPE_MENU_ITEMS[i], filter, filterLen)) filteredCount++;
}
if (filteredCount > 0) {
if (keymap_key_pressed_raw(SDLK_DOWN)) qiSelectedIndex = (qiSelectedIndex + 1) % filteredCount;
if (keymap_key_pressed_raw(SDLK_UP)) qiSelectedIndex = (qiSelectedIndex - 1 + filteredCount) % filteredCount;
if (qiSelectedIndex >= filteredCount) qiSelectedIndex = 0;
if (keymap_key_pressed_raw(SDLK_RETURN) || keymap_key_pressed_raw(SDLK_KP_ENTER)) {
BlockType chosenType = BLOCK_PARAGRAPH;
int idx = 0;
for (int i = 0; i < BLOCK_TYPE_MENU_ITEM_COUNT; i++) {
if (!block_type_menu_item_matches(&BLOCK_TYPE_MENU_ITEMS[i], filter, filterLen)) continue;
if (idx == qiSelectedIndex) { chosenType = BLOCK_TYPE_MENU_ITEMS[i].type; break; }
idx++;
}
/* BLOCK_TABLE_HEADER_CELL doubles as the Table row's sentinel --
inserting a table means creating rows*cols new blocks, which
doesn't fit editor_quick_insert_apply's single-block-type-change
signature, so it's intercepted here rather than passed through. */
if (chosenType == BLOCK_TABLE_HEADER_CELL) {
editor_insert_table(&ed, 2, 2);
} else {
editor_quick_insert_apply(&ed, chosenType);
}
quickInsertDismissed = true;
}
}
if (keymap_key_pressed_raw(SDLK_ESCAPE)) quickInsertDismissed = true;
} else {
if (keymap_key_pressed_raw(SDLK_RETURN) || keymap_key_pressed_raw(SDLK_KP_ENTER)) editor_enter(&ed);
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_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_key_pressed_raw(SDLK_HOME)) editor_move_home(&ed, shift);
if (keymap_key_pressed_raw(SDLK_END)) editor_move_end(&ed, shift);
if (block_type_is_table_cell(ed.doc.blocks[ed.cursorBlock].type) && keymap_key_pressed_raw(SDLK_TAB)) {
editor_table_move_cell(&ed, shift ? -1 : 1);
}
}
bool overTurnIntoUi = point_in_element(CLAY_ID("TurnIntoIcon"), mousePos)
|| (turnIntoOpen && point_in_element(CLAY_ID("TurnIntoMenuBox"), mousePos));
bool overQuickInsertUi = quickInsertOpen && point_in_element(CLAY_ID("QuickInsertMenu"), mousePos);
if (fi.mouseLeftPressed && !overTurnIntoUi && !overQuickInsertUi) {
editor_click(&ed, &cache, fonts, mousePos);
mouseDownInEditor = true;
} else if (mouseDownInEditor && mouseLeftDown && !overTurnIntoUi && !overQuickInsertUi) {
editor_drag_to(&ed, &cache, fonts, mousePos);
}
if (turnIntoOpen && ed.cursorBlock != turnIntoAnchorBlock) turnIntoOpen = false;
/* Safety net for state changes the catch-all above didn't see this frame (a mouse
click moved the cursor without any key press) -- forces the popup closed the
instant the pure trigger condition stops holding, rather than leaving it rendered
(mis-anchored to whatever block the cursor now sits in) until the next keystroke. */
if (quickInsertOpen && !block_type_menu_quick_insert_trigger_active(&ed)) quickInsertOpen = false;
/* 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 (keymap_key_pressed_raw(SDLK_ESCAPE)) mode = MODE_EDITING;
} else if (mode == MODE_SHORTCUTS_HELP) {
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 (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 (fi.mouseLeftReleased) mouseDownInEditor = false;
if (ed.cursorBlock != lastBlock || ed.cursorOffset != lastOffset) {
blinkResetTime = now;
lastBlock = ed.cursorBlock;
lastOffset = ed.cursorOffset;
}
if (ed.dirty != lastDirty) {
lastDirty = ed.dirty;
update_window_title(window, &ed);
}
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);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
int toggledTaskBlock;
TurnIntoMenuState turnIntoMenuState = { .open = turnIntoOpen };
QuickInsertMenuState quickInsertMenuState = { .open = quickInsertOpen, .selectedIndex = qiSelectedIndex };
BlockTypeMenuClickResult menuClick;
ModalAction action = render_frame(&ed, &cache, fonts, renderer, fi.mouseLeftPressed, (float)winW, (float)winH,
deltaTime, caretBlinkT, mode, &toggledTaskBlock, &openPrompt, confirmForOpen,
&turnIntoMenuState, &quickInsertMenuState, &menuClick, imageCache);
SDL_RenderPresent(renderer);
switch (action) {
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);
if (menuClick.turnIntoIconClicked) { turnIntoOpen = !turnIntoOpen; turnIntoAnchorBlock = ed.cursorBlock; }
if (menuClick.turnIntoClickedType >= 0) {
/* BLOCK_TABLE_HEADER_CELL doubles as the Table row's sentinel, same as in the
quick-insert path -- Turn-Into normally preserves a block's content, but there's
no meaningful way to preserve one block's text as a whole new grid, so this
discards it and inserts a fresh table instead, exactly like quick-insert does. */
if ((BlockType)menuClick.turnIntoClickedType == BLOCK_TABLE_HEADER_CELL) {
editor_insert_table(&ed, 2, 2);
} else {
editor_turn_into_apply(&ed, (BlockType)menuClick.turnIntoClickedType);
}
turnIntoOpen = false;
}
if (menuClick.turnIntoClickedOutside) turnIntoOpen = false;
if (menuClick.quickInsertClickedType >= 0) {
if ((BlockType)menuClick.quickInsertClickedType == BLOCK_TABLE_HEADER_CELL) {
editor_insert_table(&ed, 2, 2);
} else {
editor_quick_insert_apply(&ed, (BlockType)menuClick.quickInsertClickedType);
}
quickInsertDismissed = true;
quickInsertOpen = false;
}
}
layout_cache_free(&cache);
image_cache_free(imageCache);
editor_free(&ed);
fonts_unload(fonts);
lang_free();
SDL_StopTextInput();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
return 0;
}