[Add] Taskbar and window management work

- Adds a taskbar that displays and manages open application windows
- close, minimise and restore functionality
- resizeable windows
- Refactored desktop event handling to manage window focus and render
  order
This commit is contained in:
Ritchie Cunningham 2025-09-28 14:12:11 +01:00
parent 6876cbff95
commit ea25cb6cc7
13 changed files with 372 additions and 40 deletions

View File

@ -15,7 +15,7 @@
#include <ui/boot_sequence.h>
void GameState::_init_desktop(void) {
_desktop = std::make_unique<Desktop>();
_desktop = std::make_unique<Desktop>(_screen_width, _screen_height);
auto term = std::make_unique<Terminal>(_network.get());
auto term_window = std::make_unique<UIWindow>("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<ClientNetwork>();
@ -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;
}

View File

@ -34,6 +34,8 @@ private:
std::unique_ptr<BootSequence> _boot_sequence;
std::unique_ptr<MainMenu> _main_menu;
Screen _current_screen;
int _screen_width;
int _screen_height;
void _init_desktop(void);
void _run_server(void);

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -0,0 +1,39 @@
#include <cstdio>
#include "cursor_manager.h"
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_mouse.h>
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);
}

View File

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

View File

@ -1,4 +1,5 @@
#include <algorithm>
#include <ctime>
#include <memory>
#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 <SDL3/SDL_video.h>
#include <ui/cursor_manager.h>
#include "ui/ui_window.h"
Desktop::Desktop(void) {
Desktop::Desktop(int screen_width, int screen_height) {
_taskbar = std::make_unique<Taskbar>(screen_width, screen_height);
_focused_window = nullptr;
}
@ -24,7 +29,33 @@ void Desktop::add_window(std::unique_ptr<UIWindow> 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<UIWindow>& 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<UIWindow>& 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<UIWindow>& 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);
}
}
}

View File

@ -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<UIWindow> 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<std::unique_ptr<UIWindow>> _windows;
std::unique_ptr<Taskbar> _taskbar;
UIWindow* _focused_window;
};

68
client/src/ui/taskbar.cpp Normal file
View File

@ -0,0 +1,68 @@
#include "taskbar.h"
#include <memory>
#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<std::unique_ptr<UIWindow>>& 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<std::unique_ptr<UIWindow>>& 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;
}

28
client/src/ui/taskbar.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <SDL3/SDL_events.h>
#include <vector>
#include <memory>
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<std::unique_ptr<UIWindow>>& windows,
UIWindow* focused_window);
UIWindow* handle_event(SDL_Event* event, int screen_height,
const std::vector<std::unique_ptr<UIWindow>>& windows);
int get_height(void) const;
private:
int _width;
int _height;
int _y_pos;
};

View File

@ -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<Terminal> 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. */

View File

@ -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<Terminal> 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<Terminal> _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;
};