#include "muscles.h" #include "ui.h" int single_line_filterer(char *str, int len) { char *in = str; char *out = str; for (int i = 0; i < len; i++) { if (*in == '\n') break; if (*in != '\r') *out++ = *in; in++; } return out - str; } int multiline_ascii_filterer(char *str, int len) { char *in = str; char *out = str; for (int i = 0; i < len; i++) { if (*in == '\t' || *in == '\n' || (*in >= ' ' && *in <= '~')) *out++ = *in; in++; } return out - str; } void Editor::clear() { text.clear(); set_cursor(primary, 0); set_cursor(secondary, 0); selected = false; x_offset = 0; y_offset = 0; lines = vis_lines = 0; columns = vis_columns = 0; } void Editor::set_text_from_buffer(std::pair>&& buffer) { char *str = (char*)buffer.second.get(); int len = multiline_ascii_filterer(str, buffer.first); text = std::string(str, len); set_cursor(primary, 0); set_cursor(secondary, 0); selected = false; x_offset = 0; y_offset = 0; measure_text(); } void Editor::refresh(Render_Clip& clip, Font *font, float scroll_x, float scroll_y) { this->clip = clip; this->font = font; font_h = font->render.text_height(); digit_w = font->render.digit_width(); if (scroll_x >= 0) x_offset = scroll_x; if (scroll_y >= 0) y_offset = scroll_y; vis_columns = (clip.x_upper - clip.x_lower) / digit_w; vis_lines = (clip.y_upper - clip.y_lower) / font_h; } void Editor::erase(Cursor& cursor, bool is_back) { if (is_back) { if (cursor.cursor <= 0) return; set_cursor(cursor, cursor.cursor - 1); } else if (cursor.cursor >= text.size()) return; text.erase(cursor.cursor, 1); } // Used for left/right arrow movement and when adding/removing characters void Editor::set_cursor(Cursor& cursor, int cur) { int c = 0; int l = 0; int i, len = text.size(); for (i = 0; i < cur && i < len; i++) { if (text[i] == '\t') c += tab_width; else c++; if (text[i] == '\n') { c = 0; l++; } } cursor.line = l; cursor.column = c; cursor.target_column = c; cursor.cursor = i; } // Used for up/down arrow movement and mouse selection on y-axis void Editor::set_line(Cursor& cursor, int line_idx) { if (cursor.line == line_idx) return; int l = 0; int c = 0; int len = text.size(); int prev_col_end = 0; int last_nl = 0; bool reached_target = false; int idx = 0; for (int i = 0; i < len; i++) { if (l == line_idx && c >= cursor.target_column) { cursor.column = c; reached_target = true; idx = i; break; } if (l > line_idx && line_idx >= 0) { cursor.column = prev_col_end; l--; idx = i-1; reached_target = true; break; } if (text[i] == '\t') c += tab_width; else c++; if (text[i] == '\n') { l++; last_nl = i+1; prev_col_end = c-1; c = 0; } } if (!reached_target) { int end = 0; int chars = 0; for (int i = last_nl; i < len; i++) { if (end < cursor.column) chars++; end += text[i] == '\t' ? tab_width : 1; } if (cursor.column > end || cursor.target_column >= end) cursor.column = end; cursor.cursor = last_nl + chars; } else cursor.cursor = idx; cursor.line = l; } // Used for home/end movement and mouse selection on x-axis void Editor::set_column(Cursor& cursor, int col_idx) { int len = text.size(); int start = 0, end = 0; bool past_end = true; int l = 0; for (int i = 0; i < len; i++) { if (text[i] != '\n') continue; if (end) start = end+1; end = i; if (l >= cursor.line) { past_end = false; break; } l++; } if (past_end) { if (end) start = end+1; end = len; } int max_cols = 0; int cols = 0; int chars = 0; int line_len = end - start; bool found_column = false; for (int i = 0; i < line_len; i++) { if (!found_column && max_cols >= col_idx) { cols = max_cols; chars = i; found_column = true; } max_cols += text[start+i] == '\t' ? tab_width : 1; } if (!found_column) { cols = max_cols; chars = line_len; } if (col_idx < 0 || col_idx > max_cols) { col_idx = max_cols; chars = line_len; } else col_idx = cols; cursor.column = cursor.target_column = col_idx; cursor.line = l; cursor.cursor = start + chars; } bool is_word_char(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; } bool is_space(char c) { return c == ' ' || c == '\t'; } int previous_word(std::string& text, int cur) { if (cur > 0) cur--; int old = cur; while (cur > 0 && is_space(text[cur])) cur--; while (cur > 0 && is_word_char(text[cur])) cur--; if (old > cur && cur < text.size() && (text[cur] == ' ' || text[cur] == '\t')) cur++; return cur; } int next_word(std::string& text, int cur, bool selecting) { bool skip_word = true; if (is_space(text[cur])) { skip_word = selecting; while (cur < text.size() && is_space(text[cur])) cur++; } if (skip_word) { cur++; while (cur < text.size() && is_word_char(text[cur])) cur++; } if (!selecting) { while (cur < text.size() && (text[cur] == ' ' || text[cur] == '\t')) cur++; } return cur; } std::pair get_text_span(int a, int b) { std::pair span; if (a < b) { span.first = a; span.second = b - a; } else { span.first = b; span.second = a - b; } return span; } void Editor::measure_text() { lines = columns = 0; int cols = 0; for (auto& c : text) { cols += c == '\t' ? tab_width : 1; if (c == '\n') { lines++; if (cols > columns) columns = cols; cols = 0; } } if (cols > columns) columns = cols; lines++; columns++; } int Editor::handle_input(Input& input) { char ch = 0; bool ctrl = input.lctrl || input.rctrl; bool shift = input.lshift || input.rshift; bool pgup = input.strike(input.pgup); bool pgdown = input.strike(input.pgdown); bool esc = false; bool enter = false; bool page_move = false; bool key_press = true; bool end_selection = true; bool erased = false; bool paste = false; bool update = false; bool was_selected = selected; if (input.strike(input.back)) { if (selected && primary.cursor == secondary.cursor) selected = false; if (!selected) { if (ctrl) { int end = primary.cursor; int cur = primary.cursor - 1; if (shift) { shift = false; while (cur > 0 && text[cur] != '\n') cur--; if (text[cur] == '\n') cur++; } else cur = previous_word(text, primary.cursor); if (cur < 0) cur = 0; text.erase(cur, end - cur); set_cursor(primary, cur); } else erase(primary, true); } erased = true; update = true; } else if (input.strike(input.del)) { if (selected && primary.cursor == secondary.cursor) selected = false; if (!selected) { if (ctrl) { int start = primary.cursor; int cur = primary.cursor + 1; if (shift) { shift = false; while (cur <= text.size() && text[cur] != '\n') cur++; } else cur = next_word(text, primary.cursor, false); text.erase(start, cur - start); set_cursor(primary, start); } else erase(primary, false); } erased = true; update = true; } else if (input.strike(input.esc)) esc = true; else if (input.strike(input.enter)) { enter = true; if (multiline) ch = '\n'; } else if (multiline && input.strike(input.tab)) { if (secondary.cursor != primary.cursor) { Cursor *first = primary.cursor < secondary.cursor ? &primary : &secondary; Cursor *second = primary.cursor < secondary.cursor ? &secondary : &primary; Cursor cur = *first; for (int i = first->line; i <= second->line; i++) { set_column(cur, 0); if (shift) { if (text[cur.cursor] == '\t') text.erase(cur.cursor, 1); } else text.insert(text.begin() + cur.cursor, '\t'); set_line(cur, i+1); } end_selection = false; update = true; } else ch = '\t'; } else if (input.strike(input.home)) { if (ctrl) set_line(primary, 0); set_column(primary, 0); } else if (input.strike(input.end)) { if (ctrl) set_line(primary, -1); set_column(primary, -1); } else if (multiline && (pgup || pgdown)) { float h = clip.y_upper - clip.y_lower; int visible = h / font_h; int delta = pgup ? -visible : visible; int req_line = primary.line + delta; if (req_line < 0) req_line = 0; set_line(primary, req_line); y_scroll_delta = (float)delta * font_h; page_move = true; } else if (input.strike(input.left)) { int cur = primary.cursor - 1; if (ctrl) cur = previous_word(text, primary.cursor); set_cursor(primary, cur); } else if (input.strike(input.right)) { int cur = primary.cursor + 1; if (ctrl) cur = next_word(text, primary.cursor, shift); set_cursor(primary, cur); } else if (input.strike(input.up) && !ctrl) { int l = primary.line - 1; if (l < 0) l = 0; set_line(primary, l); } else if (input.strike(input.down) && !ctrl) { set_line(primary, primary.line + 1); } else if (input.ch || input.last_key) { if (ctrl && use_clipboard) { if (selected && secondary.cursor != primary.cursor && (input.last_key == 'x' || input.last_key == 'c')) { auto span = get_text_span(primary.cursor, secondary.cursor); std::string clip_str(text, span.first, span.second); sdl_copy(clip_str); erased = input.last_key == 'x'; end_selection = erased; } else if (input.last_key == 'v') { paste = true; } else if (input.last_key == 'a') { set_cursor(primary, 0); set_cursor(secondary, text.size()); selected = true; end_selection = false; } else end_selection = false; } else if (input.ch) { if (numeric_only) { if (input.ch >= '0' && input.ch <= '9') ch = input.ch; } else ch = input.ch; } } else { end_selection = false; key_press = false; } if (end_selection) { bool expunge = selected && (erased || paste || ch); if (expunge && secondary.cursor != primary.cursor) { auto span = get_text_span(primary.cursor, secondary.cursor); if (primary.cursor > secondary.cursor) primary = secondary; text.erase(span.first, span.second); update = true; } selected = false; } if (paste) { auto filterer = multiline ? multiline_ascii_filterer : single_line_filterer; int advance = sdl_paste_into(text, primary.cursor, filterer); set_cursor(primary, primary.cursor + advance); update = true; } if (ch) { text.insert(text.begin() + primary.cursor, ch); set_cursor(primary, primary.cursor + 1); update = true; } else if (input.lmouse || shift) selected = true; if (!selected) secondary = primary; if (update) { measure_text(); } if (key_press) { int first_vis_col = x_offset / digit_w; int first_vis_line = y_offset / font_h; int last_vis_col = vis_columns + x_offset / digit_w; int last_vis_line = vis_lines + y_offset / font_h; if (primary.column < first_vis_col) x_scroll_delta = -digit_w * (float)(first_vis_col - primary.column); else if (secondary.column < first_vis_col) x_scroll_delta = -digit_w * (float)(first_vis_col - secondary.column); else if (primary.column > last_vis_col) x_scroll_delta = digit_w * (float)(primary.column - last_vis_col); else if (secondary.column > last_vis_col) x_scroll_delta = digit_w * (float)(secondary.column - last_vis_col); if (primary.line < first_vis_line) y_scroll_delta = -font_h * (float)(first_vis_line - primary.line); else if (secondary.line < first_vis_line) y_scroll_delta = -font_h * (float)(first_vis_line - secondary.line); else if (primary.line > last_vis_line) y_scroll_delta = font_h * (float)(primary.line - last_vis_line); else if (secondary.line > last_vis_line) y_scroll_delta = font_h * (float)(secondary.line - last_vis_line); } int flags = (update || enter) ? 1 : 0; flags |= (key_press || update || selected != was_selected) ? 2 : 0; flags |= end_selection ? 4 : 0; flags |= esc ? 8 : 0; flags |= page_move ? 16 : 0; return flags; } void Editor::update_cursor(float x, float y, Font *font, float scale, int ticks, bool click) { float edge = border * scale; float gl_w = font->render.digit_width(); float gl_h = font->render.text_height(); x += x_offset - edge; y += y_offset - edge; int line = y / gl_h; int col = x / gl_w + 0.5; if (line < 0) line = 0; if (col < 0) col = 0; if (ticks - last_tick < click_window) { if (click) click_run++; } else click_run = 1; if (click) { if (click_run == 1) { set_line(primary, line); set_column(primary, col); secondary = primary; } else if (click_run == 2) { set_cursor(primary, previous_word(text, primary.cursor + 1)); set_cursor(secondary, next_word(text, primary.cursor, true)); selected = true; } else if (click_run == 3) { set_column(primary, 0); secondary = primary; set_column(secondary, -1); selected = true; } last_tick = ticks; } else if (click_run <= 1) { set_line(primary, line); set_column(primary, col); } } void Editor::apply_scroll(Scroll *hscroll, Scroll *vscroll) { if (hscroll) hscroll->scroll(x_scroll_delta); else { x_offset += x_scroll_delta; x_offset = x_offset < 0 ? 0 : x_offset; } if (vscroll) vscroll->scroll(y_scroll_delta); else { y_offset += y_scroll_delta; y_offset = y_offset < 0 ? 0 : y_offset; } x_scroll_delta = y_scroll_delta = 0; } void Editor::draw_selection_box(Renderer renderer, RGBA& color) { if (!selected || primary.cursor == secondary.cursor) return; float line_pad = font_h * 0.15f; Editor::Cursor *first = primary.cursor < secondary.cursor ? &primary : &secondary; Editor::Cursor *second = primary.cursor < secondary.cursor ? &secondary : &primary; auto col_to_x = [this](int column) { return (column * digit_w) - x_offset; }; auto line_to_y = [this](int line) { return (line * font_h) - y_offset; }; float left = clip.x_lower; float top = clip.y_lower + line_pad; Rect_Int r = {0}; if (first->line == second->line) { r.x = left + col_to_x(first->column); r.y = top + line_to_y(first->line); r.w = (second->column - first->column) * digit_w + 1; r.h = font_h + 1; sdl_draw_rect_clipped(r, clip, color, renderer); } else { float line_w = clip.x_upper - clip.x_lower; float x = col_to_x(first->column); r.x = left + x; r.y = top + line_to_y(first->line); r.w = line_w - x; r.h = font_h + 1; sdl_draw_rect_clipped(r, clip, color, renderer); int gulf = second->line - first->line - 1; if (gulf > 0) { r.x = left; r.y += font_h; r.w = line_w; r.h = gulf * font_h + 1; sdl_draw_rect_clipped(r, clip, color, renderer); } if (second->column > 0) { r.x = left; r.y = top + line_to_y(second->line); r.w = col_to_x(second->column) + 1; r.h = font_h + 1; sdl_draw_rect_clipped(r, clip, color, renderer); } } } void Editor::draw_cursor(Renderer renderer, RGBA& color, Editor::Cursor& cursor, Point& pos, Render_Clip& cur_clip, float scale) { float line_pad = font_h * 0.15f; float caret_x = pos.x - x_offset + ((float)cursor.column * digit_w); float caret_y = pos.y - y_offset + (line_pad * 1.5f) + ((float)cursor.line * font_h); float caret_w = cursor_width * scale; if (caret_w < 1) caret_w = 1; Rect caret = { caret_x, caret_y, caret_w, font_h - line_pad }; sdl_draw_rect_clipped(caret, cur_clip, color, renderer); }