diff --git a/client/src/game_state.cpp b/client/src/game_state.cpp index 1e80c9a..014909a 100644 --- a/client/src/game_state.cpp +++ b/client/src/game_state.cpp @@ -15,7 +15,7 @@ #include void GameState::_init_desktop(void) { - _desktop = std::make_unique(); + _desktop = std::make_unique(_screen_width, _screen_height); auto term = std::make_unique(_network.get()); auto term_window = std::make_unique("Terminal", 100, 100, 800, 500); @@ -40,11 +40,16 @@ void GameState::_run_server(void) { } } -GameState::GameState(void) : _current_screen(Screen::MAIN_MENU) {} +GameState::GameState(void) : + _current_screen(Screen::MAIN_MENU), + _screen_width(0), + _screen_height(0) {} GameState::~GameState(void) = default; void GameState::init(int screen_width, int screen_height) { + _screen_width = screen_width; + _screen_height = screen_height; /* Create and connect the network client. */ _network = std::make_unique(); @@ -77,7 +82,7 @@ void GameState::handle_event(SDL_Event* event) { break; case Screen::DESKTOP: if(_desktop) { - _desktop->handle_event(event); + _desktop->handle_event(event, _screen_height); } break; } diff --git a/client/src/game_state.h b/client/src/game_state.h index 702f80d..d45cc18 100644 --- a/client/src/game_state.h +++ b/client/src/game_state.h @@ -34,6 +34,8 @@ private: std::unique_ptr _boot_sequence; std::unique_ptr _main_menu; Screen _current_screen; + int _screen_width; + int _screen_height; void _init_desktop(void); void _run_server(void); diff --git a/client/src/gfx/shape_renderer.cpp b/client/src/gfx/shape_renderer.cpp index 3dbeb09..5928455 100644 --- a/client/src/gfx/shape_renderer.cpp +++ b/client/src/gfx/shape_renderer.cpp @@ -54,3 +54,25 @@ void ShapeRenderer::draw_rect(int x, int y, int width, int height, const Color& glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); } + +void ShapeRenderer::draw_triangle(int x1, int y1, int x2, int y2, int x3, + int y3, const Color& color) { + _shape_shader->use(); + _shape_shader->set_vec3("objectColor", color.r, color.g, color.b); + + float vertices[3][2] = { + {(float)x1, (float)y1}, + {(float)x2, (float)y2}, + {(float)x3, (float)y3}, + }; + + glBindVertexArray(_vao); + glBindBuffer(GL_ARRAY_BUFFER, _vbo); + /* + * We're overwriting the buffer content here, this is fine for just one triangle, + * but don't do it for everything ;) + */ + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLES, 0, 3); + glBindVertexArray(0); +} diff --git a/client/src/gfx/shape_renderer.h b/client/src/gfx/shape_renderer.h index 1cdfc3f..729bbb7 100644 --- a/client/src/gfx/shape_renderer.h +++ b/client/src/gfx/shape_renderer.h @@ -9,6 +9,8 @@ public: ~ShapeRenderer(void); void draw_rect(int x, int y, int width, int height, const Color& color); + void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, + const Color& color); private: Shader* _shape_shader; diff --git a/client/src/main.cpp b/client/src/main.cpp index ad9bb08..7879c7d 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 "ui/cursor_manager.h" const int SCREEN_WIDTH = 1280; const int SCREEN_HEIGHT = 720; @@ -66,6 +67,9 @@ int main(int argc, char** argv) { /* Listen for text input. */ SDL_StartTextInput(window); + /* Init cursor manager. */ + CursorManager::init(); + /* Init text renderer. */ TextRenderer* txt_render_instance = new TextRenderer(SCREEN_WIDTH, SCREEN_HEIGHT); txt_render_instance->load_font("assets/fonts/hack/Hack-Regular.ttf", 14); @@ -116,6 +120,7 @@ int main(int argc, char** argv) { delete shape_renderer_instance; delete txt_render_instance; SDL_GL_DestroyContext(context); + CursorManager::quit(); SDL_DestroyWindow(window); SDL_Quit(); diff --git a/client/src/ui/cursor_manager.cpp b/client/src/ui/cursor_manager.cpp new file mode 100644 index 0000000..882cf60 --- /dev/null +++ b/client/src/ui/cursor_manager.cpp @@ -0,0 +1,39 @@ +#include + +#include "cursor_manager.h" +#include +#include + +SDL_Cursor* CursorManager::_arrow_cursor = nullptr; +SDL_Cursor* CursorManager::_resize_cursor = nullptr; + +void CursorManager::init(void) { + _arrow_cursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); + _resize_cursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE); + + if(!_arrow_cursor || !_resize_cursor) { + printf("Failed to create system cursors! SDL_Error: %s\n", SDL_GetError()); + } +} + +void CursorManager::set_cursor(CursorType type) { + SDL_Cursor* cursor_to_set = _arrow_cursor; + switch(type) { + case CursorType::ARROW: + cursor_to_set = _arrow_cursor; + break; + case CursorType::RESIZE_NWSE: + cursor_to_set = _resize_cursor; + break; + } + + if(SDL_GetCursor() != cursor_to_set) { + SDL_SetCursor(cursor_to_set); + } +} + +void CursorManager::quit(void) { + SDL_DestroyCursor(_arrow_cursor); + SDL_DestroyCursor(_resize_cursor); +} + diff --git a/client/src/ui/cursor_manager.h b/client/src/ui/cursor_manager.h new file mode 100644 index 0000000..730f5fa --- /dev/null +++ b/client/src/ui/cursor_manager.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +enum class CursorType { + ARROW, + RESIZE_NWSE /* Diagonal resize arrow. */ +}; + +class CursorManager { +public: + static void init(void); + static void set_cursor(CursorType type); + static void quit(void); + +private: + static SDL_Cursor* _arrow_cursor; + static SDL_Cursor* _resize_cursor; +}; diff --git a/client/src/ui/desktop.cpp b/client/src/ui/desktop.cpp index eac83fc..947f547 100644 --- a/client/src/ui/desktop.cpp +++ b/client/src/ui/desktop.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "desktop.h" @@ -6,9 +7,13 @@ #include "gfx/shape_renderer.h" #include "gfx/txt_renderer.h" #include "terminal.h" +#include "ui/taskbar.h" +#include +#include #include "ui/ui_window.h" -Desktop::Desktop(void) { +Desktop::Desktop(int screen_width, int screen_height) { +_taskbar = std::make_unique(screen_width, screen_height); _focused_window = nullptr; } @@ -24,7 +29,33 @@ void Desktop::add_window(std::unique_ptr window) { } } -void Desktop::handle_event(SDL_Event* event) { +void Desktop::_set_focused_window(UIWindow* window) { + if(window == _focused_window) { + return; + } + + if(_focused_window) { + _focused_window->set_focused(false); + } + + _focused_window = window; + if(_focused_window) { + _focused_window->set_focused(true); + + /* Find window in vector and move it to the end. */ + auto it = std::find_if(_windows.begin(), _windows.end(), + [window](const std::unique_ptr& w) + { return w.get() == window; }); + + if(it != _windows.end()) { + auto window_to_move = std::move(*it); + _windows.erase(it); + _windows.push_back(std::move(window_to_move)); + } + } +} + +void Desktop::handle_event(SDL_Event* event, int screen_height) { if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { int mouse_x = event->button.x; int mouse_y = event->button.y; @@ -32,22 +63,35 @@ void Desktop::handle_event(SDL_Event* event) { /* Find the top-most window that was clicked. */ for(int i = _windows.size()-1; i >= 0; --i) { if(_windows[i].get()->is_point_inside(mouse_x, mouse_y)) { - /* If not focused, focus it. */ - if(_windows[i].get() != _focused_window) { - if(_focused_window) { - _focused_window->set_focused(false); - } - _focused_window = _windows[i].get(); - _focused_window->set_focused(true); - - /* Move window to the front. */ - auto window_to_move = std::move(_windows[i]); - _windows.erase(_windows.begin() + i); - _windows.push_back(std::move(window_to_move)); + if(!_windows[i]->is_minimized()) { + _set_focused_window(_windows[i].get()); } break; } } + } else if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) { + UIWindow* clicked_window = _taskbar->handle_event(event, screen_height, _windows); + if(clicked_window) { + if(clicked_window == _focused_window && !clicked_window->is_minimized()) { + clicked_window->minimize(); + _set_focused_window(nullptr); + } else { + clicked_window->restore(); + _set_focused_window(clicked_window); + } + } + } else if(event->type == SDL_EVENT_MOUSE_MOTION) { + bool on_resize_handle = false; + /* Iterate backwards since top-most windows are at the end. */ + for(int i = _windows.size() - 1; i >= 0; --i) { + if(_windows[i]->is_mouse_over_resize_handle(event->motion.x, + event->motion.y)) { + on_resize_handle = true; + break; + } + } + CursorManager::set_cursor(on_resize_handle ? CursorType::RESIZE_NWSE + : CursorType::ARROW); } if(_focused_window) { @@ -61,12 +105,10 @@ void Desktop::handle_event(SDL_Event* event) { void Desktop::update(void) { /* Remove closed windows. */ - _windows.erase(std::remove_if(_windows.begin(), _windows.end(), - [](const std::unique_ptr& window) { - Terminal* term = window->get_content(); - return term && term->close(); - }), - _windows.end()); + _windows.erase( + std::remove_if(_windows.begin(), _windows.end(), + [](const std::unique_ptr& w) { return w->should_close(); }), + _windows.end()); } UIWindow* Desktop::get_focused_window(void) { @@ -75,7 +117,11 @@ UIWindow* Desktop::get_focused_window(void) { void Desktop::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height, bool show_cursor) { + /* TODO: Render wallpaper here. */ + _taskbar->render(shape_renderer, txt_renderer, _windows, _focused_window); for(const auto& win : _windows) { - win.get()->render(shape_renderer, txt_renderer, screen_height, show_cursor); + if(!win->is_minimized()) { + win.get()->render(shape_renderer, txt_renderer, screen_height, show_cursor); + } } } diff --git a/client/src/ui/desktop.h b/client/src/ui/desktop.h index 3b8925f..de8c8f4 100644 --- a/client/src/ui/desktop.h +++ b/client/src/ui/desktop.h @@ -7,14 +7,15 @@ #include "gfx/shape_renderer.h" #include "gfx/txt_renderer.h" #include "ui/ui_window.h" +#include "ui/taskbar.h" class Desktop { public: - Desktop(void); + Desktop(int screen_width, int screen_height); ~Desktop(void); void add_window(std::unique_ptr window); - void handle_event(SDL_Event* event); + void handle_event(SDL_Event* event, int screen_height); void update(void); void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height, bool show_cursor); @@ -22,6 +23,8 @@ public: UIWindow* get_focused_window(void); private: + void _set_focused_window(UIWindow* window); std::vector> _windows; + std::unique_ptr _taskbar; UIWindow* _focused_window; }; diff --git a/client/src/ui/taskbar.cpp b/client/src/ui/taskbar.cpp new file mode 100644 index 0000000..9292d7f --- /dev/null +++ b/client/src/ui/taskbar.cpp @@ -0,0 +1,68 @@ +#include "taskbar.h" +#include +#include "gfx/shape_renderer.h" +#include "gfx/txt_renderer.h" +#include "gfx/types.h" +#include "ui/ui_window.h" + +Taskbar::Taskbar(int screen_width, int screen_height) { + _width = screen_width; + _height = 32; + _y_pos = 0; /* Taksbar at bottom because boring? */ +} + +Taskbar::~Taskbar(void) {} + +void Taskbar::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, + const std::vector>& windows, + UIWindow* focused_window) { + + const Color taskbar_color = { 0.1f, 0.12f, 0.14f }; + const Color button_color = { 0.2f, 0.22f, 0.24f }; + const Color button_focused_color = { 0.3f, 0.32f, 0.34f }; + const Color button_text_color = { 0.9f, 0.9f, 0.9f }; + + + shape_renderer->draw_rect(0, _y_pos, _width, _height, taskbar_color); + + int button_width = 150; + int padding = 5; + int x_offset = padding; + + for(const auto& window : windows) { + bool is_focused = (window.get() == focused_window); + shape_renderer->draw_rect(x_offset, _y_pos + padding, button_width, + _height - (padding * 2), + is_focused ? button_focused_color : button_color); + + /* TODO: Truncate text when too long. */ + txt_renderer->render_text(window->get_title().c_str(), x_offset + 10, + _y_pos + 11, 1.0f, button_text_color); + x_offset += button_width + padding; + } +} + +UIWindow* Taskbar::handle_event(SDL_Event* event, int screen_height, + const std::vector>& windows) { + if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) { + int mouse_x = event->button.x; + int mouse_y = screen_height - event->button.y; /* Convert to UI coords. */ + + if(mouse_y >= _y_pos && mouse_y <= _y_pos + _height) { + int button_width = 150; + int padding = 5; + int x_offset = padding; + for(auto& window : windows) { + if(mouse_x >= x_offset && mouse_x <= x_offset + button_width) { + return window.get(); /* Return clicked window. */ + } + x_offset += button_width + padding; + } + } + } + return nullptr; /* No window button was clicked. */ +} + +int Taskbar::get_height(void) const { + return _height; +} diff --git a/client/src/ui/taskbar.h b/client/src/ui/taskbar.h new file mode 100644 index 0000000..297d444 --- /dev/null +++ b/client/src/ui/taskbar.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +class UIWindow; +class ShapeRenderer; +class TextRenderer; + +class Taskbar { +public: + Taskbar(int screen_width, int screen_height); + ~Taskbar(void); + + void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, + const std::vector>& windows, + UIWindow* focused_window); + + UIWindow* handle_event(SDL_Event* event, int screen_height, + const std::vector>& windows); + + int get_height(void) const; +private: + int _width; + int _height; + int _y_pos; +}; diff --git a/client/src/ui/ui_window.cpp b/client/src/ui/ui_window.cpp index 794964b..e20dd37 100644 --- a/client/src/ui/ui_window.cpp +++ b/client/src/ui/ui_window.cpp @@ -5,20 +5,49 @@ #include "gfx/txt_renderer.h" #include "gfx/types.h" -UIWindow::UIWindow(const char* title, int x, int y, int width, int height) { - _title = title; - _x = x; - _y = y; - _width = width; - _height = height; - _content = nullptr; - _is_dragging = false; - _is_hovered = false; - _is_focused = false; /* Not focused by default? */ -} +UIWindow::UIWindow(const char* title, int x, int y, int width, int height) : + _title(title), + _x(x), + _y(y), + _width(width), + _height(height), + _content(nullptr), + _is_dragging(false), + _is_hovered(false), + _is_focused(false), + _should_close(false), + _state(WindowState::NORMAL), + _is_resizing(false), + _resize_margin(10) {} UIWindow::~UIWindow(void) {} +void UIWindow::minimize(void) { + _state = WindowState::MINIMIZED; +} + +void UIWindow::restore(void) { + _state = WindowState::NORMAL; +} + +bool UIWindow::is_minimized(void) const { + return _state == WindowState::MINIMIZED; +} + +const std::string& UIWindow::get_title(void) const { + return _title; +} + +bool UIWindow::is_mouse_over_resize_handle(int mouse_x, int mouse_y) const { + return (mouse_x >= _x + _width - _resize_margin && mouse_x <= _x + _width && + mouse_y >= _y + _height - _resize_margin && + mouse_y <= _y + _height); +} + +bool UIWindow::should_close(void) const { + return _should_close; +} + void UIWindow::set_content(std::unique_ptr term) { _content = std::move(term); } @@ -45,6 +74,9 @@ void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, const Color title_bar_color = { 0.15f, 0.15f, 0.2f }; const Color focused_title_bar_color = { 0.3f, 0.3f, 0.4f }; const Color title_text_color = { 0.9f, 0.9f, 0.9f }; + const Color close_button_color = { 0.8f, 0.2f, 0.2f }; + const Color minimize_button_color = { 0.8f, 0.8f, 0.2f }; + 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; @@ -65,6 +97,21 @@ void UIWindow::render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, txt_renderer->render_text(_title.c_str(), _x+5, y_gl+_height-title_bar_height+8, 1.0f, title_text_color); + /* Draw close button. */ + shape_renderer->draw_rect(_x + _width - 25, y_gl + _height - title_bar_height + 5, + 20, 20, close_button_color); + + /* Draw minimize button. */ + shape_renderer->draw_rect(_x + _width - 50, + y_gl + _height - title_bar_height + 5, 20, 20, + minimize_button_color); + + /* 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, + 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; @@ -79,19 +126,46 @@ void UIWindow::handle_event(SDL_Event* event) { int mouse_x = event->button.x; int mouse_y = event->button.y; - /* Is click within title bar? */ - if(mouse_x >= _x && mouse_x <= _x + _width && + /* Check for close button click. */ + int close_button_x = _x + _width - 25; + int close_button_y = _y + 5; + if(mouse_x >= close_button_x && mouse_x <= close_button_x + 20 && + mouse_y >= close_button_y && mouse_y <= close_button_y + 20) { + _should_close = true; + return; /* Stop processing this event. */ + } + + /* Check for minimize button click. */ + int minimize_button_x = _x + _width - 50; + int minimize_button_y = _y + 5; + if(mouse_x >= minimize_button_x && mouse_x <= minimize_button_x + 20 && + mouse_y >= minimize_button_y && mouse_y <= minimize_button_y + 20) { + minimize(); + return; /* Stop processing this event. */ + } + + /* Check for resize handle click (bottom-right corner). */ + if(is_mouse_over_resize_handle(mouse_x, mouse_y)) { + _is_resizing = true; + } else if(mouse_x >= _x && mouse_x <= _x + _width && mouse_y >= _y && mouse_y <= _y + title_bar_height) { + /* Is click within title bar? */ _is_dragging = true; _drag_offset_x = mouse_x - _x; _drag_offset_y = mouse_y - _y; } } else if(event->type == SDL_EVENT_MOUSE_BUTTON_UP) { _is_dragging = false; + _is_resizing = false; } else if(event->type == SDL_EVENT_MOUSE_MOTION) { if(_is_dragging) { _x = event->motion.x - _drag_offset_x; _y = event->motion.y - _drag_offset_y; + } else if(_is_resizing) { + int new_width = event->motion.x - _x; + int new_height = event->motion.y - _y; + _width = (new_width > 100) ? new_width : 100; /* Min width. */ + _height = (new_height > 80) ? new_height : 80; /* Min height. */ } /* Check if mouse hovered over window. */ diff --git a/client/src/ui/ui_window.h b/client/src/ui/ui_window.h index 0bb6c28..129cc4b 100644 --- a/client/src/ui/ui_window.h +++ b/client/src/ui/ui_window.h @@ -7,6 +7,12 @@ #include "gfx/txt_renderer.h" #include "terminal.h" +enum class WindowState { + NORMAL, + MINIMIZED, + MAXIMIZED +}; + class UIWindow { public: UIWindow(const char* title, int x, int y, int width, int height); @@ -15,19 +21,32 @@ public: void render(ShapeRenderer* shape_renderer, TextRenderer* txt_renderer, int screen_height, bool show_cursor); void handle_event(SDL_Event* event); + void minimize(void); + void restore(void); + bool is_minimized(void) const; + bool should_close(void) const; void set_focused(bool focused); bool is_point_inside(int x, int y); void set_content(std::unique_ptr term); Terminal* get_content(void); + bool is_mouse_over_resize_handle(int mouse_x, int mouse_y) const; + const std::string& get_title(void) const; private: + friend class Taskbar; /* Allow taskbar to access private members. */ int _x, _y, _width, _height; + Rect _pre_maximize_rect; std::string _title; std::unique_ptr _content; bool _is_focused; /* Managed by desktop. */ bool _is_hovered; /* Send scroll events even if not focused. */ + bool _should_close; + + WindowState _state; bool _is_dragging; int _drag_offset_x, _drag_offset_y; -}; + bool _is_resizing; + int _resize_margin; +};