198 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include <cstdio>
 | 
						|
#include <memory>
 | 
						|
#include <sstream>
 | 
						|
#include <SDL3/SDL.h>
 | 
						|
#include <GL/glew.h>
 | 
						|
#include <SDL3/SDL_events.h>
 | 
						|
 | 
						|
#include "terminal.h"
 | 
						|
#include "game_state.h"
 | 
						|
#include "gfx/types.h"
 | 
						|
#include "ui/editor.h"
 | 
						|
#include "ui/window_action.h"
 | 
						|
 | 
						|
Terminal::Terminal(GameState* game_state)
 | 
						|
    : _game_state(game_state), _should_close(false), _command_history_index(0),
 | 
						|
      _scroll_offset(0), _prompt(""), _pending_action({ActionType::NONE}),
 | 
						|
      _session_id(0) {
 | 
						|
  _input_view = std::make_unique<TextView>(&_input_buffer, false, false, false);
 | 
						|
}
 | 
						|
 | 
						|
Terminal::~Terminal(void) {}
 | 
						|
 | 
						|
void Terminal::update(int content_width, int content_height) {
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::add_history(const std::string& line) {
 | 
						|
  std::string line_with_spaces;
 | 
						|
  for(char ch : line) {
 | 
						|
    if(ch == '\t') {
 | 
						|
      line_with_spaces += "   ";
 | 
						|
    } else {
 | 
						|
      line_with_spaces += ch;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  _history.push_back(line_with_spaces);
 | 
						|
 | 
						|
  if(line == "__CLOSE_CONNECTION__") {
 | 
						|
    _should_close = true;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::set_prompt(const std::string& prompt) {
 | 
						|
  _prompt = prompt;
 | 
						|
}
 | 
						|
 | 
						|
bool Terminal::should_close(void) {
 | 
						|
  return _should_close;
 | 
						|
}
 | 
						|
 | 
						|
WindowAction Terminal::get_pending_action(void) {
 | 
						|
  WindowAction action = _pending_action;
 | 
						|
  _pending_action.type = ActionType::NONE;
 | 
						|
  return action;
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::set_session_id(uint32_t id) {
 | 
						|
  _session_id = id;
 | 
						|
}
 | 
						|
 | 
						|
uint32_t Terminal::get_session_id(void) const {
 | 
						|
  return _session_id;
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::_on_ret_press(void) {
 | 
						|
  std::string command = _input_buffer.get_line(0);
 | 
						|
  if(!command.empty()) {
 | 
						|
    _command_history.push_back(command);
 | 
						|
  }
 | 
						|
  _command_history_index = _command_history.size();
 | 
						|
 | 
						|
  _input_buffer.clear();
 | 
						|
 | 
						|
  if(command == "clear") {
 | 
						|
    _history.push_back(_prompt + "> " + command);
 | 
						|
    _history.clear();
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  /* Client-side command handling. */
 | 
						|
  std::stringstream ss(command);
 | 
						|
  std::string cmd_name;
 | 
						|
  ss >> cmd_name;
 | 
						|
  if(cmd_name == "edit") {
 | 
						|
    _history.push_back(_prompt + "> " + command);
 | 
						|
    ss >> _pending_action.payload1; /* filename. */
 | 
						|
    _pending_action.type = ActionType::READ_FILE;
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  _history.push_back(_prompt + "> " + command);
 | 
						|
  _game_state->send_network_command(_session_id, command);
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y,
 | 
						|
                            int content_width, int content_height) {
 | 
						|
  /* Pass input to TextView; if true, RET was pressed. */
 | 
						|
  if(event->type == SDL_EVENT_KEY_DOWN) {
 | 
						|
    switch(event->key.key) {
 | 
						|
      case SDLK_UP:
 | 
						|
        if(!_command_history.empty()) {
 | 
						|
          _command_history_index = std::max(0, _command_history_index-1);
 | 
						|
          _input_buffer.set_text(_command_history[_command_history_index]);
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case SDLK_DOWN:
 | 
						|
        if(!_command_history.empty()) {
 | 
						|
          _command_history_index =
 | 
						|
            std::min((int)_command_history.size(), _command_history_index+1);
 | 
						|
          if(_command_history_index < (int)_command_history.size()) {
 | 
						|
            _input_buffer.set_text(_command_history[_command_history_index]);
 | 
						|
          } else {
 | 
						|
            _input_buffer.clear();
 | 
						|
          }
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      default:
 | 
						|
        if(_input_view->handle_event(event, content_height)) { _on_ret_press(); }
 | 
						|
    }
 | 
						|
  } else {
 | 
						|
    if(_input_view->handle_event(event, content_height)) { _on_ret_press(); }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::scroll(int amount, int win_content_height) {
 | 
						|
  /* amount > 0 = scroll up. amount < 0 = scroll down. */
 | 
						|
  _scroll_offset += amount;
 | 
						|
 | 
						|
  /* Lower bound: Don't scroll below the top of the history. */
 | 
						|
  if(_scroll_offset < 0) {
 | 
						|
    _scroll_offset = 0;
 | 
						|
  }
 | 
						|
  /* Upper bound: Don't scroll past the last command. */
 | 
						|
  float line_height = 20.0f;
 | 
						|
  int visible_lines = win_content_height / line_height;
 | 
						|
  int max_scroll = (_history.size()+1) - visible_lines;
 | 
						|
  if(max_scroll < 0) max_scroll = 0;
 | 
						|
  if(_scroll_offset > max_scroll) {
 | 
						|
    _scroll_offset = max_scroll;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void Terminal::render(const RenderContext& context, int x, int y_screen, int y_gl,
 | 
						|
                      int width, int height) {
 | 
						|
  const Color white = { 1.0f, 1.0f, 1.0f };
 | 
						|
  const Color green = { 0.2f, 1.0f, 0.2f };
 | 
						|
  float line_height = 20.0f;
 | 
						|
  float padding     = 5.0f;
 | 
						|
 | 
						|
  /*
 | 
						|
   * Auto-scroll to bottom if the user has not manually scrolled up.
 | 
						|
   * Ensures input line is always visible after submitting a command.
 | 
						|
   */
 | 
						|
  int visible_lines = (height-padding) / line_height;
 | 
						|
  int max_scroll_offset = (_history.size() + 1) - visible_lines;
 | 
						|
  if(max_scroll_offset < 0) max_scroll_offset = 0;
 | 
						|
  if(_scroll_offset >= max_scroll_offset - 1) _scroll_offset = max_scroll_offset;
 | 
						|
 | 
						|
  context.ui_renderer->begin_text();
 | 
						|
 | 
						|
  /* Enable scissor test to clip rendering to the window content area. */
 | 
						|
  glEnable(GL_SCISSOR_TEST);
 | 
						|
  glScissor(x, y_gl, width, height);
 | 
						|
 | 
						|
  /* Draw History. */
 | 
						|
  for(size_t i = _scroll_offset; i < _history.size(); ++i) {
 | 
						|
    float line_y_pos = y_screen + padding + ((i - _scroll_offset) * line_height);
 | 
						|
    /* Culling: If line is already below the view, stop. */
 | 
						|
    if(line_y_pos > y_screen + height) { break; }
 | 
						|
    context.ui_renderer->render_text(_history[i].c_str(), x+padding, line_y_pos+18, white);
 | 
						|
  }
 | 
						|
 | 
						|
  /* Draw current input line. */
 | 
						|
  float prompt_line_y = (y_screen + padding) + ((_history.size() - _scroll_offset) * line_height);
 | 
						|
  float prompt_baseline_y = prompt_line_y + 18;
 | 
						|
 | 
						|
  /* Render prompt string. */
 | 
						|
  std::string prompt_str = _prompt + "> ";
 | 
						|
  context.ui_renderer->render_text(prompt_str.c_str(), x+padding, prompt_baseline_y, green);
 | 
						|
 | 
						|
  /* Render text view for the input right after prompt. */
 | 
						|
  float input_x_pos = x + padding + (prompt_str.length() * 8.5f); /* Estimate width */
 | 
						|
  float input_width = width - (input_x_pos-x);
 | 
						|
  SyntaxTheme theme; /* Terminal doesn't need highlighting, just a default theme. */
 | 
						|
  _input_view->render_text_content(context.ui_renderer, theme, input_x_pos, prompt_line_y, input_width, line_height);
 | 
						|
 | 
						|
  context.ui_renderer->flush_text();
 | 
						|
 | 
						|
  /* Disable scissor test. */
 | 
						|
  glDisable(GL_SCISSOR_TEST);
 | 
						|
 | 
						|
  if(context.show_cursor) {
 | 
						|
    context.ui_renderer->begin_shapes();
 | 
						|
    _input_view->render_cursor(context.ui_renderer, theme, input_x_pos, prompt_line_y, input_width, line_height);
 | 
						|
    context.ui_renderer->flush_shapes();
 | 
						|
  }
 | 
						|
}
 |