From 4a6d33292a951232c2f43a17a26cbcb198616d34 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sun, 28 Sep 2025 23:31:17 +0100 Subject: [PATCH] [Refactor] Abstract text editing into reusable components. --- client/src/terminal.cpp | 39 +++++++--------- client/src/terminal.h | 6 ++- client/src/ui/text_view.cpp | 77 +++++++++++++++++++++++++++++++ client/src/ui/text_view.h | 19 ++++++++ common/src/ui/text_buffer.cpp | 86 +++++++++++++++++++++++++++++++++++ common/src/ui/text_buffer.h | 33 ++++++++++++++ 6 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 client/src/ui/text_view.cpp create mode 100644 client/src/ui/text_view.h create mode 100644 common/src/ui/text_buffer.cpp create mode 100644 common/src/ui/text_buffer.h diff --git a/client/src/terminal.cpp b/client/src/terminal.cpp index e6cc525..a16b008 100644 --- a/client/src/terminal.cpp +++ b/client/src/terminal.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -8,11 +9,10 @@ #include "gfx/txt_renderer.h" #include "gfx/types.h" -Terminal::Terminal(ClientNetwork* network) : _network(network) { - /* Placeholder welcome message to history. */ - _should_close = false; - _scroll_offset = 0; - _prompt = ""; +Terminal::Terminal(ClientNetwork* network) + : _network(network), _should_close(false), _scroll_offset(0), + _prompt("") { + _input_view = std::make_unique(&_input_buffer); } Terminal::~Terminal(void) {} @@ -45,8 +45,8 @@ void Terminal::set_prompt(const std::string& prompt) { bool Terminal::close(void) { return _should_close; } void Terminal::_on_ret_press(void) { - std::string command = _input_buffer; - _input_buffer.clear(); /* Clear the input buffer immediately. */ + std::string command = _input_buffer.get_line(0); + _input_buffer.clear(); /* Add the command to history. */ _history.push_back(_prompt + "> " + command); @@ -70,16 +70,9 @@ void Terminal::_on_ret_press(void) { } void Terminal::handle_input(SDL_Event* event) { - if(event->type == SDL_EVENT_TEXT_INPUT) { - /* Append chars to the input buffer. */ - _input_buffer += event->text.text; - } else if(event->type == SDL_EVENT_KEY_DOWN) { - /* Handle special keys. */ - if(event->key.key == SDLK_BACKSPACE && _input_buffer.length() > 0) { - _input_buffer.pop_back(); - } else if(event->key.key == SDLK_RETURN) { - _on_ret_press(); - } + /* Pass input to TextView; if true, RET was pressed. */ + if(_input_view->handle_event(event)) { + _on_ret_press(); } } @@ -120,14 +113,16 @@ void Terminal::render(TextRenderer* renderer, int x, int y, int width, int heigh } /* Draw current input line. */ - std::string line_to_render = _prompt + "> " + _input_buffer; - if(show_cursor) { - line_to_render += "_"; - } float prompt_y_pos = (y+height) - padding - line_height - ((_history.size()-_scroll_offset)*line_height); - renderer->render_text(line_to_render.c_str(), x+padding, prompt_y_pos, 1.0f, green); + /* Render prompt string. */ + std::string prompt_str = _prompt + "> "; + renderer->render_text(prompt_str.c_str(), x + padding, prompt_y_pos, 1.0f, green); + + /* Render text view for the input right after prompt. */ + float input_x_pos = x + padding + (prompt_str.length() * 8.5f); /* Estimate width */ + _input_view->render(renderer, input_x_pos, prompt_y_pos, show_cursor); /* Disable scissor test. */ glDisable(GL_SCISSOR_TEST); } diff --git a/client/src/terminal.h b/client/src/terminal.h index 7224683..bf289ce 100644 --- a/client/src/terminal.h +++ b/client/src/terminal.h @@ -1,10 +1,13 @@ #pragma once #include #include +#include #include #include "gfx/txt_renderer.h" #include "client_network.h" +#include "ui/text_buffer.h" +#include "ui/text_view.h" class Terminal { public: @@ -22,10 +25,11 @@ public: private: void _on_ret_press(void); - std::string _input_buffer; bool _should_close; std::vector _history; int _scroll_offset; std::string _prompt; ClientNetwork* _network; + TextBuffer _input_buffer; + std::unique_ptr _input_view; }; diff --git a/client/src/ui/text_view.cpp b/client/src/ui/text_view.cpp new file mode 100644 index 0000000..af98db3 --- /dev/null +++ b/client/src/ui/text_view.cpp @@ -0,0 +1,77 @@ +#include +#include "text_view.h" +#include "gfx/txt_renderer.h" +#include "gfx/types.h" +#include "ui/text_buffer.h" + +TextView::TextView(TextBuffer* buffer) : _buffer(buffer) {} + +TextView::~TextView(void) {} + +bool TextView::handle_event(SDL_Event* event) { + if(!_buffer) return false; + + if(event->type == SDL_EVENT_TEXT_INPUT) { + _buffer->insert_char(event->text.text[0]); + } else if(event->type == SDL_EVENT_KEY_DOWN) { + switch(event->key.key) { + case SDLK_BACKSPACE: + _buffer->backspace(); + break; + case SDLK_RETURN: + /* Instead of adding newline, signal that input was submitted. */ + return true; + break; + case SDLK_LEFT: + _buffer->move_cursor(-1,0); + break; + case SDLK_RIGHT: + _buffer->move_cursor(1,0); + break; + case SDLK_UP: + _buffer->move_cursor(0,-1); + break; + case SDLK_DOWN: + _buffer->move_cursor(0,1); + break; + case SDLK_HOME: + _buffer->move_cursor_home(); + break; + case SDLK_END: + _buffer->move_cursor_end(); + break; + default: + break; + } + } + return false; +} + +void TextView::render(TextRenderer* renderer, int x, int y, bool show_cursor) { + if(!_buffer) return; + + const Color text_color = { 1.0f, 1.0f, 1.0f }; + float line_height = 20.0f; /* TODO: Get font metrics? */ + + /* + * Just render first line (for term input) for now. + * The code editor would need to iterate through _buffer->get_line_count() + */ + std::string line = _buffer->get_line(0); + Point cursor_pos = _buffer->get_cursor_pos(); + + if(show_cursor) { + /* + * This hacky. we should calculate the text width + * up to the cursor pos to draw correctly. + * For now, just append an underscore. + */ + if(cursor_pos.col == line.length()) { + line += "_"; + } else { + line.insert(cursor_pos.col, 1, '_'); + } + } + + renderer->render_text(line.c_str(), x, y, 1.0f, text_color); +} diff --git a/client/src/ui/text_view.h b/client/src/ui/text_view.h new file mode 100644 index 0000000..b9170d0 --- /dev/null +++ b/client/src/ui/text_view.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "ui/text_buffer.h" + +class TextRenderer; + +class TextView { +public: + TextView(TextBuffer* buffer); + ~TextView(void); + + bool handle_event(SDL_Event* event); + void render(TextRenderer* renderer, int x, int y, bool show_cursor); + +private: + TextBuffer* _buffer; +}; diff --git a/common/src/ui/text_buffer.cpp b/common/src/ui/text_buffer.cpp new file mode 100644 index 0000000..3c81893 --- /dev/null +++ b/common/src/ui/text_buffer.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include "ui/text_buffer.h" + +TextBuffer::TextBuffer(void) : _cursor_row(0), _cursor_col(0) { + _lines.emplace_back(""); /* Start with a single empty line. */ +} + +TextBuffer::~TextBuffer(void) {} + +void TextBuffer::insert_char(char c) { + _lines[_cursor_row].insert(_cursor_col, 1, c); + _cursor_col++; +} + +void TextBuffer::backspace(void) { + if(_cursor_col > 0) { + _lines[_cursor_row].erase(_cursor_col - 1, 1); + _cursor_col--; + } else if(_cursor_row > 0) { + /* If start of line, merge with previous. */ + _cursor_col = _lines[_cursor_row-1].length(); + _lines[_cursor_row-1] += _lines[_cursor_row]; + _lines.erase(_lines.begin() + _cursor_row); + _cursor_row--; + } +} + +void TextBuffer::newline(void) { + std::string current_line = _lines[_cursor_row]; + std::string new_line = current_line.substr(_cursor_col); + _lines[_cursor_row] = current_line.substr(0, _cursor_col); + + _cursor_row++; + _lines.insert(_lines.begin() + _cursor_row, new_line); + _cursor_col = 0; +} + +void TextBuffer::move_cursor(int dx, int dy) { + _cursor_col += dx; + _cursor_row += dy; + + /* Clamp cursor row. */ + _cursor_row = std::max(0, std::min((int)_lines.size()-1, _cursor_row)); + /* Clamp cursor col. */ + _cursor_col = std::max(0, std::min((int)_lines[_cursor_row].length(), _cursor_col)); +} + +void TextBuffer::move_cursor_home(void) { + _cursor_col = 0; +} + +void TextBuffer::move_cursor_end(void) { + _cursor_col = _lines[_cursor_row].length(); +} + +const std::string& TextBuffer::get_line(int row) const { + return _lines[row]; +} + +size_t TextBuffer::get_line_count(void) const { + return _lines.size(); +} + +Point TextBuffer::get_cursor_pos(void) const { + return { _cursor_row, _cursor_col }; +} + +std::string TextBuffer::get_text(void) const { + std::stringstream ss; + for(size_t i = 0; i < _lines.size(); ++i) { + ss << _lines[i]; + if(i < _lines.size()-1) { + ss << "\n"; /* Or another separator? */ + } + } + return ss.str(); +} + +void TextBuffer::clear(void) { + _lines.clear(); + _lines.emplace_back(""); + _cursor_row = 0; + _cursor_col = 0; +} diff --git a/common/src/ui/text_buffer.h b/common/src/ui/text_buffer.h new file mode 100644 index 0000000..e3eadad --- /dev/null +++ b/common/src/ui/text_buffer.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +struct Point { + int row; + int col; +}; + +class TextBuffer { +public: + TextBuffer(void); + ~TextBuffer(void); + + void insert_char(char c); + void backspace(void); + void newline(void); + void move_cursor(int dx, int dy); + void move_cursor_home(void); + void move_cursor_end(void); + + const std::string& get_line(int row) const; + size_t get_line_count(void) const; + Point get_cursor_pos(void) const; + std::string get_text(void) const; + void clear(void); + +private: + std::vector _lines; + int _cursor_row; + int _cursor_col; +};