From a6e159f0da51f5d0ecf650d032c1e0d380d43047 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Fri, 3 Oct 2025 23:56:30 +0100 Subject: [PATCH] Menu bar for for text editor etc. --- client/src/game_state.cpp | 12 ++-- client/src/game_state.h | 5 +- client/src/gfx/types.h | 10 ++++ client/src/main.cpp | 9 ++- client/src/terminal.cpp | 12 ++-- client/src/terminal.h | 6 +- client/src/ui/desktop.cpp | 22 +++----- client/src/ui/desktop.h | 5 +- client/src/ui/editor.cpp | 43 ++++++++++++--- client/src/ui/editor.h | 12 ++-- client/src/ui/i_window_content.h | 8 +-- client/src/ui/menu_bar.cpp | 95 ++++++++++++++++++++++++++++++++ client/src/ui/menu_bar.h | 40 ++++++++++++++ client/src/ui/ui_window.cpp | 24 ++++---- client/src/ui/ui_window.h | 6 +- 15 files changed, 242 insertions(+), 67 deletions(-) create mode 100644 client/src/ui/menu_bar.cpp create mode 100644 client/src/ui/menu_bar.h diff --git a/client/src/game_state.cpp b/client/src/game_state.cpp index da3b377..a0d9a2e 100644 --- a/client/src/game_state.cpp +++ b/client/src/game_state.cpp @@ -4,6 +4,7 @@ #include #include +#include "gfx/types.h" #include "network_manager.h" #include "game_state.h" #include "gfx/shape_renderer.h" @@ -135,7 +136,7 @@ void GameState::update(void) { if(separator_pos != std::string::npos) { std::string filepath = payload.substr(0, separator_pos); std::string content = payload.substr(separator_pos+2); - auto editor = std::make_unique(); + auto editor = std::make_unique(filepath); editor->set_buffer_content(content); auto editor_window = std::make_unique(filepath.c_str(), 200, 200, 600, 400); @@ -192,22 +193,21 @@ void GameState::update(void) { } } -void GameState::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, - int screen_height, bool show_cursor) { +void GameState::render(const RenderContext& context) { switch(_current_screen) { case Screen::MAIN_MENU: if(_main_menu) { - _main_menu->render(shape_renderer, txt_renderer); + _main_menu->render(context.shape_renderer, context.txt_renderer); } break; case Screen::BOOTING: if(_boot_sequence) { - _boot_sequence->render(txt_renderer, screen_height); + _boot_sequence->render(context.txt_renderer, context.screen_height); } break; case Screen::DESKTOP: if(_desktop) { - _desktop->render(shape_renderer, txt_renderer, screen_height, show_cursor); + _desktop->render(context); } break; } diff --git a/client/src/game_state.h b/client/src/game_state.h index 15d2570..cc1d633 100644 --- a/client/src/game_state.h +++ b/client/src/game_state.h @@ -2,6 +2,8 @@ #include +#include "gfx/types.h" + class ClientNetwork; class Desktop; class BootSequence; @@ -25,8 +27,7 @@ public: void start_single_player_now(int screen_width, int screen_height); void handle_event(SDL_Event* event, int screen_width, int screen_height); void update(void); - void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, - int screen_height, bool show_cursor); + void render(const RenderContext& context); private: std::unique_ptr _network; diff --git a/client/src/gfx/types.h b/client/src/gfx/types.h index 8362cc6..1752237 100644 --- a/client/src/gfx/types.h +++ b/client/src/gfx/types.h @@ -1,5 +1,8 @@ #pragma once +class ShapeRenderer; +class TextRenderer; + struct Color { float r, g, b; }; @@ -7,3 +10,10 @@ struct Color { struct Rect { int x, y, w, h; }; + +struct RenderContext { + ShapeRenderer* shape_renderer; + TextRenderer* txt_renderer; + int screen_height; + bool show_cursor; +}; diff --git a/client/src/main.cpp b/client/src/main.cpp index f5082aa..3edece2 100644 --- a/client/src/main.cpp +++ b/client/src/main.cpp @@ -8,6 +8,7 @@ #include "gfx/shape_renderer.h" #include "gfx/txt_renderer.h" #include "game_state.h" +#include "gfx/types.h" #include "ui/cursor_manager.h" const int SCREEN_WIDTH = 1280; @@ -109,7 +110,13 @@ int main(int argc, char** argv) { glClearColor(0.04f, 0.05f, 0.06, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - game_state->render(shape_renderer_instance, txt_render_instance, SCREEN_HEIGHT, show_cursor); + RenderContext context = { + shape_renderer_instance, + txt_render_instance, + SCREEN_HEIGHT, + show_cursor + }; + game_state->render(context); /* It's really odd to call it SwapWindow now, rather than SwapBuffer. */ SDL_GL_SwapWindow(window); diff --git a/client/src/terminal.cpp b/client/src/terminal.cpp index f8afa4e..008dee2 100644 --- a/client/src/terminal.cpp +++ b/client/src/terminal.cpp @@ -88,7 +88,7 @@ void Terminal::_on_ret_press(void) { } } -void Terminal::handle_input(SDL_Event* event) { +void Terminal::handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y) { /* Pass input to TextView; if true, RET was pressed. */ if(_input_view->handle_event(event)) { _on_ret_press(); } } @@ -111,8 +111,7 @@ void Terminal::scroll(int amount, int win_content_height) { } } -void Terminal::render(TextRenderer* renderer, int x, int y, int width, int height, - bool show_cursor) { +void Terminal::render(const RenderContext& context, int x, int y, int width, int height) { const Color white = { 1.0f, 1.0f, 1.0f }; const Color green = { 0.2f, 1.0f, 0.2f }; @@ -126,7 +125,7 @@ void Terminal::render(TextRenderer* renderer, int x, int y, int width, int heigh /* Draw History. */ for(size_t i = _scroll_offset; i < _history.size(); ++i) { float y_pos = (y+height) - padding - line_height - ((i - _scroll_offset) * line_height); - renderer->render_text(_history[i].c_str(), x+padding, y_pos, 1.0f, white); + context.txt_renderer->render_text(_history[i].c_str(), x+padding, y_pos, 1.0f, white); } /* Draw current input line. */ @@ -135,12 +134,13 @@ void Terminal::render(TextRenderer* renderer, int x, int y, int width, int heigh /* Render prompt string. */ std::string prompt_str = _prompt + "> "; - renderer->render_text(prompt_str.c_str(), x + padding, prompt_y_pos, 1.0f, green); + context.txt_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 */ float input_width = width - (input_x_pos-x); - _input_view->render(renderer, input_x_pos, prompt_y_pos, input_width, line_height, show_cursor); + _input_view->render(context.txt_renderer, input_x_pos, prompt_y_pos, input_width, + line_height, context.show_cursor); /* Disable scissor test. */ glDisable(GL_SCISSOR_TEST); } diff --git a/client/src/terminal.h b/client/src/terminal.h index 37812a1..04b2d83 100644 --- a/client/src/terminal.h +++ b/client/src/terminal.h @@ -4,8 +4,8 @@ #include #include -#include "gfx/txt_renderer.h" #include "client_network.h" +#include "gfx/types.h" #include "ui/text_buffer.h" #include "ui/text_view.h" #include "ui/i_window_content.h" @@ -17,8 +17,8 @@ public: ~Terminal(void); void update(void) override; - void handle_input(SDL_Event* event) override; - void render(TextRenderer* renderer, int x, int y, int width, int height, bool show_cursor) override; + void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y) override; + void render(const RenderContext& context, int x, int y, int width, int height) override; void scroll(int amount, int content_height) override; void add_history(const std::string& line); void set_prompt(const std::string& prompt); diff --git a/client/src/ui/desktop.cpp b/client/src/ui/desktop.cpp index 0516a1d..20bb760 100644 --- a/client/src/ui/desktop.cpp +++ b/client/src/ui/desktop.cpp @@ -5,6 +5,7 @@ #include #include +#include "gfx/types.h" #include "ui/editor.h" #include "desktop.h" #include @@ -146,14 +147,6 @@ void Desktop::handle_event(SDL_Event* event, int screen_width, int screen_height if(_focused_window) { _focused_window->handle_event(event, screen_width, screen_height, _taskbar->get_height()); - IWindowContent* content = _focused_window->get_content(); - if(content) { - /* - * Only terminal should receive direct text input this way. - * Editor gets it through the generic handle_event call. - */ - content->handle_input(event); - } } } @@ -202,22 +195,21 @@ UIWindow* Desktop::get_focused_window(void) { return _focused_window; } -void Desktop::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, - int screen_height, bool show_cursor) { - _render_wallpaper(txt_renderer); +void Desktop::render(const RenderContext& context) { + _render_wallpaper(context.txt_renderer); if(_launcher_is_open) { - _launcher->render(shape_renderer, txt_renderer, screen_height); + _launcher->render(context.shape_renderer, context.txt_renderer, context.screen_height); } - _taskbar->render(shape_renderer, txt_renderer, _windows, _focused_window); + _taskbar->render(context.shape_renderer, context.txt_renderer, _windows, _focused_window); /* Render non-focused windows first. */ for(const auto& win : _windows) { if(win.get() != _focused_window && !win->is_minimized()) { - win.get()->render(shape_renderer, txt_renderer, screen_height, show_cursor); + win.get()->render(context); } } /* Render focused window last so it's on top. */ if(_focused_window && !_focused_window->is_minimized()) { - _focused_window->render(shape_renderer, txt_renderer, screen_height, show_cursor); + _focused_window->render(context); } } diff --git a/client/src/ui/desktop.h b/client/src/ui/desktop.h index e01efb3..2ca2b40 100644 --- a/client/src/ui/desktop.h +++ b/client/src/ui/desktop.h @@ -5,8 +5,8 @@ #include #include "client_network.h" -#include "gfx/shape_renderer.h" #include "gfx/txt_renderer.h" +#include "gfx/types.h" #include "ui/ui_window.h" #include "ui/taskbar.h" #include "ui/launcher.h" @@ -28,8 +28,7 @@ public: void add_window(std::unique_ptr window); void handle_event(SDL_Event* event, int screen_width, int screen_height); void update(int screen_width, int screen_height); - void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height, - bool show_cursor); + void render(const RenderContext& context); UIWindow* get_focused_window(void); diff --git a/client/src/ui/editor.cpp b/client/src/ui/editor.cpp index aaa7f63..3beaea3 100644 --- a/client/src/ui/editor.cpp +++ b/client/src/ui/editor.cpp @@ -1,11 +1,28 @@ #include "editor.h" #include -#include "gfx/txt_renderer.h" +#include "gfx/types.h" #include "ui/window_action.h" Editor::Editor(void) - : _should_close(false), _pending_action({ActionType::NONE}) { + : _should_close(false), _pending_action({ActionType::NONE}), + _filename("untitled.txt") { _view = std::make_unique(&_buffer, true); + _menu_bar = std::make_unique(25); + _menu_bar->add_menu("File"); + _menu_bar->add_menu_item("File", "Save", [this]() { + _pending_action = {ActionType::WRITE_FILE, _filename, _buffer.get_text()}; + }); +} + +Editor::Editor(const std::string& filename) + : _should_close(false), _pending_action({ActionType::NONE}), + _filename(filename) { + _view = std::make_unique(&_buffer, true); + _menu_bar = std::make_unique(25); + _menu_bar->add_menu("File"); + _menu_bar->add_menu_item("File", "Save", [this]() { + _pending_action = {ActionType::WRITE_FILE, _filename, _buffer.get_text()}; + }); } Editor::~Editor(void) {} @@ -14,20 +31,28 @@ void Editor::update(void) { /* Nothing to do yet. */ } -void Editor::handle_input(SDL_Event* event) { +void Editor::handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y) { + if(!event) return; + + _menu_bar->handle_event(event, window_x, window_y, window_gl_y); + /* We don't care about the return val here. RET is just newline. */ if(event->type == SDL_EVENT_KEY_DOWN && event->key.key == SDLK_S && (event->key.mod & SDL_KMOD_CTRL)) { /* C-S pressed, create a save action. */ - _pending_action = { ActionType::WRITE_FILE, "test.txt", _buffer.get_text() }; + _pending_action = { ActionType::WRITE_FILE, _filename, _buffer.get_text() }; } else { _view->handle_event(event); } } -void Editor::render(TextRenderer* renderer, int x, int y, int width, int height, - bool show_cursor) { - _view->render(renderer, x, y, width, height, show_cursor); +void Editor::render(const RenderContext& context, int x, int y, int width, int height) { + int menu_bar_height = _menu_bar->get_height(); + int menu_bar_y_gl = y + height - menu_bar_height; + _menu_bar->render(context.shape_renderer, context.txt_renderer, x, menu_bar_y_gl, width); + + _view->render(context.txt_renderer, x, y, width, height - menu_bar_height, + context.show_cursor); } void Editor::scroll(int amount, int content_height) { @@ -47,3 +72,7 @@ WindowAction Editor::get_pending_action(void) { _pending_action.type = ActionType::NONE; /* Clear action. */ return action; } + +const std::string& Editor::get_filename(void) const { + return _filename; +} diff --git a/client/src/ui/editor.h b/client/src/ui/editor.h index 9851668..55247b4 100644 --- a/client/src/ui/editor.h +++ b/client/src/ui/editor.h @@ -2,29 +2,33 @@ #include -#include "gfx/txt_renderer.h" +#include "gfx/types.h" #include "i_window_content.h" #include "ui/text_buffer.h" #include "text_view.h" #include "ui/window_action.h" +#include "ui/menu_bar.h" class Editor : public IWindowContent { public: Editor(void); + Editor(const std::string& filename); ~Editor(void) override; void update(void) override; - void handle_input(SDL_Event* event) override; - void render(TextRenderer* renderer, int x, int y, int width, int height, - bool show_cursor) override; + void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y) override; + void render(const RenderContext& context, int x, int y, int width, int height) override; void scroll(int amount, int content_height) override; bool should_close(void) override; void set_buffer_content(const std::string& content); WindowAction get_pending_action(void) override; + const std::string& get_filename(void) const; private: TextBuffer _buffer; std::unique_ptr _view; + std::unique_ptr _menu_bar; bool _should_close; WindowAction _pending_action; + std::string _filename; }; diff --git a/client/src/ui/i_window_content.h b/client/src/ui/i_window_content.h index a6c4718..4fb02f6 100644 --- a/client/src/ui/i_window_content.h +++ b/client/src/ui/i_window_content.h @@ -3,16 +3,14 @@ #include #include "window_action.h" - -class TextRenderer; +#include "gfx/types.h" class IWindowContent { public: virtual ~IWindowContent(void) = default; virtual void update(void) = 0; - virtual void handle_input(SDL_Event* event) = 0; - virtual void render(TextRenderer* renderer, int x, int y, int width, int height, - bool show_cursor = 0) = 0; + virtual void handle_input(SDL_Event* event, int window_x, int window_y, int window_gl_y) = 0; + virtual void render(const RenderContext& context, int x, int y, int width, int height) = 0; virtual void scroll(int amount, int content_height) = 0; virtual bool should_close(void) = 0; virtual WindowAction get_pending_action() = 0; diff --git a/client/src/ui/menu_bar.cpp b/client/src/ui/menu_bar.cpp new file mode 100644 index 0000000..1ca890d --- /dev/null +++ b/client/src/ui/menu_bar.cpp @@ -0,0 +1,95 @@ +#include "menu_bar.h" +#include "gfx/shape_renderer.h" +#include "gfx/txt_renderer.h" +#include "gfx/types.h" + +MenuBar::MenuBar(int height) + : _height(height) {} + +MenuBar::~MenuBar(void) {} + +void MenuBar::add_menu(const std::string& label) { + _menus.push_back({label, {}, false}); +} + +void MenuBar::add_menu_item(const std::string& menu_label, const std::string& item_label, + std::function action) { + for(auto& menu : _menus) { + if(menu.label == menu_label) { + menu.items.push_back({item_label, action}); + return; + } + } +} + +int MenuBar::get_height(void) const { + return _height; +} + +void MenuBar::handle_event(SDL_Event* event, int window_x, int window_y, int window_gl_y) { + if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + int mouse_x = event->button.x; + int mouse_y = event->button.y; + + int menu_x = window_x; + for(size_t i = 0; i < _menus.size(); ++i) { + int menu_width = 60; /* Just hardcode this for now. */ + if(mouse_x >= menu_x && mouse_x <= menu_x + menu_width && + mouse_y >= window_y && mouse_y <= window_y + _height) { + + if(_open_menu_index == (int)i) { + _open_menu_index = -1; /* Close if open already. */ + } else { + _open_menu_index = i; + } + return; + } + menu_x += menu_width; + } + + if(_open_menu_index != -1) { + Menu& open_menu = _menus[_open_menu_index]; + int item_y = window_gl_y + _height; + int item_height = 30; + int dropdown_width = 150; + for(const auto& item: open_menu.items) { + if(mouse_x >= window_x && mouse_x <= window_x + dropdown_width && + (window_gl_y - mouse_y) >= item_y && (window_gl_y - mouse_y) <= item_y + item_height) { + item.action(); + _open_menu_index = -1; /* Close menu. */ + return; + } + item_y += item_height; + } + } + /* Close any open menu when clicked outside. */ + _open_menu_index = -1; + } +} + +void MenuBar::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, + int x, int y_gl, int width) { + const Color bg_color = {0.15f, 0.17f, 0.19f}; + const Color text_color = {0.9f, 0.9f, 0.9f}; + const Color hover_color = {0.3f, 0.32f, 0.34f}; + + shape_renderer->draw_rect(x, y_gl, width, _height, bg_color); + + int menu_x = x; + for(size_t i = 0; i < _menus.size(); ++i) { + int menu_width = 60; + txt_renderer->render_text(_menus[i].label.c_str(), menu_x+10, y_gl+8, 1.0f, text_color); + + if(_open_menu_index == (int)i) { + int item_gl_y = y_gl - 30; /* Draw items below the bar. */ + int item_height = 30; + int dropdown_width = 150; + for(const auto& item : _menus[i].items) { + shape_renderer->draw_rect(menu_x, item_gl_y, dropdown_width, item_height, bg_color); + txt_renderer->render_text(item.label.c_str(), menu_x+10, item_gl_y+8, 1.0f, text_color); + item_gl_y -= item_height; + } + } + menu_x += menu_width; + } +} diff --git a/client/src/ui/menu_bar.h b/client/src/ui/menu_bar.h new file mode 100644 index 0000000..30beaad --- /dev/null +++ b/client/src/ui/menu_bar.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#include "gfx/shape_renderer.h" +#include "gfx/txt_renderer.h" + +struct MenuItem { + std::string label; + std::function action; +}; + +struct Menu { + std::string label; + std::vector items; + bool is_open = false; +}; + +class MenuBar { +public: + MenuBar(int height); + ~MenuBar(void); + + void add_menu(const std::string& label); + void add_menu_item(const std::string& menu_label, const std::string& item_label, + std::function action); + + void handle_event(SDL_Event* event, int window_x, int window_y, int window_gl_y); + void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int x, + int y_gl, int width); + + int get_height(void) const; +private: + int _height; + std::vector _menus; + int _open_menu_index = -1; +}; diff --git a/client/src/ui/ui_window.cpp b/client/src/ui/ui_window.cpp index 5544b88..3c87eae 100644 --- a/client/src/ui/ui_window.cpp +++ b/client/src/ui/ui_window.cpp @@ -5,7 +5,6 @@ #include "gfx/txt_renderer.h" #include "gfx/types.h" #include "ui/i_window_content.h" -#include "ui/window_action.h" UIWindow::UIWindow(const char* title, int x, int y, int width, int height) : _title(title), @@ -78,8 +77,7 @@ bool UIWindow::is_point_inside(int x, int y) { y >= _y && y <= _y + _height); } -void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, - int screen_height, bool show_cursor) { +void UIWindow::render(const RenderContext& context) { int title_bar_height = 30; /* Define colours. */ @@ -90,22 +88,22 @@ void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, const Color resize_handle_color = { 0.9f, 0.9f, 0.9f }; /* Convert top-left coords to bottom-left for OpenGL. */ - int y_gl = screen_height - _y - _height; + int y_gl = context.screen_height - _y - _height; /* Draw main window frame/background. */ - shape_renderer->draw_rect(_x, y_gl, _width, _height, frame_color); + context.shape_renderer->draw_rect(_x, y_gl, _width, _height, frame_color); /* Draw title bar. */ if(_is_focused) { - shape_renderer->draw_rect(_x, y_gl+_height-title_bar_height, _width, title_bar_height, + context.shape_renderer->draw_rect(_x, y_gl+_height-title_bar_height, _width, title_bar_height, focused_title_bar_color); } else { - shape_renderer->draw_rect(_x, y_gl+_height-title_bar_height, _width, title_bar_height, + context.shape_renderer->draw_rect(_x, y_gl+_height-title_bar_height, _width, title_bar_height, title_bar_color); } /* Draw title text. */ - txt_renderer->render_text(_title.c_str(), _x+5, y_gl+_height-title_bar_height+8, + context.txt_renderer->render_text(_title.c_str(), _x+5, y_gl+_height-title_bar_height+8, 1.0f, title_text_color); /* Draw title bar buttons. */ @@ -113,7 +111,7 @@ void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int button_margin = 5; int x_offset = _x + _width - button_size - button_margin; for(const auto& button : _title_bar_buttons) { - shape_renderer->draw_rect(x_offset, y_gl + _height - title_bar_height + button_margin, + context.shape_renderer->draw_rect(x_offset, y_gl + _height - title_bar_height + button_margin, button_size, button_size, button.color); x_offset -= (button_size + button_margin); } @@ -121,13 +119,13 @@ void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, /* Draw Resize handle. */ int corner_x = _x + _width; int corner_y = y_gl; - shape_renderer->draw_triangle(corner_x, corner_y + 10, corner_x - 10, + context.shape_renderer->draw_triangle(corner_x, corner_y + 10, corner_x - 10, corner_y, corner_x, corner_y, resize_handle_color); if(_content) { int content_y_gl = y_gl; int content_height_gl = _height - title_bar_height; - _content->render(txt_renderer, _x, content_y_gl, _width, content_height_gl, show_cursor); + _content->render(context, _x, content_y_gl, _width, content_height_gl); } } @@ -209,5 +207,9 @@ void UIWindow::handle_event(SDL_Event* event, int screen_width, _content->scroll(-event->wheel.y, content_height_gl); } } + if(_content) { + int y_gl = screen_height - _y - _height; + _content->handle_input(event, _x, _y, y_gl); + } } diff --git a/client/src/ui/ui_window.h b/client/src/ui/ui_window.h index 0de0b8c..67b9d91 100644 --- a/client/src/ui/ui_window.h +++ b/client/src/ui/ui_window.h @@ -4,8 +4,7 @@ #include #include -#include "gfx/shape_renderer.h" -#include "gfx/txt_renderer.h" +#include "gfx/types.h" #include "i_window_content.h" enum class WindowState { @@ -31,8 +30,7 @@ public: ~UIWindow(void); void update(void); - void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height, - bool show_cursor); + void render(const RenderContext& context); void handle_event(SDL_Event* event, int screen_width, int screen_height, int taskbar_height); void minimize(void);