commit 64ee43df2499e5a0d336e5191f1e81cb125fd0a6 Author: Jens Date: Thu Mar 5 23:46:48 2026 +0100 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f7c83c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*] +root = true +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76fbabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.idea +*.log +tmp/ + +build/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e16aacd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.23) + +project(editor) + +include(FetchContent) +include(FindPkgConfig) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 5.5 +) + +FetchContent_Declare( + sds + GIT_REPOSITORY https://github.com/antirez/sds.git + GIT_TAG 2.0.0 +) + +FetchContent_MakeAvailable(raylib) + +FetchContent_MakeAvailable(sds) +if(NOT TARGET sds) + add_library(sds STATIC "${sds_SOURCE_DIR}/sds.c") + target_include_directories(sds PUBLIC "${sds_SOURCE_DIR}") +endif() + +find_package(Freetype REQUIRED) +pkg_check_modules(HARFBUZZ REQUIRED harfbuzz) + +add_executable(editor) + +target_compile_features(editor PRIVATE c_std_11) +set_property(TARGET editor PROPERTY C_EXTENSIONS OFF) + +target_sources(editor + PRIVATE + main.c + editor.c +) + +target_compile_options(editor PRIVATE -Wall -Werror) +target_compile_options(editor PRIVATE ${HARFBUZZ_CFLAGS_OTHER}) + +target_include_directories(editor PRIVATE ${HARFBUZZ_INCLUDE_DIRS}) + +target_link_libraries(editor + PRIVATE + raylib + sds + Freetype::Freetype + ${HARFBUZZ_LIBRARIES} +) diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..ac0526e --- /dev/null +++ b/TODO.org @@ -0,0 +1,7 @@ +#+title: Todo + +* TODOs +** TODO render cursor +** TODO render better font (SDF?) +** TODO find functional language to embed +*** [?] do I need to write my own VM? diff --git a/editor.c b/editor.c new file mode 100644 index 0000000..77e2cf4 --- /dev/null +++ b/editor.c @@ -0,0 +1,5 @@ +#include "editor.h" + +EditorState init_editor_state(sds init_text) { + return (EditorState){.cursor_pos = 4, .text = init_text}; +} diff --git a/editor.h b/editor.h new file mode 100644 index 0000000..a380c0a --- /dev/null +++ b/editor.h @@ -0,0 +1,17 @@ +#ifndef EDITOR_H_ +#define EDITOR_H_ + +#include "sds.h" +#include + +typedef struct { + uint32_t cursor_pos; + sds text; +} EditorState; + +/* + * initializes the editor state + */ +EditorState init_editor_state(sds init_text); + +#endif // EDITOR_H_ diff --git a/main.c b/main.c new file mode 100644 index 0000000..5e4397c --- /dev/null +++ b/main.c @@ -0,0 +1,32 @@ +#include "editor.h" +#include "raylib.h" +#include +#include + +EditorState editor_state; +Font editor_font; + +void ed_load_font() {} + +int main() { + sds init_text = sdsnew("Das ist nur ein Test String🙂!"); + editor_state = init_editor_state(init_text); + + printf("%s\n", editor_state.text); + + InitWindow(800, 600, "editor"); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLACK); + + DrawText(editor_state.text, 8, 8, 18, RAYWHITE); + DrawRectangle(8 * editor_state.cursor_pos + 8, 8, 2, 18, GREEN); + + EndDrawing(); + } + + CloseWindow(); + + return 0; +} diff --git a/vendor/arena.h b/vendor/arena.h new file mode 100644 index 0000000..7ec4c8f --- /dev/null +++ b/vendor/arena.h @@ -0,0 +1,451 @@ +// Copyright 2022 Alexey Kutepov + +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#ifndef ARENA_H_ +#define ARENA_H_ + +#include +#include + +#ifndef ARENA_NOSTDIO +#include +#include +#endif // ARENA_NOSTDIO + +#ifndef ARENA_ASSERT +#include +#define ARENA_ASSERT assert +#endif + +#define ARENA_BACKEND_LIBC_MALLOC 0 +#define ARENA_BACKEND_LINUX_MMAP 1 +#define ARENA_BACKEND_WIN32_VIRTUALALLOC 2 +#define ARENA_BACKEND_WASM_HEAPBASE 3 + +#ifndef ARENA_BACKEND +#define ARENA_BACKEND ARENA_BACKEND_LIBC_MALLOC +#endif // ARENA_BACKEND + +typedef struct Region Region; + +struct Region { + Region *next; + size_t count; + size_t capacity; + uintptr_t data[]; +}; + +typedef struct { + Region *begin, *end; +} Arena; + +typedef struct { + Region *region; + size_t count; +} Arena_Mark; + +#ifndef ARENA_REGION_DEFAULT_CAPACITY +#define ARENA_REGION_DEFAULT_CAPACITY (8*1024) +#endif // ARENA_REGION_DEFAULT_CAPACITY + +Region *new_region(size_t capacity); +void free_region(Region *r); + +void *arena_alloc(Arena *a, size_t size_bytes); +void *arena_realloc(Arena *a, void *oldptr, size_t oldsz, size_t newsz); +char *arena_strdup(Arena *a, const char *cstr); +void *arena_memdup(Arena *a, void *data, size_t size); +void *arena_memcpy(void *dest, const void *src, size_t n); +#ifndef ARENA_NOSTDIO +char *arena_sprintf(Arena *a, const char *format, ...); +char *arena_vsprintf(Arena *a, const char *format, va_list args); +#endif // ARENA_NOSTDIO + +Arena_Mark arena_snapshot(Arena *a); +void arena_reset(Arena *a); +void arena_rewind(Arena *a, Arena_Mark m); +void arena_free(Arena *a); +void arena_trim(Arena *a); + +#ifndef ARENA_DA_INIT_CAP +#define ARENA_DA_INIT_CAP 256 +#endif // ARENA_DA_INIT_CAP + +#ifdef __cplusplus + #define cast_ptr(ptr) (decltype(ptr)) +#else + #define cast_ptr(...) +#endif + +#define arena_da_append(a, da, item) \ + do { \ + if ((da)->count >= (da)->capacity) { \ + size_t new_capacity = (da)->capacity == 0 ? ARENA_DA_INIT_CAP : (da)->capacity*2; \ + (da)->items = cast_ptr((da)->items)arena_realloc( \ + (a), (da)->items, \ + (da)->capacity*sizeof(*(da)->items), \ + new_capacity*sizeof(*(da)->items)); \ + (da)->capacity = new_capacity; \ + } \ + \ + (da)->items[(da)->count++] = (item); \ + } while (0) + +// Append several items to a dynamic array +#define arena_da_append_many(a, da, new_items, new_items_count) \ + do { \ + if ((da)->count + (new_items_count) > (da)->capacity) { \ + size_t new_capacity = (da)->capacity; \ + if (new_capacity == 0) new_capacity = ARENA_DA_INIT_CAP; \ + while ((da)->count + (new_items_count) > new_capacity) new_capacity *= 2; \ + (da)->items = cast_ptr((da)->items)arena_realloc( \ + (a), (da)->items, \ + (da)->capacity*sizeof(*(da)->items), \ + new_capacity*sizeof(*(da)->items)); \ + (da)->capacity = new_capacity; \ + } \ + arena_memcpy((da)->items + (da)->count, (new_items), (new_items_count)*sizeof(*(da)->items)); \ + (da)->count += (new_items_count); \ + } while (0) + +// Append a sized buffer to a string builder +#define arena_sb_append_buf arena_da_append_many + +// Append a NULL-terminated string to a string builder +#define arena_sb_append_cstr(a, sb, cstr) \ + do { \ + const char *s = (cstr); \ + size_t n = arena_strlen(s); \ + arena_da_append_many(a, sb, s, n); \ + } while (0) + +// Append a single NULL character at the end of a string builder. So then you can +// use it a NULL-terminated C string +#define arena_sb_append_null(a, sb) arena_da_append(a, sb, 0) + +#endif // ARENA_H_ + +#ifdef ARENA_IMPLEMENTATION + +#if ARENA_BACKEND == ARENA_BACKEND_LIBC_MALLOC +#include + +// TODO: instead of accepting specific capacity new_region() should accept the size of the object we want to fit into the region +// It should be up to new_region() to decide the actual capacity to allocate +Region *new_region(size_t capacity) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t)*capacity; + // TODO: it would be nice if we could guarantee that the regions are allocated by ARENA_BACKEND_LIBC_MALLOC are page aligned + Region *r = (Region*)malloc(size_bytes); + ARENA_ASSERT(r); // TODO: since ARENA_ASSERT is disableable go through all the places where we use it to check for failed memory allocation and return with NULL there. + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + free(r); +} +#elif ARENA_BACKEND == ARENA_BACKEND_LINUX_MMAP +#include +#include + +Region *new_region(size_t capacity) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t) * capacity; + Region *r = mmap(NULL, size_bytes, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + ARENA_ASSERT(r != MAP_FAILED); + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t) * r->capacity; + int ret = munmap(r, size_bytes); + ARENA_ASSERT(ret == 0); +} + +#elif ARENA_BACKEND == ARENA_BACKEND_WIN32_VIRTUALALLOC + +#if !defined(_WIN32) +# error "Current platform is not Windows" +#endif + +#define WIN32_LEAN_AND_MEAN +#include + +#define INV_HANDLE(x) (((x) == NULL) || ((x) == INVALID_HANDLE_VALUE)) + +Region *new_region(size_t capacity) +{ + SIZE_T size_bytes = sizeof(Region) + sizeof(uintptr_t) * capacity; + Region *r = VirtualAllocEx( + GetCurrentProcess(), /* Allocate in current process address space */ + NULL, /* Unknown position */ + size_bytes, /* Bytes to allocate */ + MEM_COMMIT | MEM_RESERVE, /* Reserve and commit allocated page */ + PAGE_READWRITE /* Permissions ( Read/Write )*/ + ); + if (INV_HANDLE(r)) + ARENA_ASSERT(0 && "VirtualAllocEx() failed."); + + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + if (INV_HANDLE(r)) + return; + + BOOL free_result = VirtualFreeEx( + GetCurrentProcess(), /* Deallocate from current process address space */ + (LPVOID)r, /* Address to deallocate */ + 0, /* Bytes to deallocate ( Unknown, deallocate entire page ) */ + MEM_RELEASE /* Release the page ( And implicitly decommit it ) */ + ); + + if (FALSE == free_result) + ARENA_ASSERT(0 && "VirtualFreeEx() failed."); +} + +#elif ARENA_BACKEND == ARENA_BACKEND_WASM_HEAPBASE + +// Stolen from https://surma.dev/things/c-to-webassembly/ + +extern unsigned char __heap_base; +// Since ARENA_BACKEND_WASM_HEAPBASE entirely hijacks __heap_base it is expected that no other means of memory +// allocation are used except the arenas. +unsigned char* bump_pointer = &__heap_base; +// TODO: provide a way to deallocate all the arenas at once by setting bump_pointer back to &__heap_base? + +// __builtin_wasm_memory_size and __builtin_wasm_memory_grow are defined in units of page sizes +#define ARENA_WASM_PAGE_SIZE (64*1024) + +Region *new_region(size_t capacity) +{ + size_t size_bytes = sizeof(Region) + sizeof(uintptr_t)*capacity; + Region *r = (void*)bump_pointer; + + // grow memory brk() style + size_t current_memory_size = ARENA_WASM_PAGE_SIZE * __builtin_wasm_memory_size(0); + size_t desired_memory_size = (size_t) bump_pointer + size_bytes; + if (desired_memory_size > current_memory_size) { + size_t delta_bytes = desired_memory_size - current_memory_size; + size_t delta_pages = (delta_bytes + (ARENA_WASM_PAGE_SIZE - 1))/ARENA_WASM_PAGE_SIZE; + if (__builtin_wasm_memory_grow(0, delta_pages) < 0) { + ARENA_ASSERT(0 && "memory.grow failed"); + return NULL; + } + } + + bump_pointer += size_bytes; + + r->next = NULL; + r->count = 0; + r->capacity = capacity; + return r; +} + +void free_region(Region *r) +{ + // Since ARENA_BACKEND_WASM_HEAPBASE uses a primitive bump allocator to + // allocate the regions, free_region() does nothing. It is generally + // not recommended to free arenas anyway since it is better to keep + // reusing already allocated memory with arena_reset(). + (void) r; +} + +#else +# error "Unknown Arena backend" +#endif + +// TODO: add debug statistic collection mode for arena +// Should collect things like: +// - How many times new_region was called +// - How many times existing region was skipped +// - How many times allocation exceeded ARENA_REGION_DEFAULT_CAPACITY + +void *arena_alloc(Arena *a, size_t size_bytes) +{ + size_t size = (size_bytes + sizeof(uintptr_t) - 1)/sizeof(uintptr_t); + + if (a->end == NULL) { + ARENA_ASSERT(a->begin == NULL); + size_t capacity = ARENA_REGION_DEFAULT_CAPACITY; + if (capacity < size) capacity = size; + a->end = new_region(capacity); + a->begin = a->end; + } + + while (a->end->count + size > a->end->capacity && a->end->next != NULL) { + a->end = a->end->next; + } + + if (a->end->count + size > a->end->capacity) { + ARENA_ASSERT(a->end->next == NULL); + size_t capacity = ARENA_REGION_DEFAULT_CAPACITY; + if (capacity < size) capacity = size; + a->end->next = new_region(capacity); + a->end = a->end->next; + } + + void *result = &a->end->data[a->end->count]; + a->end->count += size; + return result; +} + +void *arena_realloc(Arena *a, void *oldptr, size_t oldsz, size_t newsz) +{ + if (newsz <= oldsz) return oldptr; + void *newptr = arena_alloc(a, newsz); + char *newptr_char = (char*)newptr; + char *oldptr_char = (char*)oldptr; + for (size_t i = 0; i < oldsz; ++i) { + newptr_char[i] = oldptr_char[i]; + } + return newptr; +} + +size_t arena_strlen(const char *s) +{ + size_t n = 0; + while (*s++) n++; + return n; +} + +void *arena_memcpy(void *dest, const void *src, size_t n) +{ + char *d = dest; + const char *s = src; + for (; n; n--) *d++ = *s++; + return dest; +} + +char *arena_strdup(Arena *a, const char *cstr) +{ + size_t n = arena_strlen(cstr); + char *dup = (char*)arena_alloc(a, n + 1); + arena_memcpy(dup, cstr, n); + dup[n] = '\0'; + return dup; +} + +void *arena_memdup(Arena *a, void *data, size_t size) +{ + return arena_memcpy(arena_alloc(a, size), data, size); +} + +#ifndef ARENA_NOSTDIO +char *arena_vsprintf(Arena *a, const char *format, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + int n = vsnprintf(NULL, 0, format, args_copy); + va_end(args_copy); + + ARENA_ASSERT(n >= 0); + char *result = (char*)arena_alloc(a, n + 1); + vsnprintf(result, n + 1, format, args); + + return result; +} + +char *arena_sprintf(Arena *a, const char *format, ...) +{ + va_list args; + va_start(args, format); + char *result = arena_vsprintf(a, format, args); + va_end(args); + + return result; +} +#endif // ARENA_NOSTDIO + +Arena_Mark arena_snapshot(Arena *a) +{ + Arena_Mark m; + if(a->end == NULL){ //snapshot of uninitialized arena + ARENA_ASSERT(a->begin == NULL); + m.region = a->end; + m.count = 0; + }else{ + m.region = a->end; + m.count = a->end->count; + } + + return m; +} + +void arena_reset(Arena *a) +{ + for (Region *r = a->begin; r != NULL; r = r->next) { + r->count = 0; + } + + a->end = a->begin; +} + +void arena_rewind(Arena *a, Arena_Mark m) +{ + if(m.region == NULL){ //snapshot of uninitialized arena + arena_reset(a); //leave allocation + return; + } + + m.region->count = m.count; + for (Region *r = m.region->next; r != NULL; r = r->next) { + r->count = 0; + } + + a->end = m.region; +} + +void arena_free(Arena *a) +{ + Region *r = a->begin; + while (r) { + Region *r0 = r; + r = r->next; + free_region(r0); + } + a->begin = NULL; + a->end = NULL; +} + +void arena_trim(Arena *a){ + Region *r = a->end->next; + while (r) { + Region *r0 = r; + r = r->next; + free_region(r0); + } + a->end->next = NULL; +} + +#endif // ARENA_IMPLEMENTATION