Major refactor of text layout

This commit is contained in:
Krzosa Karol
2024-06-28 09:30:22 +02:00
parent 784028c7a9
commit 95cafa3200
4 changed files with 248 additions and 257 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ x64/Release
.vs/
src/external
build/
build/
*.rdbg

134
src/text_editor/layout.cpp Normal file
View File

@@ -0,0 +1,134 @@
struct LayoutColumn {
Rect2 rect;
int64_t pos;
int codepoint;
};
struct LayoutRow {
Rect2 rect;
Array<LayoutColumn> columns;
};
struct Layout {
Array<LayoutRow> rows;
Vec2 buffer_world_pixel_size;
};
struct Window {
Font font;
float font_size;
float font_spacing;
Rect2 rect;
Vec2 scroll; // window_world_to_window_units
Cursor main_cursor_begin_frame;
Array<Cursor> cursors;
Buffer buffer;
Arena layout_arena;
Layout layout;
};
template <class T1, class T2>
struct Tuple {
T1 a;
T2 b;
};
Layout CalculateLayout(Arena *arena, Buffer &buffer, Font font, float font_size, float font_spacing) {
Layout layout = {};
layout.rows.allocator = *arena;
float scaleFactor = font_size / font.baseSize; // Character quad scaling factor
float text_offset_y = 0;
ForItem(line_range, buffer.lines) {
float textOffsetX = 0.0f;
LayoutRow *row = layout.rows.alloc();
row->columns.allocator = *arena;
row->rect.min = {textOffsetX, text_offset_y};
BufferIter iter = Iterate(buffer, line_range);
for (;; Advance(&iter)) {
bool end_of_buffer = iter.pos == buffer.len;
bool new_line = iter.pos == line_range.max;
bool in_range = IsValid(iter);
bool continue_looping = end_of_buffer || new_line || in_range;
if (!continue_looping) break;
int codepoint = '\n'; // @todo: questionable choice
if (in_range) codepoint = iter.item;
int index = GetGlyphIndex(font, codepoint);
GlyphInfo *glyph = font.glyphs + index;
Vec2 glyph_position = {textOffsetX, text_offset_y};
float x_to_offset_by = ((float)glyph->advanceX * scaleFactor + font_spacing);
if (glyph->advanceX == 0) x_to_offset_by = ((float)font.recs[index].width * scaleFactor + font_spacing);
Vec2 cell_size = {x_to_offset_by, font_size};
Rect2 cell_rect = {glyph_position, Vector2Add(glyph_position, cell_size)};
row->columns.add({cell_rect, iter.pos, codepoint});
row->rect.max = cell_rect.max;
textOffsetX += x_to_offset_by;
if (end_of_buffer || new_line) break;
}
layout.buffer_world_pixel_size.x = Max(layout.buffer_world_pixel_size.x, textOffsetX);
text_offset_y += font_size;
}
layout.buffer_world_pixel_size.y = text_offset_y;
return layout;
}
LayoutRow *GetLayoutRow(Window &window, float ypos_window_buffer_world_units) {
float line_spacing = window.font_size;
int64_t line = (int64_t)floorf(ypos_window_buffer_world_units / line_spacing);
if (line >= 0 && line < window.layout.rows.len) {
return window.layout.rows.data + line;
}
return NULL;
}
LayoutColumn *GetLayoutColumn(LayoutRow *row, float xpos_window_buffer_world_units) {
if (!row) return NULL;
For(row->columns) {
if (xpos_window_buffer_world_units >= it.rect.min.x && xpos_window_buffer_world_units <= it.rect.max.x) {
return &it;
}
}
return NULL;
}
Tuple<LayoutRow *, LayoutColumn *> GetRowCol(Window &window, Vec2 pos_buffer_world_units) {
Tuple<LayoutRow *, LayoutColumn *> result = {};
result.a = GetLayoutRow(window, pos_buffer_world_units.y);
result.b = GetLayoutColumn(result.a, pos_buffer_world_units.x);
return result;
}
Tuple<LayoutRow *, LayoutColumn *> GetRowCol(Window &window, int64_t pos) {
Tuple<LayoutRow *, LayoutColumn *> result = {};
For(window.layout.rows) {
if (window.layout.rows.len == 0) continue;
int64_t first_pos = it.columns.first()->pos;
int64_t last_pos = it.columns.last()->pos;
if (pos >= first_pos && pos <= last_pos) {
result.a = &it;
break;
}
}
if (result.a) {
For(result.a->columns) {
if (it.pos == pos) {
result.b = &it;
break;
}
}
}
return result;
}

