// TODO: fix j being outside of the box SOME FUCKING HOW #include "UI.h" #include "../resources/resources.h" #include #include #include #include void HighlightCharacter(Vector2 lineStartPos, char *line, int index, Font *font, int height, int spacing) { float x_position = 0; for (int i = 0; i < index; i++) { x_position += MeasureChar(font, height, line[i]); x_position += spacing; } DrawLine(lineStartPos.x + x_position, lineStartPos.y, lineStartPos.x + x_position, lineStartPos.y + height, COLORSCHEME_BROWN); } TextNode *CreateNode(TextNode *prev, TextNode *next) { TextNode *node = malloc(sizeof(TextNode)); if (node == NULL) return NULL; node->NextNode = next; node->PrevNode = prev; node->len = 0; node->text = calloc(DEFAULT_TEXT_NODE_SIZE, sizeof(char)); if (node->text != NULL) node->cap = DEFAULT_TEXT_NODE_SIZE; TraceLog(LOG_INFO, "Created a new node"); return node; } void FreeNode(TextNode *node) { if (node == NULL) return; free(node->text); free(node); TraceLog(LOG_INFO, "Deleted a node"); } /// Recursively frees the nodes void FreeNodeRec(TextNode *node) { if (node == NULL) return; FreeNodeRec(node->NextNode); free(node->text); free(node); } bool AddSpace(TextNode *base) { TraceLog(LOG_INFO, "Adding space to a node"); if (base == NULL) return false; char *new = realloc(base->text, base->cap + DEFAULT_TEXT_NODE_SIZE); if (new == NULL) return false; base->text = new; memset(new + base->cap, 0, DEFAULT_TEXT_NODE_SIZE); base->cap += DEFAULT_TEXT_NODE_SIZE; return true; } bool AddSpaceDetermined(TextNode *base, size_t additional_space) { TraceLog(LOG_INFO, "Adding space to a node"); if (base == NULL) return false; char *new = realloc(base->text, base->cap + additional_space); if (new == NULL) return false; base->text = new; memset(new + base->cap, 0, additional_space); base->cap += additional_space; return true; } void AppendTextToNode(TextNode *base, char *new_text) { if (base == NULL || new_text == NULL) return; size_t len = strlen(base->text); /// >= and not > because it needs a \0 if (len + strlen(new_text) >= base->cap) { if (!AddSpace(base)) return; } strcpy(base->text + len, new_text); } TextArea *CreateTextArea() { TextArea *outp = malloc(sizeof(TextArea)); if (outp == NULL) return NULL; TextNode *start_node = CreateNode(NULL, NULL); if (start_node == NULL) { free(outp); return NULL; } outp->StartNode = start_node; outp->CurrentNode = start_node; outp->NodeLenth = 1; return outp; } bool PushNode(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return false; TextNode *new_node = CreateNode(ta->CurrentNode, ta->CurrentNode->NextNode); if (new_node == NULL) return false; if (ta->CurrentNode->NextNode != NULL) ((TextNode *)ta->CurrentNode->NextNode)->PrevNode = new_node; ta->CurrentNode->NextNode = new_node; ta->NodeLenth += 1; return true; } void PushNodeBefore(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; TextNode *new_node = CreateNode(ta->CurrentNode->PrevNode, ta->CurrentNode); if (new_node == NULL) return; if (ta->CurrentNode->PrevNode != NULL) ((TextNode *)ta->CurrentNode->PrevNode)->NextNode = new_node; if (ta->CurrentNode == ta->StartNode) ta->StartNode = new_node; ta->CurrentNode->PrevNode = new_node; ta->NodeLenth += 1; } void PopCurrent(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL || ta->NodeLenth <= 1) return; TextNode *current = ta->CurrentNode; if (current->PrevNode != NULL) ((TextNode *)current->PrevNode)->NextNode = current->NextNode; if (current->NextNode != NULL) ((TextNode *)current->NextNode)->PrevNode = current->PrevNode; /// One of those is guaranteed not to be null (hopefully) if (current->PrevNode != NULL) ta->CurrentNode = current->PrevNode; else ta->CurrentNode = current->NextNode; ta->NodeLenth -= 1; FreeNode(current); } void FreeTextArea(TextArea *ta) { FreeNodeRec(ta->StartNode); free(ta); } /// Returns a char the cursor is at char GetCurrentChar(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return 0x0; return ta->CurrentNode->text[(int)ta->CursorPosition.x]; } /// This function is used in the skips through words () bool IsSkippable(char c) { return isalpha(c) || isdigit(c); } void MoveCursorRight(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; ta->CursorPosition.x += 1; size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void MoveCursorRightWord(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; do { ta->CursorPosition.x += 1; } while (IsSkippable(GetCurrentChar(ta))); size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void MoveCursorEnd(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; ta->CursorPosition.x = ta->CurrentNode->len; } void MoveCursorLeft(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; ta->CursorPosition.x -= 1; if (ta->CursorPosition.x < 0) ta->CursorPosition.x = 0; } void MoveCursorLeftWord(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; do { ta->CursorPosition.x -= 1; } while (IsSkippable(GetCurrentChar(ta)) && ta->CursorPosition.x > 0); size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void MoveCursorHome(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; ta->CursorPosition.x = 0; } void MoveCursorDown(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; if (ta->CurrentNode->NextNode == NULL) return; ta->CurrentNode = ta->CurrentNode->NextNode; ta->CursorPosition.y += 1; size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void MoveCursorUp(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; if (ta->CursorPosition.y == 0 || ta->CurrentNode->PrevNode == NULL) return; ta->CurrentNode = ta->CurrentNode->PrevNode; ta->CursorPosition.y -= 1; size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void BreakLine(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; if (ta->CursorPosition.x == 0) { PushNodeBefore(ta); ta->CursorPosition.y += 1; return; } if (!PushNode(ta)) { // Something went wrong, silently undermine TraceLog(LOG_ERROR, "Failed to push a node: the initializator returned NULL"); return; } size_t current_node_len = strlen(ta->CurrentNode->text); if (ta->CursorPosition.x < current_node_len) { char *after_str = (ta->CurrentNode->text + (int)ta->CursorPosition.x); size_t after_str_len = strlen(after_str); if (((TextNode *)ta->CurrentNode->NextNode)->cap < after_str_len) { if (!AddSpaceDetermined(ta->CurrentNode->NextNode, after_str_len)) { TraceLog(LOG_ERROR, "Failed to push a node: the space reservaton failed"); } } strcpy(((TextNode *)ta->CurrentNode->NextNode)->text, after_str); ((TextNode *)ta->CurrentNode->NextNode)->len = after_str_len; for (int i = ta->CursorPosition.x; i < current_node_len; i++) { ta->CurrentNode->text[i] = 0x0; } ta->CurrentNode->len -= after_str_len; } ta->CursorPosition.y += 1; ta->CursorPosition.x = 0; ta->CurrentNode = ta->CurrentNode->NextNode; } void InsertChar(TextArea *ta, char new_char) { if (ta == NULL || ta->CurrentNode == NULL) return; size_t line_len = ta->CurrentNode->len; if (line_len + 2 > ta->CurrentNode->cap) { /// The reallocation fails if (!AddSpace(ta->CurrentNode)) return; } if (ta->CursorPosition.x < line_len) { // TraceLog(LOG_INFO, "memcpy"); memmove(ta->CurrentNode->text + (int)ta->CursorPosition.x + 1, ta->CurrentNode->text + (int)ta->CursorPosition.x, line_len - (int)ta->CursorPosition.x + 1); } else { (ta->CurrentNode->text)[(int)ta->CursorPosition.x+1] = 0x0; } (ta->CurrentNode->text)[(int)ta->CursorPosition.x] = new_char; ta->CursorPosition.x += 1; ta->CurrentNode->len += 1; line_len += 1; if (ta->CursorPosition.x > line_len) ta->CursorPosition.x = line_len; } void DeleteChar(TextArea *ta) { // TODO: Split this function in parts if (ta == NULL || ta->CurrentNode == NULL) return; if (ta->CursorPosition.x == 0) { if (ta->CursorPosition.y <= 0 || ta->CurrentNode->PrevNode == NULL) return; int target_x = strlen(((TextNode *)ta->CurrentNode->PrevNode)->text); // The cursor should remove prevoius line break AppendTextToNode(ta->CurrentNode->PrevNode, ta->CurrentNode->text); PopCurrent(ta); /// The cursor is guaranteed to go up because the y coordinate is > 0 ta->CursorPosition.y -= 1; ta->CursorPosition.x = target_x; // ta->CurrentNode->len -= 1; if (ta->CurrentNode->len < 0) ta->CurrentNode->len = 0; return; }; size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x == line_len) { ta->CurrentNode->text[line_len - 1] = 0x0; } else { char *after_str = (ta->CurrentNode->text + (int)ta->CursorPosition.x); strcpy(ta->CurrentNode->text + (int)ta->CursorPosition.x - 1, after_str); } ta->CursorPosition.x -= 1; ta->CurrentNode->len -= 1; if (ta->CurrentNode->len < 0) ta->CurrentNode->len = 0; } bool IsKeyPressedFix(int key) { return IsKeyPressed(key) || IsKeyPressedRepeat(key); } void PrintAllLines(TextArea *ta) { TraceLog(LOG_INFO, "Cursor: %dx%d", (int)ta->CursorPosition.x, (int)ta->CursorPosition.y); TextNode *node = ta->StartNode; while (node != NULL) { TraceLog(LOG_INFO, "%d\t| %s", node->len, node->text); node = node->NextNode; } } /// Assumes that control is pressed void HandleControlComands(TextArea *ta) { if (IsKeyPressedFix(KEY_RIGHT)) MoveCursorRightWord(ta); if (IsKeyPressedFix(KEY_LEFT)) MoveCursorLeftWord(ta); // emacs-like stuff if (IsKeyPressedFix(KEY_F)) MoveCursorRight(ta); if (IsKeyPressedFix(KEY_B)) MoveCursorLeft(ta); if (IsKeyPressedFix(KEY_N)) MoveCursorDown(ta); if (IsKeyPressedFix(KEY_P)) MoveCursorUp(ta); if (IsKeyPressedFix(KEY_A)) MoveCursorHome(ta); if (IsKeyPressedFix(KEY_E)) MoveCursorEnd(ta); if (IsKeyPressedFix(KEY_O)) { BreakLine(ta); MoveCursorUp(ta); MoveCursorEnd(ta); }; } /// Assumes that control is pressed void HandleAltComands(TextArea *ta) { // emacs-like stuff if (IsKeyPressedFix(KEY_F)) MoveCursorRightWord(ta); if (IsKeyPressedFix(KEY_B)) MoveCursorLeftWord(ta); } void HandleKeyboard(TextArea *ta) { if (IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)) { HandleControlComands(ta); return; } if (IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT)) { HandleAltComands(ta); return; } int charKey = GetCharPressed(); while (charKey > 0) { if (charKey >= 32 && charKey <= 125) { InsertChar(ta, (char)charKey); } charKey = GetCharPressed(); } if (IsKeyPressedFix(KEY_BACKSPACE)) DeleteChar(ta); if (IsKeyPressedFix(KEY_ENTER)) BreakLine(ta); if (IsKeyPressedFix(KEY_LEFT)) MoveCursorLeft(ta); if (IsKeyPressedFix(KEY_RIGHT)) MoveCursorRight(ta); if (IsKeyPressedFix(KEY_DOWN)) MoveCursorDown(ta); if (IsKeyPressedFix(KEY_UP)) MoveCursorUp(ta); if (IsKeyPressedFix(KEY_HOME)) MoveCursorHome(ta); if (IsKeyPressedFix(KEY_END)) MoveCursorEnd(ta); if (IsKeyPressedFix(KEY_F3)) PrintAllLines(ta); } /// draws a text area on the screen. Main draw entry point void DrawTextArea(TextArea *ta, Rectangle rec, Font *font, int height, int spacing, int *scroll) { int vertical_acc = rec.y - *scroll; int text_height = ta->NodeLenth * height; int max_scroll = text_height - rec.height; if (text_height >= rec.height && IsMouseInRec(rec)) { *scroll -= GetMouseWheelMove() * SCROLL_SENC; if (*scroll < 0) *scroll = 0; if (*scroll > max_scroll) *scroll = max_scroll; } Vector2 pos = { .x = rec.x + spacing, .y = rec.y - *scroll }; TextNode *node = ta->StartNode; BeginScissorMode(rec.x, rec.y, rec.width, rec.height); while (node != NULL) { DrawTextEx(*font, node->text, pos, height, spacing, COLORSCHEME_BROWN); if (node == ta->CurrentNode) HighlightCharacter(pos, node->text, ta->CursorPosition.x, font, height, spacing); node = node->NextNode; pos.y += height; } EndScissorMode(); }