Command refactor

This commit is contained in:
Krzosa Karol
2024-07-21 17:38:02 +02:00
parent 9554160edc
commit 5d3f1fcf08
5 changed files with 532 additions and 552 deletions

View File

@@ -0,0 +1 @@

View File

@@ -17,8 +17,10 @@
#include "raylib.h"
#include "raylib_utils.cpp"
#include "view.h"
#include "view_commands.cpp"
#include "colors.cpp"
#include "view.cpp"
#include "view_draw.cpp"
/*

View File

@@ -1,551 +0,0 @@
struct View {
Font font;
Int font_size;
Int font_spacing;
Int line_spacing;
Int char_spacing;
bool mouse_selecting;
Vec2I scroll;
Buffer *buffer;
Array<Caret> carets;
Rect2I rect;
};
Rect2I GetVisibleCells(const View &view);
Int MoveOnWhitespaceBoundaryForward(Buffer &buffer, Int pos) {
pos = Clamp(pos, (Int)0, buffer.len);
bool standing_on_whitespace = IsWhitespace(buffer.str[pos]);
bool seek_whitespace = standing_on_whitespace == false;
bool seek_word = standing_on_whitespace;
Int result = buffer.len;
Int prev_pos = pos;
for (Int i = pos; i < buffer.len; i += 1) {
bool whitespace = IsWhitespace(buffer.str[i]);
if (seek_word && !whitespace) {
result = i;
break;
}
if (seek_whitespace && whitespace) {
result = i;
break;
}
prev_pos = i;
}
return result;
}
Int MoveOnWhitespaceBoundaryBackward(Buffer &buffer, Int pos) {
pos = Clamp(pos - 1, (Int)0, buffer.len);
bool standing_on_whitespace = IsWhitespace(buffer.str[pos]);
bool seek_whitespace = standing_on_whitespace == false;
bool seek_word = standing_on_whitespace;
Int result = 0;
Int prev_pos = pos;
for (Int i = pos; i >= 0; i -= 1) {
bool whitespace = IsWhitespace(buffer.str[i]);
if (seek_word && !whitespace) {
result = prev_pos;
break;
}
if (seek_whitespace && whitespace) {
result = prev_pos;
break;
}
prev_pos = i;
}
return result;
}
Int MoveOnWhitespaceBoundaryDown(Buffer &buffer, Int pos) {
Int result = pos;
Int next_line = PosToLine(buffer, pos) + 1;
for (Int line = next_line; line < buffer.line_starts.len; line += 1) {
Range line_range = GetLineRange(buffer, line);
result = line_range.min;
bool whitespace_line = true;
for (Int i = line_range.min; i < line_range.max; i += 1) {
if (!IsWhitespace(buffer.str[i])) {
whitespace_line = false;
break;
}
}
if (whitespace_line) break;
}
return result;
}
Int MoveOnWhitespaceBoundaryUp(Buffer &buffer, Int pos) {
Int result = pos;
Int next_line = PosToLine(buffer, pos) - 1;
for (Int line = next_line; line >= 0; line -= 1) {
Range line_range = GetLineRange(buffer, line);
result = line_range.min;
bool whitespace_line = true;
for (Int i = line_range.min; i < line_range.max; i += 1) {
if (!IsWhitespace(buffer.str[i])) {
whitespace_line = false;
break;
}
}
if (whitespace_line) break;
}
return result;
}
Int MovePosByXY(Buffer &buffer, Int pos, XY offset) {
XY xy = PosToXY(buffer, pos);
Int result = XYToPosWithoutNL(buffer, {xy.col + offset.col, xy.line + offset.line});
return result;
}
Int MoveCaret(Buffer &buffer, Int pos, int direction, bool ctrl_pressed) {
ProfileFunction();
Assert(direction >= 0 && direction <= 3);
if (ctrl_pressed) {
switch (direction) {
case 0: return MoveOnWhitespaceBoundaryForward(buffer, pos);
case 1: return MoveOnWhitespaceBoundaryBackward(buffer, pos);
case 2: return MoveOnWhitespaceBoundaryDown(buffer, pos);
case 3: return MoveOnWhitespaceBoundaryUp(buffer, pos);
default: return pos;
}
} else {
switch (direction) {
case 0: return Clamp(pos + 1, (Int)0, buffer.len);
case 1: return Clamp(pos - 1, (Int)0, buffer.len);
case 2: return MovePosByXY(buffer, pos, {0, 1});
case 3: return MovePosByXY(buffer, pos, {0, -1});
default: return pos;
}
}
}
void AfterEdit(View *view, Array<Edit> edits) {
//
// Offset all cursors by edits
//
Scratch scratch;
Array<Caret> new_cursors = TightCopy(scratch, view->carets);
ForItem(edit, edits) {
Int remove_size = GetSize(edit.range);
Int insert_size = edit.string.len;
Int offset = insert_size - remove_size;
for (Int i = 0; i < view->carets.len; i += 1) {
Caret &old_cursor = view->carets.data[i];
Caret &new_cursor = new_cursors.data[i];
if (old_cursor.range.min == edit.range.min) {
new_cursor.range.min = new_cursor.range.max = new_cursor.range.min + insert_size;
} else if (old_cursor.range.min > edit.range.min) {
new_cursor.range.min = new_cursor.range.max = new_cursor.range.min + offset;
}
}
}
for (Int i = 0; i < view->carets.len; i += 1) view->carets[i] = new_cursors[i];
// Make sure all cursors are in range
For(view->carets) it.range = Clamp(*view->buffer, it.range);
}
void MultiCursorReplace(View *view, String16 string) {
Scratch scratch;
MergeCarets(&view->carets);
Array<Edit> edits = {scratch};
For(view->carets) AddEdit(&edits, it.range, string);
ApplyEdits(view->buffer, edits);
AfterEdit(view, edits);
}
union KeyEncode {
struct {
U32 key;
U8 ctrl : 1;
U8 alt : 1;
U8 shift : 1;
};
U64 u64;
};
typedef void CommandProc(View *view);
Table<CommandProc *> RegisteredBindings;
KeyEncode Key(int key) {
KeyEncode result = {};
result.key = key;
return result;
}
KeyEncode KeyControl(int key) {
KeyEncode result = {};
result.key = key;
result.ctrl = true;
return result;
}
KeyEncode KeyShiftControl(int key) {
KeyEncode result = {};
result.key = key;
result.ctrl = true;
result.shift = true;
return result;
}
KeyEncode KeyControlAlt(int key) {
KeyEncode result = {};
result.key = key;
result.ctrl = true;
result.alt = true;
return result;
}
KeyEncode KeyShiftAlt(int key) {
KeyEncode result = {};
result.key = key;
result.shift = true;
result.alt = true;
return result;
}
void RegisterBinding(CommandProc *proc, KeyEncode key) {
RegisteredBindings.insert(key.u64, proc);
}
void CommandClearCarets(View *view) {
view->carets.len = 1;
}
void HandleKeybindings(View *_view) {
ProfileFunction();
View &view = *_view;
Buffer &buf = *view.buffer;
Caret main_caret_on_begin_frame = view.carets[0];
if (RegisteredBindings.cap == 0) {
RegisterBinding(CommandClearCarets, Key(KEY_ESCAPE));
}
{
KeyEncode key = {};
key.ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
key.alt = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT);
key.shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
int keys[] = {
KEY_APOSTROPHE,
KEY_COMMA,
KEY_MINUS,
KEY_PERIOD,
KEY_SLASH,
KEY_ZERO,
KEY_ONE,
KEY_TWO,
KEY_THREE,
KEY_FOUR,
KEY_FIVE,
KEY_SIX,
KEY_SEVEN,
KEY_EIGHT,
KEY_NINE,
KEY_SEMICOLON,
KEY_EQUAL,
KEY_A,
KEY_B,
KEY_C,
KEY_D,
KEY_E,
KEY_F,
KEY_G,
KEY_H,
KEY_I,
KEY_J,
KEY_K,
KEY_L,
KEY_M,
KEY_N,
KEY_O,
KEY_P,
KEY_Q,
KEY_R,
KEY_S,
KEY_T,
KEY_U,
KEY_V,
KEY_W,
KEY_X,
KEY_Y,
KEY_Z,
KEY_LEFT_BRACKET,
KEY_BACKSLASH,
KEY_RIGHT_BRACKET,
KEY_GRAVE,
KEY_SPACE,
KEY_ESCAPE,
KEY_ENTER,
KEY_TAB,
KEY_BACKSPACE,
KEY_INSERT,
KEY_DELETE,
KEY_RIGHT,
KEY_LEFT,
KEY_DOWN,
KEY_UP,
KEY_PAGE_UP,
KEY_PAGE_DOWN,
KEY_HOME,
KEY_END,
KEY_CAPS_LOCK,
KEY_SCROLL_LOCK,
KEY_NUM_LOCK,
KEY_PRINT_SCREEN,
KEY_PAUSE,
KEY_F1,
KEY_F2,
KEY_F3,
KEY_F4,
KEY_F5,
KEY_F6,
KEY_F7,
KEY_F8,
KEY_F9,
KEY_F10,
KEY_F11,
KEY_F12,
};
for (int i = 0; i < sizeof(keys); i += 1) {
bool press = IsKeyPressed(keys[i]) || IsKeyPressedRepeat(keys[i]);
if (!press) continue;
key.key = keys[i];
CommandProc *proc = RegisteredBindings.get(key.u64, NULL);
if (proc) proc(&view);
}
}
if (IsKeyDown(KEY_F1)) {
view.scroll.x -= (Int)(GetMouseWheelMove() * 48);
} else {
view.scroll.y -= (Int)(GetMouseWheelMove() * 48);
}
if (IsKeyDown(KEY_F2)) {
if (IsKeyDown(KEY_LEFT_CONTROL)) {
LoadBigLine(view.buffer);
} else {
LoadBigText(view.buffer);
}
}
if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressedRepeat(KEY_ESCAPE)) {
view.carets.len = 1;
}
if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyDown(KEY_LEFT_ALT) && (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN))) {
For(view.carets) it = MakeCaret(GetFront(it));
MergeCarets(&view.carets);
Scratch scratch;
Array<Edit> edits = {scratch};
For(view.carets) {
Int line = PosToLine(buf, it.range.min);
Range range = GetLineRange(buf, line);
String16 string = Copy(scratch, GetString(buf, range));
AddEdit(&edits, {range.max, range.max}, string);
}
ApplyEdits(&buf, edits);
AfterEdit(&view, edits);
For(view.carets) it = MakeCaret(MoveCaret(buf, it.range.min, 2, false));
} else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyDown(KEY_LEFT_ALT) && (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP))) {
For(view.carets) it = MakeCaret(GetFront(it));
MergeCarets(&view.carets);
Scratch scratch;
Array<Edit> edits = {scratch};
For(view.carets) {
Int line = PosToLine(buf, it.range.min);
Range range = GetLineRange(buf, line);
String16 string = Copy(scratch, GetString(buf, range));
AddEdit(&edits, {range.min, range.min}, string);
}
ApplyEdits(&buf, edits);
AfterEdit(&view, edits);
For(view.carets) it = MakeCaret(MoveCaret(buf, it.range.min, 3, false));
} else {
int keys[4] = {KEY_RIGHT, KEY_LEFT, KEY_DOWN, KEY_UP};
for (int i = 0; i < 4; i += 1) {
if (IsKeyPressed(keys[i]) || IsKeyPressedRepeat(keys[i])) {
For(view.carets) {
if (IsKeyDown(KEY_LEFT_SHIFT)) {
Int front = GetFront(it);
Int new_front = MoveCaret(buf, front, i, IsKeyDown(KEY_LEFT_CONTROL));
it = ChangeFront(it, new_front);
} else {
Int p = keys[i] == KEY_RIGHT || keys[i] == KEY_DOWN ? it.range.max : it.range.min;
if (GetSize(it.range) == 0) {
it = MakeCaret(MoveCaret(buf, p, i, IsKeyDown(KEY_LEFT_CONTROL)));
} else {
it = MakeCaret(p);
}
}
}
}
}
}
int key_page_down = IsKeyPressed(KEY_PAGE_DOWN) || IsKeyPressedRepeat(KEY_PAGE_DOWN);
int key_page_up = IsKeyPressed(KEY_PAGE_UP) || IsKeyPressedRepeat(KEY_PAGE_UP);
if (key_page_up || key_page_down) {
Rect2I visible_cells_rect = GetVisibleCells(view);
Int y = GetSize(visible_cells_rect).y - 2;
if (key_page_up) y = -y;
For(view.carets) {
XY xy = PosToXY(buf, GetFront(it));
xy.line += y;
Int pos = XYToPos(buf, xy);
if (IsKeyDown(KEY_LEFT_SHIFT)) {
it = ChangeFront(it, pos);
} else {
it = MakeCaret(pos);
}
}
}
{
int keys[2] = {KEY_HOME, KEY_END};
for (Int i = 0; i < 2; i += 1) {
if (IsKeyPressed(keys[i]) || IsKeyPressedRepeat(keys[i])) {
For(view.carets) {
Int end_of_buffer = 0;
Range line_range = GetLineRange(buf, PosToLine(buf, GetFront(it)), &end_of_buffer);
Int diff = keys[i] == KEY_END ? i - end_of_buffer : 0;
if (IsKeyDown(KEY_LEFT_SHIFT)) {
it = ChangeFront(it, line_range.e[i] - diff);
} else {
it.range.max = it.range.min = line_range.e[i] - diff;
}
}
}
}
}
if (IsKeyPressed(KEY_ENTER) || IsKeyPressedRepeat(KEY_ENTER)) {
MultiCursorReplace(&view, L"\n");
}
{
int keys[] = {KEY_DELETE, KEY_BACKSPACE};
for (int i = 0; i < 2; i += 1) {
if (IsKeyPressed(keys[i]) || IsKeyPressedRepeat(keys[i])) {
// Select things to delete
For(view.carets) {
if (GetSize(it.range)) continue;
Int pos = MoveCaret(buf, it.range.min, i, IsKeyDown(KEY_LEFT_CONTROL));
it = MakeCaret(pos, it.range.min);
}
MultiCursorReplace(&view, {});
}
}
}
for (int c = GetCharPressed(); c; c = GetCharPressed()) {
// we interpret 2 byte sequences as 1 byte when rendering but we still
// want to read them properly.
String16 string = L"?";
UTF16Result result = UTF32ToUTF16((uint32_t)c);
if (!result.error) string = {(wchar_t *)result.out_str, result.len};
MultiCursorReplace(&view, string);
}
{
ProfileScope(mouse);
Vec2 _mouse = GetMousePosition();
bool mouse_in_view = CheckCollisionPointRec(_mouse, ToRectangle(view.rect));
Vec2I mouse = ToVec2I(_mouse);
if (!view.mouse_selecting) {
if (mouse_in_view) {
SetMouseCursor(MOUSE_CURSOR_IBEAM);
} else {
SetMouseCursor(MOUSE_CURSOR_DEFAULT);
}
}
Vec2I mworld = mouse - view.rect.min + view.scroll;
Vec2I pos = mworld / Vec2I{view.char_spacing, view.line_spacing};
XY xy = {(Int)(pos.x), (Int)(pos.y)};
Int p = XYToPosWithoutNL(buf, xy);
if (mouse_in_view && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (!IsKeyDown(KEY_LEFT_CONTROL)) {
view.carets.len = 0;
}
Add(&view.carets, MakeCaret(p, p));
} else if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
view.mouse_selecting = true;
}
}
if (view.mouse_selecting) {
if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT)) view.mouse_selecting = false;
Caret &caret = *GetLast(view.carets);
caret = ChangeFront(caret, p);
MergeCarets(&view.carets);
}
}
// Scrolling with caret
if (!AreEqual(main_caret_on_begin_frame, view.carets[0])) {
Caret c = view.carets[0];
Int front = GetFront(c);
XY xy = PosToXY(buf, front);
Rect2I visible = GetVisibleCells(view);
Vec2I visible_cells = GetSize(visible);
Vec2I visible_size = visible_cells * Vec2I{view.char_spacing, view.line_spacing};
Vec2I rect_size = GetSize(view.rect);
if (xy.line > visible.max.y - 2) {
Int set_view_at_line = xy.line - (visible_cells.y - 1);
Int cut_off_y = Max((Int)0, visible_size.y - rect_size.y);
view.scroll.y = (set_view_at_line * view.line_spacing) + cut_off_y;
}
if (xy.line < visible.min.y + 1) {
view.scroll.y = xy.line * view.line_spacing;
}
if (xy.col >= visible.max.x - 1) {
Int set_view_at_line = xy.col - (visible_cells.x - 1);
Int cut_off_x = Max((Int)0, visible_size.x - rect_size.x);
view.scroll.x = (set_view_at_line * view.char_spacing) + cut_off_x;
}
if (xy.col <= visible.min.x) {
view.scroll.x = xy.col * view.char_spacing;
}
}
// Clip scroll
{
ProfileScope(clip_scroll);
Int last_line = LastLine(view.buffer[0]);
view.scroll.y = Clamp(view.scroll.y, (Int)0, Max((Int)0, (last_line - 1) * view.line_spacing));
// @note:
// GetCharCountOfLongestLine is a bottleneck, there is probably an algorithm for
// calculating this value incrementally but do we even need X scrollbar or x clipping?
view.scroll.x = ClampBottom(view.scroll.x, (Int)0);
}
}

17
src/text_editor/view.h Normal file
View File

@@ -0,0 +1,17 @@
struct View {
Font font;
Int font_size;
Int font_spacing;
Int line_spacing;
Int char_spacing;
bool mouse_selecting;
Vec2I scroll;
Buffer *buffer;
Array<Caret> carets;
Rect2I rect;
};
Rect2I GetVisibleCells(const View &view);
void AfterEdit(View *view, Array<Edit> edits);

View File

@@ -0,0 +1,511 @@
Int MoveOnWhitespaceBoundaryForward(Buffer &buffer, Int pos) {
pos = Clamp(pos, (Int)0, buffer.len);
bool standing_on_whitespace = IsWhitespace(buffer.str[pos]);
bool seek_whitespace = standing_on_whitespace == false;
bool seek_word = standing_on_whitespace;
Int result = buffer.len;
Int prev_pos = pos;
for (Int i = pos; i < buffer.len; i += 1) {
bool whitespace = IsWhitespace(buffer.str[i]);
if (seek_word && !whitespace) {
result = i;
break;
}
if (seek_whitespace && whitespace) {
result = i;
break;
}
prev_pos = i;
}
return result;
}
Int MoveOnWhitespaceBoundaryBackward(Buffer &buffer, Int pos) {
pos = Clamp(pos - 1, (Int)0, buffer.len);
bool standing_on_whitespace = IsWhitespace(buffer.str[pos]);
bool seek_whitespace = standing_on_whitespace == false;
bool seek_word = standing_on_whitespace;
Int result = 0;
Int prev_pos = pos;
for (Int i = pos; i >= 0; i -= 1) {
bool whitespace = IsWhitespace(buffer.str[i]);
if (seek_word && !whitespace) {
result = prev_pos;
break;
}
if (seek_whitespace && whitespace) {
result = prev_pos;
break;
}
prev_pos = i;
}
return result;
}
Int MoveOnWhitespaceBoundaryDown(Buffer &buffer, Int pos) {
Int result = pos;
Int next_line = PosToLine(buffer, pos) + 1;
for (Int line = next_line; line < buffer.line_starts.len; line += 1) {
Range line_range = GetLineRange(buffer, line);
result = line_range.min;
bool whitespace_line = true;
for (Int i = line_range.min; i < line_range.max; i += 1) {
if (!IsWhitespace(buffer.str[i])) {
whitespace_line = false;
break;
}
}
if (whitespace_line) break;
}
return result;
}
Int MoveOnWhitespaceBoundaryUp(Buffer &buffer, Int pos) {
Int result = pos;
Int next_line = PosToLine(buffer, pos) - 1;
for (Int line = next_line; line >= 0; line -= 1) {
Range line_range = GetLineRange(buffer, line);
result = line_range.min;
bool whitespace_line = true;
for (Int i = line_range.min; i < line_range.max; i += 1) {
if (!IsWhitespace(buffer.str[i])) {
whitespace_line = false;
break;
}
}
if (whitespace_line) break;
}
return result;
}
Int MovePosByXY(Buffer &buffer, Int pos, XY offset) {
XY xy = PosToXY(buffer, pos);
Int result = XYToPosWithoutNL(buffer, {xy.col + offset.col, xy.line + offset.line});
return result;
}
const int DIR_RIGHT = 0;
const int DIR_LEFT = 1;
const int DIR_DOWN = 2;
const int DIR_UP = 3;
Int MovePos(Buffer &buffer, Int pos, int direction, bool ctrl_pressed) {
ProfileFunction();
Assert(direction >= 0 && direction <= 3);
if (ctrl_pressed) {
switch (direction) {
case DIR_RIGHT: return MoveOnWhitespaceBoundaryForward(buffer, pos);
case DIR_LEFT: return MoveOnWhitespaceBoundaryBackward(buffer, pos);
case DIR_DOWN: return MoveOnWhitespaceBoundaryDown(buffer, pos);
case DIR_UP: return MoveOnWhitespaceBoundaryUp(buffer, pos);
default: return pos;
}
} else {
switch (direction) {
case DIR_RIGHT: return Clamp(pos + 1, (Int)0, buffer.len);
case DIR_LEFT: return Clamp(pos - 1, (Int)0, buffer.len);
case DIR_DOWN: return MovePosByXY(buffer, pos, {0, 1});
case DIR_UP: return MovePosByXY(buffer, pos, {0, -1});
default: return pos;
}
}
}
void AfterEdit(View *view, Array<Edit> edits) {
//
// Offset all cursors by edits
//
Scratch scratch;
Array<Caret> new_cursors = TightCopy(scratch, view->carets);
ForItem(edit, edits) {
Int remove_size = GetSize(edit.range);
Int insert_size = edit.string.len;
Int offset = insert_size - remove_size;
for (Int i = 0; i < view->carets.len; i += 1) {
Caret &old_cursor = view->carets.data[i];
Caret &new_cursor = new_cursors.data[i];
if (old_cursor.range.min == edit.range.min) {
new_cursor.range.min = new_cursor.range.max = new_cursor.range.min + insert_size;
} else if (old_cursor.range.min > edit.range.min) {
new_cursor.range.min = new_cursor.range.max = new_cursor.range.min + offset;
}
}
}
for (Int i = 0; i < view->carets.len; i += 1) view->carets[i] = new_cursors[i];
// Make sure all cursors are in range
For(view->carets) it.range = Clamp(*view->buffer, it.range);
}
void Command_Replace(View *view, String16 string) {
Scratch scratch;
MergeCarets(&view->carets);
Array<Edit> edits = {scratch};
For(view->carets) AddEdit(&edits, it.range, string);
ApplyEdits(view->buffer, edits);
AfterEdit(view, edits);
}
void Command_DuplicateLine(View *view, int direction) {
Assert(direction == DIR_UP || direction == DIR_DOWN);
For(view->carets) it = MakeCaret(GetFront(it));
MergeCarets(&view->carets);
Scratch scratch;
Array<Edit> edits = {scratch};
For(view->carets) {
Int line = PosToLine(*view->buffer, it.range.min);
Range range = GetLineRange(*view->buffer, line);
String16 string = Copy(scratch, GetString(*view->buffer, range));
Int pos = direction == DIR_UP ? range.min : range.max;
AddEdit(&edits, Rng(pos), string);
}
ApplyEdits(view->buffer, edits);
AfterEdit(view, edits);
For(view->carets) it = MakeCaret(MovePos(*view->buffer, it.range.min, direction, false));
}
bool SHIFT_PRESSED = true;
void Command_MoveCursorsByPageSize(View *_view, int direction, bool shift = false) {
Assert(direction == DIR_UP || direction == DIR_DOWN);
View &view = *_view;
Buffer &buf = *view.buffer;
Rect2I visible_cells_rect = GetVisibleCells(view);
Int y = GetSize(visible_cells_rect).y - 2;
if (direction == DIR_UP) y = -y;
For(view.carets) {
XY xy = PosToXY(buf, GetFront(it));
xy.line += y;
Int pos = XYToPos(buf, xy);
if (shift) {
it = ChangeFront(it, pos);
} else {
it = MakeCaret(pos);
}
}
}
void Command_MoveCursorsToSide(View *_view, int direction, bool shift = false) {
Assert(direction == DIR_LEFT || direction == DIR_RIGHT);
View &view = *_view;
Buffer &buf = *view.buffer;
For(view.carets) {
Int end_of_buffer = 0;
Range line_range = GetLineRange(buf, PosToLine(buf, GetFront(it)), &end_of_buffer);
Int pos = line_range.min;
if (direction == DIR_RIGHT) {
pos = line_range.max - (1 - end_of_buffer);
}
if (shift) {
it = ChangeFront(it, pos);
} else {
it.range.max = it.range.min = pos;
}
}
}
bool CTRL_PRESSED = true;
void Command_Delete(View *_view, int direction, bool ctrl = false) {
Assert(direction == DIR_LEFT || direction == DIR_RIGHT);
View &view = *_view;
Buffer &buf = *view.buffer;
// Select things to delete
For(view.carets) {
if (GetSize(it.range)) continue;
Int pos = MovePos(buf, it.range.min, direction, ctrl);
it = MakeCaret(pos, it.range.min);
}
Command_Replace(&view, {});
}
inline bool Press(int key) {
bool result = IsKeyPressed(key) || IsKeyPressedRepeat(key);
return result;
}
inline bool Shift() {
bool result = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
return result;
}
inline bool Ctrl() {
bool result = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
return result;
}
inline bool Alt() {
bool result = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT);
return result;
}
inline bool CtrlPress(int key) {
bool result = Press(key) && Ctrl();
return result;
}
inline bool ShiftPress(int key) {
bool result = Press(key) && Shift();
return result;
}
inline bool CtrlShiftPress(int key) {
bool result = Press(key) && Shift() && Ctrl();
return result;
}
inline bool CtrlAltPress(int key) {
bool result = Press(key) && Ctrl() && Alt();
return result;
}
void HandleKeybindings(View *_view) {
ProfileFunction();
View &view = *_view;
Buffer &buf = *view.buffer;
Caret main_caret_on_begin_frame = view.carets[0];
if (IsKeyDown(KEY_F1)) {
view.scroll.x -= (Int)(GetMouseWheelMove() * 48);
} else {
view.scroll.y -= (Int)(GetMouseWheelMove() * 48);
}
if (CtrlPress(KEY_F2)) {
LoadBigLine(view.buffer);
} else if (Press(KEY_F2)) {
LoadBigText(view.buffer);
}
if (Press(KEY_ESCAPE)) {
view.carets.len = 1;
}
if (CtrlAltPress(KEY_DOWN)) {
Command_DuplicateLine(&view, DIR_DOWN);
} else if (CtrlShiftPress(KEY_DOWN)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_DOWN, true));
} else if (CtrlPress(KEY_DOWN)) {
For(view.carets) it = MakeCaret(MovePos(buf, it.range.max, DIR_DOWN, true));
} else if (ShiftPress(KEY_DOWN)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_DOWN, false));
} else if (Press(KEY_DOWN)) {
For(view.carets) {
if (GetSize(it.range) == 0) {
it = MakeCaret(MovePos(buf, it.range.max, DIR_DOWN, false));
} else {
it = MakeCaret(it.range.max);
}
}
}
if (CtrlAltPress(KEY_UP)) {
Command_DuplicateLine(&view, DIR_UP);
} else if (CtrlShiftPress(KEY_UP)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_UP, true));
} else if (CtrlPress(KEY_UP)) {
For(view.carets) it = MakeCaret(MovePos(buf, it.range.min, DIR_UP, true));
} else if (ShiftPress(KEY_UP)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_UP, false));
} else if (Press(KEY_UP)) {
For(view.carets) {
if (GetSize(it.range) == 0) {
it = MakeCaret(MovePos(buf, it.range.min, DIR_UP, false));
} else {
it = MakeCaret(it.range.min);
}
}
}
if (CtrlShiftPress(KEY_LEFT)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_LEFT, true));
} else if (CtrlPress(KEY_LEFT)) {
For(view.carets) {
if (GetSize(it.range) != 0 && GetFront(it) != it.range.min) {
it = MakeCaret(it.range.min);
} else {
it = MakeCaret(MovePos(buf, it.range.min, DIR_LEFT, true));
}
}
} else if (ShiftPress(KEY_LEFT)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_LEFT, false));
} else if (Press(KEY_LEFT)) {
For(view.carets) {
if (GetSize(it.range) == 0) {
it = MakeCaret(MovePos(buf, it.range.min, DIR_LEFT, false));
} else {
it = MakeCaret(it.range.min);
}
}
}
if (CtrlShiftPress(KEY_RIGHT)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_RIGHT, true));
} else if (CtrlPress(KEY_RIGHT)) {
For(view.carets) {
if (GetSize(it.range) != 0 && GetFront(it) != it.range.max) {
it = MakeCaret(it.range.max);
} else {
it = MakeCaret(MovePos(buf, it.range.max, DIR_RIGHT, true));
}
}
} else if (ShiftPress(KEY_RIGHT)) {
For(view.carets) it = ChangeFront(it, MovePos(buf, GetFront(it), DIR_RIGHT, false));
} else if (Press(KEY_RIGHT)) {
For(view.carets) {
if (GetSize(it.range) == 0) {
it = MakeCaret(MovePos(buf, it.range.max, DIR_RIGHT, false));
} else {
it = MakeCaret(it.range.max);
}
}
}
if (ShiftPress(KEY_PAGE_UP)) {
Command_MoveCursorsByPageSize(&view, DIR_UP, SHIFT_PRESSED);
} else if (Press(KEY_PAGE_UP)) {
Command_MoveCursorsByPageSize(&view, DIR_UP);
}
if (ShiftPress(KEY_PAGE_DOWN)) {
Command_MoveCursorsByPageSize(&view, DIR_DOWN, SHIFT_PRESSED);
} else if (Press(KEY_PAGE_DOWN)) {
Command_MoveCursorsByPageSize(&view, DIR_DOWN);
}
if (ShiftPress(KEY_HOME)) {
Command_MoveCursorsToSide(&view, DIR_LEFT, SHIFT_PRESSED);
} else if (Press(KEY_HOME)) {
Command_MoveCursorsToSide(&view, DIR_LEFT);
}
if (ShiftPress(KEY_END)) {
Command_MoveCursorsToSide(&view, DIR_RIGHT, SHIFT_PRESSED);
} else if (Press(KEY_END)) {
Command_MoveCursorsToSide(&view, DIR_RIGHT);
}
if (Press(KEY_ENTER)) {
Command_Replace(&view, L"\n");
}
if (Press(KEY_TAB)) {
Command_Replace(&view, L" ");
}
if (CtrlPress(KEY_BACKSPACE)) {
Command_Delete(&view, DIR_LEFT, CTRL_PRESSED);
} else if (Press(KEY_BACKSPACE)) {
Command_Delete(&view, DIR_LEFT);
}
if (CtrlPress(KEY_DELETE)) {
Command_Delete(&view, DIR_RIGHT, CTRL_PRESSED);
} else if (Press(KEY_DELETE)) {
Command_Delete(&view, DIR_RIGHT);
}
for (int c = GetCharPressed(); c; c = GetCharPressed()) {
// we interpret 2 byte sequences as 1 byte when rendering but we still
// want to read them properly.
String16 string = L"?";
UTF16Result result = UTF32ToUTF16((uint32_t)c);
if (!result.error) string = {(wchar_t *)result.out_str, result.len};
Command_Replace(&view, string);
}
{
ProfileScope(mouse);
Vec2 _mouse = GetMousePosition();
bool mouse_in_view = CheckCollisionPointRec(_mouse, ToRectangle(view.rect));
Vec2I mouse = ToVec2I(_mouse);
if (!view.mouse_selecting) {
if (mouse_in_view) {
SetMouseCursor(MOUSE_CURSOR_IBEAM);
} else {
SetMouseCursor(MOUSE_CURSOR_DEFAULT);
}
}
Vec2I mworld = mouse - view.rect.min + view.scroll;
Vec2I pos = mworld / Vec2I{view.char_spacing, view.line_spacing};
XY xy = {(Int)(pos.x), (Int)(pos.y)};
Int p = XYToPosWithoutNL(buf, xy);
if (mouse_in_view && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (!IsKeyDown(KEY_LEFT_CONTROL)) {
view.carets.len = 0;
}
Add(&view.carets, MakeCaret(p, p));
} else if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
view.mouse_selecting = true;
}
}
if (view.mouse_selecting) {
if (!IsMouseButtonDown(MOUSE_BUTTON_LEFT)) view.mouse_selecting = false;
Caret &caret = *GetLast(view.carets);
caret = ChangeFront(caret, p);
MergeCarets(&view.carets);
}
}
// Scrolling with caret
if (!AreEqual(main_caret_on_begin_frame, view.carets[0])) {
Caret c = view.carets[0];
Int front = GetFront(c);
XY xy = PosToXY(buf, front);
Rect2I visible = GetVisibleCells(view);
Vec2I visible_cells = GetSize(visible);
Vec2I visible_size = visible_cells * Vec2I{view.char_spacing, view.line_spacing};
Vec2I rect_size = GetSize(view.rect);
if (xy.line > visible.max.y - 2) {
Int set_view_at_line = xy.line - (visible_cells.y - 1);
Int cut_off_y = Max((Int)0, visible_size.y - rect_size.y);
view.scroll.y = (set_view_at_line * view.line_spacing) + cut_off_y;
}
if (xy.line < visible.min.y + 1) {
view.scroll.y = xy.line * view.line_spacing;
}
if (xy.col >= visible.max.x - 1) {
Int set_view_at_line = xy.col - (visible_cells.x - 1);
Int cut_off_x = Max((Int)0, visible_size.x - rect_size.x);
view.scroll.x = (set_view_at_line * view.char_spacing) + cut_off_x;
}
if (xy.col <= visible.min.x) {
view.scroll.x = xy.col * view.char_spacing;
}
}
// Clip scroll
{
ProfileScope(clip_scroll);
Int last_line = LastLine(view.buffer[0]);
view.scroll.y = Clamp(view.scroll.y, (Int)0, Max((Int)0, (last_line - 1) * view.line_spacing));
// @note:
// GetCharCountOfLongestLine is a bottleneck, there is probably an algorithm for
// calculating this value incrementally but do we even need X scrollbar or x clipping?
view.scroll.x = ClampBottom(view.scroll.x, (Int)0);
}
}