[Refactor] Abstract text editing into reusable components.

This commit is contained in:
Ritchie Cunningham 2025-09-28 23:31:17 +01:00
parent 43907509eb
commit 4a6d33292a
6 changed files with 237 additions and 23 deletions

View File

@ -1,4 +1,5 @@
#include <cstdio>
#include <memory>
#include <SDL3/SDL.h>
#include <GL/glew.h>
#include <SDL3/SDL_events.h>
@ -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<TextView>(&_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,17 +70,10 @@ 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) {
/* Pass input to TextView; if true, RET was pressed. */
if(_input_view->handle_event(event)) {
_on_ret_press();
}
}
}
void Terminal::scroll(int amount, int win_content_height) {
@ -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);
}

View File

@ -1,10 +1,13 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <SDL3/SDL.h>
#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<std::string> _history;
int _scroll_offset;
std::string _prompt;
ClientNetwork* _network;
TextBuffer _input_buffer;
std::unique_ptr<TextView> _input_view;
};

View File

@ -0,0 +1,77 @@
#include <SDL3/SDL_events.h>
#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);
}

19
client/src/ui/text_view.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <SDL3/SDL_events.h>
#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;
};

View File

@ -0,0 +1,86 @@
#include <algorithm>
#include <sstream>
#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;
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <vector>
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<std::string> _lines;
int _cursor_row;
int _cursor_col;
};