#define BASIC_IMPL #include "../basic/basic.h" #include "raylib.h" #include "raymath.h" // @todo: add history (undo, redo) // @todo: add clipboard history? #include "rect2.cpp" #include "buffer.cpp" #include "layout.cpp" Rect2 GetScreenRect() { Rect2 result = { { 0, 0}, {(float)GetRenderWidth(), (float)GetRenderHeight()} }; return result; } void DrawString(Font font, String text, Vector2 position, float fontSize, float spacing, Color tint) { if (font.texture.id == 0) font = GetFontDefault(); // Security check in case of not valid font float textOffsetX = 0.0f; // Offset X to next character to draw float scaleFactor = fontSize / font.baseSize; // Character quad scaling factor for (int i = 0; i < text.len;) { // Get next codepoint from byte string and glyph index in font int codepointByteCount = 0; int codepoint = GetCodepointNext(&text.data[i], &codepointByteCount); int index = GetGlyphIndex(font, codepoint); if ((codepoint != ' ') && (codepoint != '\t')) { DrawTextCodepoint(font, codepoint, {position.x + textOffsetX, position.y}, fontSize, tint); } if (font.glyphs[index].advanceX == 0) textOffsetX += ((float)font.recs[index].width * scaleFactor + spacing); else textOffsetX += ((float)font.glyphs[index].advanceX * scaleFactor + spacing); i += codepointByteCount; // Move text bytes counter to next codepoint } } Vector2 MeasureString(Font font, String text, float fontSize, float spacing) { Vector2 textSize = {0}; if ((font.texture.id == 0) || (text.data == NULL)) return textSize; // Security check int size = (int)text.len; // Get size in bytes of text int tempByteCounter = 0; // Used to count longer text line num chars int byteCounter = 0; float textWidth = 0.0f; float tempTextWidth = 0.0f; // Used to count longer text line width float textHeight = fontSize; float scaleFactor = fontSize / (float)font.baseSize; int letter = 0; // Current character int index = 0; // Index position in sprite font for (int i = 0; i < size;) { byteCounter++; int next = 0; letter = GetCodepointNext(&text.data[i], &next); index = GetGlyphIndex(font, letter); i += next; if (font.glyphs[index].advanceX != 0) textWidth += font.glyphs[index].advanceX; else textWidth += (font.recs[index].width + font.glyphs[index].offsetX); if (tempByteCounter < byteCounter) tempByteCounter = byteCounter; } if (tempTextWidth < textWidth) tempTextWidth = textWidth; textSize.x = tempTextWidth * scaleFactor + (float)((tempByteCounter - 1) * spacing); textSize.y = textHeight; return textSize; } int64_t MoveRight(Buffer &buffer, int64_t pos) { pos = pos + 1; pos = AdjustUTF8Pos(buffer, pos); Assert(pos >= 0 && pos <= buffer.len); return pos; } int64_t MoveLeft(Buffer &buffer, int64_t pos) { pos = pos - 1; pos = AdjustUTF8Pos(buffer, pos, -1); Assert(pos >= 0 && pos <= buffer.len); return pos; } int64_t MoveDown(Buffer &buffer, int64_t pos) { LineAndColumn info = FindLineAndColumn(buffer, pos); int64_t new_pos = FindPos(buffer, info.line.number + 1, info.column); return new_pos; } int64_t MoveUp(Buffer &buffer, int64_t pos) { LineAndColumn info = FindLineAndColumn(buffer, pos); int64_t new_pos = FindPos(buffer, info.line.number - 1, info.column); return new_pos; } void BeforeEdit(Window *window) { // Merge cursors that overlap, this needs to be handled before any edits to // make sure overlapping edits won't happen. for (int64_t cursor_i = 0; cursor_i < window->cursors.len; cursor_i += 1) { Cursor &cursor = window->cursors[cursor_i]; IterRemove(window->cursors) { IterRemovePrepare(window->cursors); if (&cursor == &it) continue; if (cursor.range.max >= it.range.min && cursor.range.max <= it.range.max) { remove_item = true; cursor.range.max = it.range.max; break; } } } } void AfterEdit(Window *window, Array edits) { // Offset all cursors by edits ForItem(edit, edits) { int64_t remove_size = GetRangeSize(edit.range); int64_t insert_size = edit.string.len; int64_t offset = insert_size - remove_size; ForItem(cursor, window->cursors) { if (edit.range.min == cursor.range.min) { if (GetRangeSize(edit.range)) { cursor.range.min += edit.string.len; } else { cursor.range.min += offset; } cursor.range.max = cursor.range.min; } else if (edit.range.min <= cursor.range.min) { if (GetRangeSize(edit.range)) { cursor.range.min += edit.string.len; } cursor.range.min += offset; cursor.range.max = cursor.range.min; } } } // Make sure all cursors are in range For(window->cursors) it.range = Clamp(window->buffer, it.range); Clear(&window->layout_arena); window->layout = CalculateLayout(&window->layout_arena, window->buffer, window->font, window->font_size, window->font_spacing); } Arena FrameArena; int main() { InitScratch(); RunBufferTests(); InitWindow(800, 600, "Hello"); SetTargetFPS(60); InitArena(&FrameArena); float font_size = 64; float font_spacing = 1; Font font = LoadFontEx("C:/Windows/Fonts/times.ttf", (int)font_size, NULL, 250); float title_bar_font_size = 15; Font title_bar_font = LoadFontEx("C:/Windows/Fonts/times.ttf", (int)title_bar_font_size, NULL, 250); if (0) font = LoadFontEx("C:/Windows/Fonts/consola.ttf", (int)font_size, NULL, 250); Array windows = {}; { Window window = {}; window.rect = GetScreenRect(); window.font = font; window.font_size = font_size; window.font_spacing; InitArena(&window.layout_arena); InitBuffer(&window.buffer); if (1) { for (int i = 0; i < 50; i += 1) { Array edits = {FrameArena}; AddEdit(&edits, GetEnd(window.buffer), Format(FrameArena, "line number: %d\n", i)); ApplyEdits(&window.buffer, edits); } } window.cursors.add({}); AfterEdit(&window, {}); windows.add(window); } // @todo: multiple ui elements, add focus // @todo: immediate mode interface for all this Vec2 camera_offset_world_to_render_units = {}; while (!WindowShouldClose()) { For(windows) { Assert(it.cursors.len); it.main_cursor_begin_frame = it.cursors[0]; } { Window *focused_window = &windows[0]; if (IsKeyDown(KEY_F1)) { camera_offset_world_to_render_units = Vector2Subtract(camera_offset_world_to_render_units, GetMouseDelta()); } if (IsKeyDown(KEY_F2)) { SetTraceLogLevel(LOG_DEBUG); } float mouse_wheel = GetMouseWheelMove() * 48; focused_window->scroll.y -= mouse_wheel; focused_window->scroll.y = ClampBottom(focused_window->scroll.y, 0.f); BeforeEdit(focused_window); // Merge cursors if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_A)) { focused_window->cursors.clear(); focused_window->cursors.add(MakeCursor(0, focused_window->buffer.len)); } if (IsKeyPressed(KEY_LEFT) || IsKeyPressedRepeat(KEY_LEFT)) { For(focused_window->cursors) { if (IsKeyDown(KEY_LEFT_CONTROL)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { int64_t front = GetFront(it); front = Seek(focused_window->buffer, front, ITERATE_BACKWARD); it = ChangeFront(it, front); } else { it.range.max = it.range.min = Seek(focused_window->buffer, it.range.min, ITERATE_BACKWARD); } } else { if (IsKeyDown(KEY_LEFT_SHIFT)) { int64_t front = GetFront(it); front = MoveLeft(focused_window->buffer, front); it = ChangeFront(it, front); } else { if (GetRangeSize(it.range) != 0) { it.range.max = it.range.min; } else { it.range.max = it.range.min = MoveLeft(focused_window->buffer, it.range.min); } } } } } if (IsKeyPressed(KEY_RIGHT) || IsKeyPressedRepeat(KEY_RIGHT)) { For(focused_window->cursors) { if (IsKeyDown(KEY_LEFT_CONTROL)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { int64_t front = GetFront(it); front = Seek(focused_window->buffer, front, ITERATE_FORWARD); it = ChangeFront(it, front); } else { it.range.max = it.range.min = Seek(focused_window->buffer, it.range.min, ITERATE_FORWARD); } } else { if (IsKeyDown(KEY_LEFT_SHIFT)) { int64_t front = GetFront(it); front = MoveRight(focused_window->buffer, front); it = ChangeFront(it, front); } else { if (GetRangeSize(it.range) != 0) { it.range.min = it.range.max; } else { it.range.max = it.range.min = MoveRight(focused_window->buffer, it.range.min); } } } } } if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) { if (IsKeyDown(KEY_LEFT_SHIFT) && IsKeyDown(KEY_LEFT_ALT)) { // Default in VSCode seems to be Ctrl + Alt + down For(focused_window->cursors) { int64_t front = GetFront(it); front = MoveDown(focused_window->buffer, front); focused_window->cursors.add({front, front}); } } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyDown(KEY_LEFT_ALT)) { // Default in VSCode seems to be Shift + Alt + down BeforeEdit(focused_window); Array edits = {FrameArena}; For(focused_window->cursors) { int64_t front = GetFront(it); Line line = FindLine(focused_window->buffer, front); String string = GetString(focused_window->buffer, {line.range.min, line.range.max + 1}); AddEdit(&edits, {line.range.max + 1, line.range.max + 1}, string); } ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); For(focused_window->cursors) { it.range.max = it.range.min = MoveDown(focused_window->buffer, it.range.min); } } else if (IsKeyDown(KEY_LEFT_SHIFT)) { For(focused_window->cursors) { int64_t front = GetFront(it); front = MoveDown(focused_window->buffer, front); it = ChangeFront(it, front); } } else { For(focused_window->cursors) { it.range.max = it.range.min = MoveDown(focused_window->buffer, it.range.min); } } } if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) { if (IsKeyDown(KEY_LEFT_SHIFT) && IsKeyDown(KEY_LEFT_ALT)) { // Default in VSCode seems to be Ctrl + Alt + up For(focused_window->cursors) { int64_t front = GetFront(it); front = MoveUp(focused_window->buffer, front); focused_window->cursors.add({front, front}); } } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyDown(KEY_LEFT_ALT)) { // Default in VSCode seems to be Shift + Alt + up BeforeEdit(focused_window); Array edits = {FrameArena}; For(focused_window->cursors) { int64_t front = GetFront(it); Line line = FindLine(focused_window->buffer, front); String string = GetString(focused_window->buffer, {line.range.min, line.range.max + 1}); AddEdit(&edits, {line.range.min, line.range.min}, string); } ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); For(focused_window->cursors) { it.range.max = it.range.min = MoveUp(focused_window->buffer, it.range.min); } } else if (IsKeyDown(KEY_LEFT_SHIFT)) { For(focused_window->cursors) { int64_t front = GetFront(it); front = MoveUp(focused_window->buffer, front); it = ChangeFront(it, front); } } else { For(focused_window->cursors) { it.range.max = it.range.min = MoveUp(focused_window->buffer, it.range.min); } } } // @todo: improve behaviour of all copy pasting if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)) { Array strings = {FrameArena}; For(focused_window->cursors) { String string = GetString(focused_window->buffer, it.range); strings.add(string); } String to_save = Merge(FrameArena, strings, "\n"); SetClipboardText(to_save.data); } if (IsKeyDown(KEY_LEFT_CONTROL) && (IsKeyPressed(KEY_V) || IsKeyPressedRepeat(KEY_V))) { BeforeEdit(focused_window); Array edits = {FrameArena}; const char *text = GetClipboardText(); String string = text; For(focused_window->cursors) { AddEdit(&edits, it.range, string); } ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); } if (IsKeyDown(KEY_LEFT_CONTROL) && (IsKeyPressed(KEY_X) || IsKeyPressedRepeat(KEY_X))) { // First, if there is no selection - select the entire line For(focused_window->cursors) { if (GetRangeSize(it.range) == 0) { Line line = FindLine(focused_window->buffer, it.range.min); it.range = {line.range.min, line.range.max + 1}; } } BeforeEdit(focused_window); Array edits = {FrameArena}; Array strings = {FrameArena}; For(focused_window->cursors) { AddEdit(&edits, it.range, ""); String string = GetString(focused_window->buffer, it.range); strings.add(string); } String to_save = Merge(FrameArena, strings, "\n"); SetClipboardText(to_save.data); ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); } if (IsKeyPressed(KEY_ENTER)) { if (IsKeyDown(KEY_LEFT_CONTROL)) { } else { } } if (IsKeyPressed(KEY_HOME) || IsKeyPressedRepeat(KEY_HOME)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { } else { } } if (IsKeyPressed(KEY_END) || IsKeyPressedRepeat(KEY_END)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { } else { } } if (IsKeyDown(KEY_LEFT_CONTROL) && (IsKeyPressed(KEY_D) || IsKeyPressedRepeat(KEY_D))) { } if (IsKeyPressed(KEY_DELETE) || IsKeyPressedRepeat(KEY_DELETE)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { } } if (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) { if (IsKeyDown(KEY_LEFT_SHIFT)) { } else { BeforeEdit(focused_window); Array edits = {FrameArena}; String string = {}; For(focused_window->cursors) { int64_t pos = MoveLeft(focused_window->buffer, it.range.min); AddEdit(&edits, {pos, it.range.min}, string); } ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); } } // Handle user input for (;;) { int c = GetCharPressed(); if (!c) break; String string = "?"; UTF8Result utf8 = UTF32ToUTF8((uint32_t)c); if (utf8.error == 0) { string = {(char *)utf8.out_str, (int64_t)utf8.len}; } BeforeEdit(focused_window); Array edits = {FrameArena}; For(focused_window->cursors) { AddEdit(&edits, it.range, string); } ApplyEdits(&focused_window->buffer, edits); AfterEdit(focused_window, edits); } } BeginDrawing(); ClearBackground(RAYWHITE); ForItem(window, windows) { Rectangle rectangle_in_render_units = ToRectangle(window.rect); DrawRectangleRec(rectangle_in_render_units, WHITE); // Figure out which lines to draw Vec2 s = GetSize(window.rect); float line_offset = font_size; float _line_min_y = (window.scroll.y) / line_offset; float _line_max_y = (s.y + window.scroll.y) / line_offset; int64_t line_min_y = (int64_t)floorf(_line_min_y); int64_t line_max_y = (int64_t)ceilf(_line_max_y); int64_t visible_lines = line_max_y - line_min_y; Array visible_columns = {FrameArena}; for (int64_t line = line_min_y; line < line_max_y && line >= 0 && line < window.layout.rows.len; line += 1) { LayoutRow &row = window.layout.rows[line]; For(row.columns) { if (CheckCollisionRecs(ToRectangle(it.rect), ToRectangle(window.rect))) { visible_columns.add(it); } } } // Update the scroll based on first cursor if (!AreEqual(window.main_cursor_begin_frame, window.cursors[0])) { Vec2 rect_size = GetSize(window.rect); float visible_cells_in_render_units = font_size * (float)visible_lines; float cut_off_in_render_units = visible_cells_in_render_units - rect_size.y; Cursor cursor = window.cursors[0]; int64_t front = GetFront(cursor); Line line = FindLine(window.buffer, front); // Scroll Y if (line.number < line_min_y) { window.scroll.y = line.number * font_size; } else if (line.number >= line_max_y) { int64_t diff = line.number - line_max_y; window.scroll.y = (line_min_y + diff) * font_size + cut_off_in_render_units; } // Scroll X Range x_distance = {line.range.min, front}; String x_distance_string = GetString(window.buffer, x_distance); Vec2 size = MeasureString(font, x_distance_string, font_size, font_spacing); if (x_distance_string.len <= 0) size.x = 0; float x_cursor_position_in_window_buffer_world_units = size.x; GlyphInfo info = GetGlyphInfo(font, ' '); float right_scroll_zone = (float)(info.image.width + info.advanceX) * 3; float window_buffer_world_right_edge = (rect_size.x + window.scroll.x) - right_scroll_zone; float window_buffer_world_left_edge = window.scroll.x; if (x_cursor_position_in_window_buffer_world_units >= window_buffer_world_right_edge) { float diff = x_cursor_position_in_window_buffer_world_units - window_buffer_world_right_edge; window.scroll.x += diff; } else if (x_cursor_position_in_window_buffer_world_units <= window_buffer_world_left_edge) { float diff = x_cursor_position_in_window_buffer_world_units - window_buffer_world_left_edge; window.scroll.x += diff; } } // Mouse selection @todo { Vec2 mouse = GetMousePosition(); Vec2 mouse_lookup = Vector2Add(mouse, window.scroll); Tuple rowcol = GetRowCol(window, mouse_lookup); if (rowcol.b) { Rect2 col_rect = rowcol.b->rect - window.scroll; Rectangle col_rectangle = ToRectangle(col_rect); if (CheckCollisionPointRec(mouse, col_rectangle)) { DrawRectangleRec(col_rectangle, {0, 0, 255, 40}); } } } // Draw the glyphs Vec2 window_rect_size = GetSize(window.rect); BeginScissorMode((int)window.rect.min.x, (int)window.rect.min.y, (int)window_rect_size.x, (int)window_rect_size.y); for (int64_t line = line_min_y; line < line_max_y; line += 1) { if (line < 0 || line >= window.layout.rows.len) continue; LayoutRow &row = window.layout.rows[line]; ForItem(col, row.columns) { Vec2 p0 = Vector2Subtract(col.rect.min, window.scroll); Vec2 p1 = Vector2Subtract(col.rect.max, window.scroll); Rect2 rect = {p0, p1}; if (!CheckCollisionRecs(ToRectangle(rect), ToRectangle(window.rect))) { continue; // Clip everything that is outside the window and screen } if (col.codepoint == '\n') { Vec2 mid = GetMid(rect); DrawCircle((int)mid.x, (int)mid.y, font_size / 8, {0, 0, 0, 120}); } else if ((col.codepoint != ' ') && (col.codepoint != '\t')) { DrawTextCodepoint(font, col.codepoint, rect.min, font_size, BLACK); } } } // Draw cursor stuff @todo: draw selection ForItem(cursor, window.cursors) { auto front = GetRowCol(window, GetFront(cursor)); auto back = GetRowCol(window, GetBack(cursor)); For(visible_columns) { if (it.pos >= cursor.range.min && it.pos < cursor.range.max) { DrawRectangleRec(ToRectangle(it.rect), {0, 50, 150, 50}); } } if (front.b) { Rect2 rect = front.b->rect; rect -= window.scroll; rect = CutLeft(&rect, 4); DrawRectangleRec(ToRectangle(rect), RED); } if (back.b) { Rect2 rect = back.b->rect; rect -= window.scroll; rect = CutLeft(&rect, 2); DrawRectangleRec(ToRectangle(rect), GREEN); } } EndScissorMode(); } EndDrawing(); } }