Files
text_editor/src/text_editor/main.cpp

651 lines
27 KiB
C++

#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;
}
bool AreEqual(float a, float b, float epsilon = 0.001f) {
bool result = (a - epsilon < b && a + epsilon > b);
return result;
}
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<Edit> 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 PermArena;
Arena FrameArena;
struct DebugLine {
bool persist;
String string;
};
Array<DebugLine> DebugLines = {};
void Dbg(const char *str, ...) {
STRING_FORMAT(FrameArena, str, result);
DebugLines.add({false, result});
}
void DbgPersist(const char *str, ...) {
STRING_FORMAT(PermArena, str, result);
DebugLines.add({true, result});
}
void Dbg_Draw() {
int y = 0;
IterRemove(DebugLines) {
IterRemovePrepare(DebugLines);
if (it.persist == false) remove_item = true;
DrawText(it.string.data, 0, y, 20, BLACK);
y += 20;
}
}
int main() {
InitScratch();
RunBufferTests();
InitWindow(800, 600, "Hello");
SetTargetFPS(60);
InitArena(&FrameArena);
InitArena(&PermArena);
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<Window> 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<Edit> 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<Edit> 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<Edit> 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<String> 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<Edit> 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<Edit> edits = {FrameArena};
Array<String> 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<Edit> 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<Edit> 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);
Array<LayoutColumn> visible_columns = CalculateVisibleColumns(&FrameArena, window);
// Mouse selection
// @todo: multicursor
// @todo: selecting while not hovering over glyph shapes
{
SetMouseCursor(MOUSE_CURSOR_DEFAULT);
Vec2 mouse = GetMousePosition();
Vec2 mouse_lookup = Vector2Add(mouse, window.scroll);
Tuple<LayoutRow *, LayoutColumn *> 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)) {
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
window.cursors.clear();
window.cursors.add({rowcol.b->pos, rowcol.b->pos});
window.mouse_selecting = true;
}
if (!window.mouse_selecting) {
SetMouseCursor(MOUSE_CURSOR_IBEAM);
}
}
if (window.mouse_selecting) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
window.mouse_selecting = false;
}
Cursor *cursor = window.cursors.last();
cursor[0] = ChangeFront(*cursor, rowcol.b->pos);
}
}
}
// Update the scroll based on first cursor
// @todo: needs a rewrite, make sure we also handle scrolling for mouse
if (!AreEqual(window.main_cursor_begin_frame, window.cursors[0])) {
#if 1
Range visible_line_range = CalculateVisibleLineRange(window);
int64_t visible_lines = GetRangeSize(visible_line_range);
float visible_size_y = font_size * (float)visible_lines;
Vec2 rect_size = GetSize(window.rect);
float cut_off_y = visible_size_y - rect_size.y;
int64_t front = GetFront(window.cursors[0]);
Line line = FindLine(window.buffer, front);
// Tuple<LayoutRow, LayoutColumn> rowcol = GetRowCol(window, front);
// DbgPersist("line num = %d visible_line_range.max = %d", (int)line.number, (int)visible_line_range.max);
// Bottom
if (line.number > visible_line_range.max - 2) {
int64_t set_view_at_line = line.number - (visible_lines - 1);
window.scroll.y = (set_view_at_line * font_size) + cut_off_y;
}
if (line.number < visible_line_range.min + 1) {
int64_t set_view_at_line = line.number;
window.scroll.y = (set_view_at_line * font_size);
}
#else
Range visible_line_range = CalculateVisibleLineRange(window);
Vec2 rect_size = GetSize(window.rect);
float visible_cells_in_render_units = font_size * (float)GetRangeSize(visible_line_range);
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 < (visible_line_range.min)) {
window.scroll.y = line.number * font_size;
} else if (line.number >= (visible_line_range.max)) {
int64_t diff = line.number - visible_line_range.max;
window.scroll.y = (visible_line_range.min + 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;
}
#endif
}
// 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);
Range visible_line_range = CalculateVisibleLineRange(window);
for (int64_t line = visible_line_range.min; line < visible_line_range.max; 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
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) {
Rect2 rect = it.rect - window.scroll;
DrawRectangleRec(ToRectangle(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();
}
Dbg_Draw();
EndDrawing();
}
}