View File

@@ -5,50 +5,11 @@
// @todo: add history (undo, redo)
// @todo: add clipboard history?
#include "buffer.cpp"
#include "rect2.cpp"
#include "buffer.cpp"
#include "layout.cpp"
// Render units - positions ready to draw, y
// World units - positions offset by screen movement
// Window units - positions inside the window (starts in left top of window)
// WindowBuffer units
// WindowBufferWorld units
struct Window {
uint64_t flags;
Rect2 rect_in_world_units;
Vec2 scroll; // window_world_to_window_units
float title_bar_pixel_size;
float line_number_bar_pixel_size;
float right_scroll_bar_pixel_size;
float bottom_scroll_bar_pixel_size;
Cursor main_cursor_begin_frame;
Array<Cursor> cursors;
Buffer buffer;
};
Vec2 WindowBufferWorldToWindowBufferUnits(Vec2 value, const Window &window) {
Vec2 result = Vector2Subtract(value, window.scroll);
return result;
}
Vec2 WindowBufferToWindowUnits(Vec2 value, Vec2 window_buffer_to_window_units) {
Vec2 result = Vector2Add(value, window_buffer_to_window_units);
return result;
}
Vec2 WorldToRenderUnits(Vec2 value, Vec2 camera_offset_world_to_render_units) {
Vec2 result = Vector2Subtract(value, camera_offset_world_to_render_units);
return result;
}
Vec2 WindowToWorldUnits(Vec2 value, const Window &window) {
Vector2 result = Vector2Add(value, window.rect_in_world_units.min);
return result;
}
Rect2 GetScreenRectRenderUnits() {
Rect2 GetScreenRect() {
Rect2 result = {
{ 0, 0},
{(float)GetRenderWidth(), (float)GetRenderHeight()}
@@ -165,7 +126,7 @@ void BeforeEdit(Window *window) {
}
void AfterEdit(Window *window, Array<Edit> edits) {
// First offset all cursors by edits
// Offset all cursors by edits
ForItem(edit, edits) {
int64_t remove_size = GetRangeSize(edit.range);
int64_t insert_size = edit.string.len;
@@ -188,7 +149,12 @@ void AfterEdit(Window *window, Array<Edit> edits) {
}
}
}
// 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;
@@ -210,26 +176,25 @@ int main() {
Array<Window> windows = {};
{
Window window = {};
window.rect_in_world_units = GetScreenRectRenderUnits();
window.rect_in_world_units.max.x *= 2;
window.rect_in_world_units.max.y *= 2;
window.title_bar_pixel_size = 20;
window.line_number_bar_pixel_size = 40;
window.right_scroll_bar_pixel_size = 30;
window.bottom_scroll_bar_pixel_size = 30;
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 sddddddddddddssssssssss faasda s: %d\n", i));
AddEdit(&edits, GetEnd(window.buffer), Format(FrameArena, "line number: %d\n", i));
ApplyEdits(&window.buffer, edits);
}
}
window.cursors.add({});
AfterEdit(&window, {});
windows.add(window);
}
@@ -487,139 +452,44 @@ int main() {
ClearBackground(RAYWHITE);
ForItem(window, windows) {
Rect2 window_rect_in_render_units = {
WorldToRenderUnits(window.rect_in_world_units.min, camera_offset_world_to_render_units),
WorldToRenderUnits(window.rect_in_world_units.max, camera_offset_world_to_render_units),
};
Rectangle rectangle_in_render_units = ToRectangle(window_rect_in_render_units);
Rectangle rectangle_in_render_units = ToRectangle(window.rect);
DrawRectangleRec(rectangle_in_render_units, WHITE);
Rect2 window_text_rect_in_render_units = window_rect_in_render_units;
Rect2 window_text_rect_in_render_units_clamped_to_screen = window_text_rect_in_render_units;
Rect2 screen_rect_in_render_units = GetScreenRectRenderUnits();
window_text_rect_in_render_units_clamped_to_screen.min.x = Clamp(window_text_rect_in_render_units_clamped_to_screen.min.x, screen_rect_in_render_units.min.x, screen_rect_in_render_units.max.x);
window_text_rect_in_render_units_clamped_to_screen.max.x = Clamp(window_text_rect_in_render_units_clamped_to_screen.max.x, screen_rect_in_render_units.min.x, screen_rect_in_render_units.max.x);
window_text_rect_in_render_units_clamped_to_screen.min.y = Clamp(window_text_rect_in_render_units_clamped_to_screen.min.y, screen_rect_in_render_units.min.y, screen_rect_in_render_units.max.y);
window_text_rect_in_render_units_clamped_to_screen.max.y = Clamp(window_text_rect_in_render_units_clamped_to_screen.max.y, screen_rect_in_render_units.min.y, screen_rect_in_render_units.max.y);
// 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;
Vec2 window_buffer_to_window_units = {window.line_number_bar_pixel_size, window.title_bar_pixel_size};
Rect2 title_bar_in_render_units = CutTop(&window_text_rect_in_render_units_clamped_to_screen, window.title_bar_pixel_size);
Rect2 line_number_bar_in_render_units = CutLeft(&window_text_rect_in_render_units_clamped_to_screen, window.line_number_bar_pixel_size);
Rect2 bottom_scroll_bar_in_render_units = CutBottom(&window_text_rect_in_render_units_clamped_to_screen, window.bottom_scroll_bar_pixel_size);
Rect2 right_scroll_bar_in_render_units = CutRight(&window_text_rect_in_render_units_clamped_to_screen, window.right_scroll_bar_pixel_size);
DrawRectangleRec(ToRectangle(title_bar_in_render_units), GRAY);
DrawRectangleRec(ToRectangle(line_number_bar_in_render_units), GRAY);
DrawRectangleRec(ToRectangle(bottom_scroll_bar_in_render_units), GRAY);
DrawRectangleRec(ToRectangle(right_scroll_bar_in_render_units), GRAY);
DrawString(title_bar_font, "title bar :)", title_bar_in_render_units.min, title_bar_font_size, font_spacing, BLACK);
if (0) {
window_text_rect_in_render_units_clamped_to_screen = Shrink(window_text_rect_in_render_units_clamped_to_screen, 10);
DrawRectangleRec(ToRectangle(window_text_rect_in_render_units_clamped_to_screen), {255, 0, 0, 50});
}
struct Cell {
Rect2 rect;
int codepoint;
int64_t column;
int64_t pos;
};
struct CellRow {
Array<Cell> cells;
Rect2 rect;
int64_t line;
};
// Compute visible rows and cells
Array<CellRow> rows = {FrameArena};
{
// Figure out which lines to draw
Vec2 s = GetSize(window_text_rect_in_render_units);
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);
for (int64_t line = line_min_y; line < line_max_y; line += 1) {
if (line < 0) break;
if (line >= window.buffer.lines.len) break;
Range line_range = window.buffer.lines[line];
Vec2 text_position_in_window_buffer_world_units = {0, line_offset * (float)line};
Vec2 text_position_in_window_buffer_units = WindowBufferWorldToWindowBufferUnits(text_position_in_window_buffer_world_units, window);
Vec2 text_position_in_window_units = WindowBufferToWindowUnits(text_position_in_window_buffer_units, window_buffer_to_window_units);
Vec2 text_position_in_world_units = WindowToWorldUnits(text_position_in_window_units, window);
Vec2 text_position_in_render_units = WorldToRenderUnits(text_position_in_world_units, camera_offset_world_to_render_units);
CellRow row = {};
row.line = line;
row.cells.allocator = FrameArena;
row.rect.min = text_position_in_render_units;
// Iterate the string and compute cells for current font
// we are also incorporating - new line and end of buffer glyphs
// into the stream!
String text = GetString(window.buffer, line_range);
if (font.texture.id == 0) font = GetFontDefault();
float textOffsetX = 0.0f;
float scaleFactor = font_size / font.baseSize; // Character quad scaling factor
for (BufferIter iter = Iterate(window.buffer, line_range);; Advance(&iter)) {
bool end_of_buffer = iter.pos == window.buffer.len;
bool new_line = iter.pos == line_range.max;
bool in_range = IsValid(iter);
bool continue_looping = end_of_buffer || new_line || in_range;
if (!continue_looping) break;
int codepoint = '\n';
if (in_range) codepoint = iter.item;
int index = GetGlyphIndex(font, codepoint);
GlyphInfo *glyph = font.glyphs + index;
Vec2 glyph_position = {text_position_in_render_units.x + textOffsetX, text_position_in_render_units.y};
float x_to_offset_by = ((float)glyph->advanceX * scaleFactor + font_spacing);
if (glyph->advanceX == 0) x_to_offset_by = ((float)font.recs[index].width * scaleFactor + font_spacing);
Vec2 cell_size = {x_to_offset_by, font_size};
Rect2 cell_rect = {glyph_position, Vector2Add(glyph_position, cell_size)};
Rectangle cell_rectangle = ToRectangle(cell_rect);
// Clip everything that is outside the window and screen
if (CheckCollisionRecs(cell_rectangle, ToRectangle(window_text_rect_in_render_units_clamped_to_screen))) {
row.cells.add({cell_rect, codepoint, iter.codepoint_index, iter.pos});
row.rect.max = cell_rect.max;
}
textOffsetX += x_to_offset_by;
if (end_of_buffer || new_line) break;
Array<LayoutColumn> 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);
}
if (row.cells.len) rows.add(row);
}
}
// Update the scroll based on first cursor
if (!AreEqual(window.main_cursor_begin_frame, window.cursors[0])) {
Vec2 rect_in_render_units = GetSize(window_text_rect_in_render_units_clamped_to_screen);
float visible_cells_in_render_units = font_size * (float)rows.len;
float cut_off_in_render_units = visible_cells_in_render_units - rect_in_render_units.y;
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
int64_t min_line = rows[0].line;
int64_t max_line = rows[rows.len - 1].line;
if (line.number < min_line) {
if (line.number < line_min_y) {
window.scroll.y = line.number * font_size;
} else if (line.number >= max_line) {
int64_t diff = line.number - max_line;
window.scroll.y = (min_line + diff) * font_size + cut_off_in_render_units;
} 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
@@ -631,7 +501,7 @@ int main() {
GlyphInfo info = GetGlyphInfo(font, ' ');
float right_scroll_zone = (float)(info.image.width + info.advanceX) * 3;
float window_buffer_world_right_edge = (rect_in_render_units.x + window.scroll.x) - right_scroll_zone;
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;
@@ -642,108 +512,73 @@ int main() {
}
}
// Draw debug markers
if (0) {
ForItem(row, rows) {
For(row.cells) {
DrawRectangleLinesEx(ToRectangle(it.rect), 1, {255, 0, 0, 50});
}
DrawRectangleLinesEx(ToRectangle(row.rect), 1, {0, 190, 50, 255});
}
}
// Mouse in text area
// Mouse selection @todo
{
// @todo: click twice to select word
// @tood: scrolling when selecting (Y and X)
// @todo: change cursors
Vec2 mouse_in_render_units = GetMousePosition();
if (CheckCollisionPointRec(mouse_in_render_units, ToRectangle(window_text_rect_in_render_units_clamped_to_screen))) {
ForItem(row, rows) {
ForItem(cell, row.cells) {
if (CheckCollisionPointRec(mouse_in_render_units, ToRectangle(cell.rect))) {
DrawRectangleRec(ToRectangle(cell.rect), {0, 0, 255, 40});
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (IsKeyDown(KEY_LEFT_CONTROL)) {
window.cursors.add({cell.pos, cell.pos});
} else {
window.cursors.clear();
window.cursors.add({cell.pos, cell.pos});
}
} else if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
Cursor *c = window.cursors.last();
*c = ChangeBack(*c, cell.pos);
}
}
}
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)) {
DrawRectangleRec(col_rectangle, {0, 0, 255, 40});
}
}
}
// Draw the glyphs
Vec2 window_text_rect_in_render_units_clamped_to_screen_size = GetSize(window_text_rect_in_render_units_clamped_to_screen);
BeginScissorMode((int)window_text_rect_in_render_units_clamped_to_screen.min.x, (int)window_text_rect_in_render_units_clamped_to_screen.min.y, (int)window_text_rect_in_render_units_clamped_to_screen_size.x, (int)window_text_rect_in_render_units_clamped_to_screen_size.y);
ForItem(row, rows) {
For(row.cells) {
if (it.codepoint == '\n') {
Vec2 mid = GetMid(it.rect);
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 ((it.codepoint != ' ') && (it.codepoint != '\t')) {
DrawTextCodepoint(font, it.codepoint, it.rect.min, font_size, BLACK);
} else if ((col.codepoint != ' ') && (col.codepoint != '\t')) {
DrawTextCodepoint(font, col.codepoint, rect.min, font_size, BLACK);
}
}
}
// Draw cursor stuff
// Draw cursor stuff @todo: draw selection
ForItem(cursor, window.cursors) {
Line min_line = FindLine(window.buffer, cursor.range.min);
Line max_line = FindLine(window.buffer, cursor.range.max);
bool selecting = cursor.range.min != cursor.range.max;
ForItem(row, rows) {
// Draw line highlight
if (row.line == min_line.number) {
DrawRectangleRec(ToRectangle(row.rect), {255, 0, 0, 30});
}
auto front = GetRowCol(window, GetFront(cursor));
auto back = GetRowCol(window, GetBack(cursor));
ForItem(cell, row.cells) {
// Draw selection
if (selecting) {
if (row.line >= min_line.number && row.line <= max_line.number) {
if (cell.pos >= cursor.range.min && cell.pos < cursor.range.max) {
DrawRectangleRec(ToRectangle(cell.rect), BLUE);
}
}
}
// Draw cursors
if (cell.pos == GetFront(cursor)) {
Rect2 rect = cell.rect;
rect = CutLeft(&rect, 4);
DrawRectangleRec(ToRectangle(rect), RED);
}
if (cell.pos == GetBack(cursor)) {
Rect2 rect = cell.rect;
rect = CutLeft(&rect, 2);
DrawRectangleRec(ToRectangle(rect), GREEN);
}
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();
// Draw the line numbers
{
Rectangle bar_rectangle = ToRectangle(line_number_bar_in_render_units);
BeginScissorMode((int)bar_rectangle.x, (int)bar_rectangle.y, (int)bar_rectangle.width, (int)bar_rectangle.height);
Vec2 text_position_in_render_units = line_number_bar_in_render_units.min;
For(rows) {
Assert(it.cells.len);
text_position_in_render_units.y = it.cells[0].rect.min.y;
String string_num = Format(FrameArena, "%lld", (long long)it.line);
DrawString(font, string_num, text_position_in_render_units, font_size, font_spacing, BLACK);
}
EndScissorMode();
}
}
EndDrawing();

View File

@@ -71,3 +71,24 @@ Rect2 CutTop(Rect2 *r, float value) {
};
return result;
}
Rect2 operator-(Rect2 r, float value) {
r.min.x -= value;
r.min.y -= value;
r.max.x -= value;
r.max.y -= value;
return r;
}
Rect2 operator-(Rect2 r, Vec2 value) {
r.min.x -= value.x;
r.min.y -= value.y;
r.max.x -= value.x;
r.max.y -= value.y;
return r;
}
Rect2 operator-=(Rect2 &r, Vec2 value) {
r = r - value;
return r;
}