// TODO: fix j being outside of the box SOME FUCKING HOW // upd. seems to be fixed with a swirch to monofont #include "UI.h" #include "../resources/resources.h" #include #include #include #include #include float GetCursorXPos(TextArea *ta) { int index = ta->CursorPosition.x; char *line = ta->CurrentNode->text; float x_position = 0; for (int i = 0; i < index; i++) { x_position += MeasureChar(ta->DisplayParams->font, ta->DisplayParams->font_size, line[i]); x_position += ta->DisplayParams->spacing; } return x_position; } /// We pass x_position not to call GetCursorXPos twice (it's expencive) void DrawCursor(TextArea *ta, Vector2 lineStartPos, float x_position) { DrawLine(lineStartPos.x + x_position, lineStartPos.y, lineStartPos.x + x_position, lineStartPos.y + ta->DisplayParams->font_size, 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); size_t new_len = strlen(new_text); /// >= and not > because it needs a \0 if (len + new_len >= base->cap) { if (!AddSpace(base)) return; } strcpy(base->text + len, new_text); base->len += new_len; } TextArea *CreateTextArea(TextAreaDisplayParams *DisplayParams) { 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->CursorPosition = (Vector2){ 0, 0 }; outp->CurrentNode = start_node; outp->NodeLenth = 1; outp->DisplayParams = DisplayParams; return outp; } void EnsureCursorIsInFrame(TextArea *ta) { TextAreaDisplayParams *params = ta->DisplayParams; Vector2 cursorPos = { .x = GetCursorXPos(ta), .y = ta->CursorPosition.y * ta->DisplayParams->font_size, }; if (cursorPos.y < params->scroll_v) { params->scroll_v = cursorPos.y; } else if (params->scroll_v + params->_area_height - params->font_size < cursorPos.y) { params->scroll_v = cursorPos.y + params->_area_height; } if (cursorPos.x < params->scroll_h) { params->scroll_h = cursorPos.x - (params->_area_width / 2.0f); if (params->scroll_h < 0) params->scroll_h = 0; } else if (params->scroll_h + params->_area_width - params->font_size < cursorPos.x) { params->scroll_h = cursorPos.x - (params->_area_width / 2.0f); } } 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); if (ta->DisplayParams != NULL) free(ta->DisplayParams); 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]; } 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; } /// 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) { if (ta->CurrentNode->NextNode == NULL) { ta->CursorPosition.x = line_len; return; } MoveCursorDown(ta); ta->CursorPosition.x = 0; } } void MoveCursorRightWord(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; size_t line_len = ta->CurrentNode->len; if (ta->CursorPosition.x == line_len) { MoveCursorRight(ta); return; } bool current_type = IsSkippable(GetCurrentChar(ta)); do { ta->CursorPosition.x += 1; } while (IsSkippable(GetCurrentChar(ta)) == current_type); 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) { if (ta->CursorPosition.y == 0 || ta->CurrentNode->PrevNode == NULL) { ta->CursorPosition.x = 0; return; } MoveCursorUp(ta); MoveCursorEnd(ta); } } void MoveCursorLeftWord(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; if (ta->CursorPosition.x == 0) { MoveCursorLeft(ta); return; } char prev_symbol = ta->CurrentNode->text[(int)ta->CursorPosition.x - 1]; bool current_type = IsSkippable(prev_symbol); do { ta->CursorPosition.x -= 1; } while (IsSkippable(GetCurrentChar(ta)) == current_type && ta->CursorPosition.x >= 0); ta->CursorPosition.x += 1; if (ta->CursorPosition.x < 0) { if (ta->CursorPosition.y == 0 || ta->CurrentNode->PrevNode == NULL) { ta->CursorPosition.x = 0; return; } MoveCursorUp(ta); MoveCursorEnd(ta); } } void MoveCursorHome(TextArea *ta) { if (ta == NULL || ta->CurrentNode == NULL) return; ta->CursorPosition.x = 0; } size_t GetStartingSpaces(char *string) { if (string == NULL) return 0; size_t idx = 0, len = strlen(string); while (string[idx] != 0 && string[idx] == ' ') idx++; if (string[idx] == 0) return 0; // no starting spaces return idx; } 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; } TextNode *next_node = (TextNode *)ta->CurrentNode->NextNode; size_t current_node_len = strlen(ta->CurrentNode->text); size_t spaces_len = GetStartingSpaces(ta->CurrentNode->text); size_t needed_space = spaces_len; size_t after_str_len = 0; char *after_str = NULL; if (ta->CursorPosition.x < current_node_len) { after_str = (ta->CurrentNode->text + (int)ta->CursorPosition.x); after_str_len = strlen(after_str); needed_space += after_str_len; } if (next_node->cap < needed_space + 1) { if (!AddSpaceDetermined(ta->CurrentNode->NextNode, needed_space + 1)) { TraceLog(LOG_ERROR, "Failed to push a node: the space reservaton failed"); return; } } for (int i = 0; i < spaces_len; ++i) { next_node->text[i] = ' '; } next_node->text[spaces_len] = 0x0; if (after_str != NULL) { strcpy(next_node->text + spaces_len, after_str); ((TextNode *)ta->CurrentNode->NextNode)->len = after_str_len + spaces_len; for (int i = ta->CursorPosition.x; i < current_node_len; i++) { ta->CurrentNode->text[i] = 0x0; } ta->CurrentNode->len -= after_str_len; } next_node->len = strlen(next_node->text); ta->CursorPosition.y += 1; ta->CursorPosition.x = spaces_len; ta->CurrentNode = ta->CurrentNode->NextNode; EnsureCursorIsInFrame(ta); } void InsertChar(TextArea *ta, char new_char) { if (ta == NULL || ta->CurrentNode == NULL) return; EnsureCursorIsInFrame(ta); 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); // Including a zero terminator here size_t after_str_len = strlen(after_str) + 1; memmove(ta->CurrentNode->text + (int)ta->CursorPosition.x - 1, after_str, after_str_len); } ta->CursorPosition.x -= 1; ta->CurrentNode->len -= 1; if (ta->CurrentNode->len < 0) ta->CurrentNode->len = 0; EnsureCursorIsInFrame(ta); } void DeleteCharAfter(TextArea *ta) { if (ta == NULL) return; if (ta->CursorPosition.x == ta->CurrentNode->len && ta->CurrentNode->NextNode == NULL) return; MoveCursorRight(ta); DeleteChar(ta); } TextArea *CreateTextAreaFromText(TextAreaDisplayParams *DisplayParams, char *text) { if (DisplayParams == NULL || text == NULL) return NULL; TextArea *outp = CreateTextArea(DisplayParams); if (outp == NULL) return NULL; const char *line_start = text; const char *next_newline = NULL; while (line_start != NULL) { next_newline = strchr(line_start, '\n'); size_t part_len = next_newline - line_start; if (next_newline == NULL) part_len = strlen(line_start); if (!PushNode(outp)) { TraceLog(LOG_ERROR, "Failed to push a node: the initializator returned NULL"); break; } TextNode *node = (TextNode *)outp->CurrentNode; if (part_len > 0) { if (node->cap < part_len) { if (!AddSpaceDetermined(node, part_len)) { TraceLog(LOG_ERROR, "Failed to reserve space for node text"); break; } } strncpy(node->text, line_start, part_len); node->text[part_len] = 0x0; node->len = part_len; } else { node->len = 0; if (node->text != NULL && node->cap > 0) { node->text[0] = '\0'; } } outp->CurrentNode = outp->CurrentNode->NextNode; if (next_newline != NULL) line_start = next_newline + 1; else line_start = NULL; } outp->CurrentNode = outp->StartNode; return outp; } 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); if (IsKeyPressedFix(KEY_EQUAL)) { if (ta->DisplayParams->font_size < MAX_FONT_SIZE) ta->DisplayParams->font_size += 1; } if (IsKeyPressedFix(KEY_MINUS)) { if (ta->DisplayParams->font_size > MIN_FONT_SIZE) ta->DisplayParams->font_size -= 1; } // 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_D)) DeleteCharAfter(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_DELETE)) DeleteCharAfter(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_TAB)) { for (int i = 0; i < TAB_WIDTH; i++) InsertChar(ta, ' '); }; if (IsKeyPressedFix(KEY_HOME)) MoveCursorHome(ta); if (IsKeyPressedFix(KEY_END)) MoveCursorEnd(ta); if (IsKeyPressedFix(KEY_F3)) PrintAllLines(ta); } /// Assumes that the cursor is in the scroll area void HandleScroll(TextArea *ta, Rectangle rec) { int scroll = GetMouseWheelMove() * SCROLL_SENC; if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) { ta->DisplayParams->scroll_h -= scroll; if (ta->DisplayParams->scroll_h < 0) ta->DisplayParams->scroll_h = 0; // TODO: figure out if we can easily get max width. Probably need some // lazy eval caching } else { int text_height = ta->NodeLenth * ta->DisplayParams->font_size; int max_scroll = text_height - rec.height; ta->DisplayParams->scroll_v -= scroll; if (ta->DisplayParams->scroll_v < 0) ta->DisplayParams->scroll_v = 0; if (text_height > rec.height && ta->DisplayParams->scroll_v > max_scroll) ta->DisplayParams->scroll_v = max_scroll; } } void UpdateTextArea(TextArea *ta, Rectangle rec) { HandleKeyboard(ta); if (IsMouseInRec(rec)) { HandleScroll(ta, rec); } } /// draws a text area on the screen. Main draw entry point void DrawTextArea(TextArea *ta, Rectangle rec) { // those are used to move a cursor in frame when typing ta->DisplayParams->_area_height = rec.height; ta->DisplayParams->_area_width = rec.width; TextAreaDisplayParams *params = ta->DisplayParams; Vector2 pos = { .x = rec.x + params->spacing - params->scroll_h, .y = rec.y + params->spacing - params->scroll_v }; Vector2 cursorPos = { .x = GetCursorXPos(ta), .y = ta->CursorPosition.y * ta->DisplayParams->font_size, }; TextNode *node = ta->StartNode; BeginScissorMode(rec.x, rec.y, rec.width, rec.height); while (node != NULL) { DrawTextEx(*params->font, node->text, pos, params->font_size, params->spacing, COLORSCHEME_BROWN); if (node == ta->CurrentNode) DrawCursor(ta, pos, cursorPos.x); node = node->NextNode; pos.y += params->font_size; } EndScissorMode(); } char *GetText(TextArea *ta) { size_t size = 0; TextNode *node = ta->StartNode; while (node != NULL) { size += node->len + 1; node = node->NextNode; } char *outp = malloc(size + 1); if (outp == NULL) { TraceLog(LOG_ERROR, "Failed to create a text from a text area"); return NULL; } size_t idx = 0; node = ta->StartNode; while (node != NULL) { memcpy(outp + idx, node->text, node->len); idx += node->len + 1; outp[idx - 1] = '\n'; node = node->NextNode; } outp[size] = 0x0; return outp; }