[Refactor] Implement batched text rendering.

Overhauls text rendering to improve performance.

Renderer was really ineffient, sending a separate GPU draw call for
every character rendered. This created a bottleneck, especially with
text-heavy UI stuff like the wallpaper.

- The 'TextRenderer' now generates a single texture atlas for all font
  glyhs on load so all characters share a single texture.
- 'begin()' / 'flush()' was added to the 'TextRenderer'. Calls to
  'render_text' now buffer vertex data (position, texture coords,
colour) instead of drawing immediately. A single 'flush()' call at the
end of a pass draws all buffered text in one draw call.
- A simple batching introduced some shitty visual layering bugs. to
  solve this, a staged flushing architecture has been implemented. Each
logical UI component is now responsible for managing its own rendering
passes, beginning and flushing text batches as needed to ensure correct
back-to-front layering.
This commit is contained in:
Ritchie Cunningham 2025-10-04 18:32:35 +01:00
parent 9d2a2f4195
commit c3316b3da1
15 changed files with 205 additions and 87 deletions

View File

@ -1,11 +1,11 @@
#version 330 core #version 330 core
in vec2 TexCoords; in vec2 TexCoords;
in vec3 ourColor;
out vec4 color; out vec4 color;
uniform sampler2D text; uniform sampler2D text;
uniform vec3 textColor;
void main() { void main() {
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
color = vec4(textColor, 1.0) * sampled; color = vec4(ourColor, 1.0) * sampled;
} }

View File

@ -1,11 +1,15 @@
#version 330 core #version 330 core
layout(location = 0) in vec4 vertex; layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;
layout (location = 2) in vec3 aColor;
out vec2 TexCoords; out vec2 TexCoords;
out vec3 ourColor;
uniform mat4 projection; uniform mat4 projection;
void main() { void main() {
gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); gl_Position = projection * vec4(aPos, 0.0, 1.0);
TexCoords = vertex.zw; TexCoords = aTexCoords;
ourColor = aColor;
} }

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include "gfx/types.h" #include "gfx/types.h"

View File

@ -17,24 +17,37 @@ TextRenderer::TextRenderer(unsigned int screen_width, unsigned int screen_height
_txt_shader->use(); _txt_shader->use();
_txt_shader->set_mat4("projection", _projecton); _txt_shader->set_mat4("projection", _projecton);
/* Configure VAO/VBO for texture coords. */ /* Configure VAO/VBO for batch rendering. */
glGenVertexArrays(1, &_vao); glGenVertexArrays(1, &_vao);
glGenBuffers(1, &_vbo); glGenBuffers(1, &_vbo);
glBindVertexArray(_vao); glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW); /* Pre-allocate buffer memory. */
glBufferData(GL_ARRAY_BUFFER, sizeof(TextVertex) * MAX_VERTICES, nullptr, GL_DYNAMIC_DRAW);
/* Position attribute. */
glEnableVertexAttribArray(0); glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)0);
/* Texture coord attribute. */
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, s));
/* Colour attribute. */
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void*)offsetof(TextVertex, r));
glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0); glBindVertexArray(0);
/* Pre-allocate vector capacity. */
_vertices.reserve(MAX_VERTICES);
} }
TextRenderer::~TextRenderer(void) { TextRenderer::~TextRenderer(void) {
delete _txt_shader; delete _txt_shader;
glDeleteTextures(1, &_atlas_texture_id);
} }
void TextRenderer::load_font(const char* font_path, unsigned int font_size) { void TextRenderer::load_font(const char* font_path, unsigned int font_size) {
_chars.clear();
FT_Library ft; FT_Library ft;
if(FT_Init_FreeType(&ft)) { if(FT_Init_FreeType(&ft)) {
printf("Could not init FreeType Library\n"); printf("Could not init FreeType Library\n");
@ -50,77 +63,120 @@ void TextRenderer::load_font(const char* font_path, unsigned int font_size) {
FT_Set_Pixel_Sizes(face, 0, font_size); FT_Set_Pixel_Sizes(face, 0, font_size);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
/* Calculate atlas dimensions. */
unsigned int atlas_width = 0;
unsigned int atlas_height = 0;
unsigned int max_height = 0;
for(unsigned char c = 0; c < 128; c++) { for(unsigned char c = 0; c < 128; c++) {
if(FT_Load_Char(face, c, FT_LOAD_RENDER)) { if(FT_Load_Char(face, c, FT_LOAD_RENDER)) {
printf("Failed to load Glyph for char %c\n", c); printf("Failed to load Glyph for char %c\n", c);
continue; continue;
} }
unsigned int texture; atlas_width += face->glyph->bitmap.width;
glGenTextures(1, &texture); max_height = std::max(max_height, face->glyph->bitmap.rows);
glBindTexture(GL_TEXTURE_2D, texture); }
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, face->glyph->bitmap.width,
face->glyph->bitmap.rows, 0, GL_RED, GL_UNSIGNED_BYTE, atlas_height = max_height;
face->glyph->bitmap.buffer);
/* Create the atlas texture. */
glGenTextures(1, &_atlas_texture_id);
glBindTexture(GL_TEXTURE_2D, _atlas_texture_id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlas_width, atlas_height, 0, GL_RED,
GL_UNSIGNED_BYTE, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
character ch = { /* Fill the atlas texture with data. */
texture, int x_offset = 0;
{ (int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows }, for(unsigned char c = 0; c < 128; c++) {
{ face->glyph->bitmap_left, face->glyph->bitmap_top }, if(FT_Load_Char(face, c, FT_LOAD_RENDER)) {
(unsigned int)face->glyph->advance.x continue;
};
_chars.insert(std::pair<char, character>(c, ch));
} }
/* Copy glyph bitmap to atlas. */
glTexSubImage2D(
GL_TEXTURE_2D, 0, x_offset, 0,
face->glyph->bitmap.width, face->glyph->bitmap.rows,
GL_RED, GL_UNSIGNED_BYTE, face->glyph->bitmap.buffer
);
/* Store character metrics and texture coords. */
_chars[c] = {
{(int)face->glyph->bitmap.width, (int)face->glyph->bitmap.rows },
{face->glyph->bitmap_left, face->glyph->bitmap_top},
(unsigned int)face->glyph->advance.x,
(float)x_offset / atlas_width,
0.0f,
(float)face->glyph->bitmap.width / atlas_width,
(float)face->glyph->bitmap.rows / atlas_height
};
x_offset += face->glyph->bitmap.width;
}
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
FT_Done_Face(face); FT_Done_Face(face);
FT_Done_FreeType(ft); FT_Done_FreeType(ft);
} }
void TextRenderer::render_text(const char* text, float x, float y, float scale, void TextRenderer::begin(void) {
const Color& color) { _vertices.clear();
}
void TextRenderer::flush(void) {
if(_vertices.empty()) {
return;
}
_txt_shader->use(); _txt_shader->use();
_txt_shader->set_vec3("textColor", color.r, color.g, color.b);
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _atlas_texture_id);
glBindVertexArray(_vao); glBindVertexArray(_vao);
for(const char* p = text; *p; p++) {
character ch = _chars[*p];
float xpos = x + ch.bearing[0] * scale;
float ypos = y - (ch.size[1] - ch.bearing[1]) * scale;
float w = ch.size[0] * scale;
float h = ch.size[1] * scale;
float vertices[6][4] = {
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos, ypos, 0.0f, 1.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos, ypos + h, 0.0f, 0.0f },
{ xpos + w, ypos, 1.0f, 1.0f },
{ xpos + w, ypos + h, 1.0f, 0.0f }
};
glBindTexture(GL_TEXTURE_2D, ch.texture_id);
glBindBuffer(GL_ARRAY_BUFFER, _vbo); glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); glBufferSubData(GL_ARRAY_BUFFER, 0, _vertices.size() * sizeof(TextVertex), _vertices.data());
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
x += (ch.advance >> 6) * scale;
}
glBindVertexArray(0); glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
void TextRenderer::render_text(const char* text, float x, float y, const Color& color) {
for(const char* p = text; *p; p++) {
unsigned char c = *p;
character ch = _chars[c];
float xpos = x + ch.bearing[0];
float ypos = y - (ch.size[1] - ch.bearing[1]);
float w = ch.size[0];
float h = ch.size[1];
_vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b});
_vertices.push_back({xpos, ypos, ch.tx, ch.ty+ch.th, color.r, color.g, color.b});
_vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b});
_vertices.push_back({xpos, ypos+h, ch.tx, ch.ty, color.r, color.g, color.b});
_vertices.push_back({xpos+w, ypos, ch.tx+ch.tw, ch.ty+ch.th, color.r, color.g, color.b});
_vertices.push_back({xpos+w, ypos+h, ch.tx+ch.tw, ch.ty, color.r, color.g, color.b});
x += (ch.advance >> 6);
}
}
float TextRenderer::get_text_width(const char* text, float scale) { float TextRenderer::get_text_width(const char* text, float scale) {
float width = 0.0f; float width = 0.0f;
for(const char* p = text; *p; p++) { for(const char* p = text; *p; p++) {
width += (_chars[*p].advance >> 6) * scale; unsigned char c = *p;
if(c < 128) {
width += (_chars[c].advance >> 6) * scale;
}
} }
return width; return width;
} }

View File

@ -1,33 +1,47 @@
#pragma once #pragma once
#include <ft2build.h> #include <ft2build.h>
#include <vector>
#include FT_FREETYPE_H #include FT_FREETYPE_H
#include <map>
#include "shader.h" #include "shader.h"
#include "types.h" #include "types.h"
struct TextVertex {
float x, y, s, t, r, g, b;
};
/* State of a single charactrer glyph. */ /* State of a single charactrer glyph. */
struct character { struct character {
unsigned int texture_id;
int size[2]; int size[2];
int bearing[2]; int bearing[2];
unsigned int advance; unsigned int advance;
float tx, ty; /* x and y offset of glyph in texture atlas. */
float tw, th; /* width and height of glyph in texture atlas. */
}; };
const int MAX_GLYPHS_PER_BATCH = 10000;
const int VERTICES_PER_GLYPH = 6;
const int MAX_VERTICES = MAX_GLYPHS_PER_BATCH * VERTICES_PER_GLYPH;
class TextRenderer { class TextRenderer {
public: public:
TextRenderer(unsigned int screen_width, unsigned int screen_height); TextRenderer(unsigned int screen_width, unsigned int screen_height);
~TextRenderer(void); ~TextRenderer(void);
void load_font(const char* font_path, unsigned int font_size); void load_font(const char* font_path, unsigned int font_size);
void render_text(const char* text, float x, float y, float scale, const Color& color); void render_text(const char* text, float x, float y, const Color& color);
float get_text_width(const char* text, float scale); float get_text_width(const char* text, float scale);
void begin(void);
void flush(void);
private: private:
Shader* _txt_shader; Shader* _txt_shader;
unsigned int _vao, _vbo; unsigned int _vao, _vbo;
std::map<char, character> _chars; unsigned int _atlas_texture_id;
character _chars[128];
std::vector<TextVertex> _vertices;
float _projecton[16]; float _projecton[16];
}; };

View File

@ -7,7 +7,6 @@
#include "terminal.h" #include "terminal.h"
#include "client_network.h" #include "client_network.h"
#include "gfx/txt_renderer.h"
#include "gfx/types.h" #include "gfx/types.h"
#include "ui/window_action.h" #include "ui/window_action.h"
@ -118,6 +117,8 @@ void Terminal::render(const RenderContext& context, int x, int y_screen, int y_g
float line_height = 20.0f; float line_height = 20.0f;
float padding = 5.0f; float padding = 5.0f;
context.ui_renderer->begin_text();
/* Enable scissor test to clip rendering to the window content area. */ /* Enable scissor test to clip rendering to the window content area. */
glEnable(GL_SCISSOR_TEST); glEnable(GL_SCISSOR_TEST);
glScissor(x, y_gl, width, height); glScissor(x, y_gl, width, height);
@ -144,4 +145,6 @@ void Terminal::render(const RenderContext& context, int x, int y_screen, int y_g
/* Disable scissor test. */ /* Disable scissor test. */
glDisable(GL_SCISSOR_TEST); glDisable(GL_SCISSOR_TEST);
context.ui_renderer->flush_text();
} }

View File

@ -196,21 +196,28 @@ UIWindow* Desktop::get_focused_window(void) {
} }
void Desktop::render(const RenderContext& context) { void Desktop::render(const RenderContext& context) {
/* Pass 1: Background. */
context.ui_renderer->begin_text();
_render_wallpaper(context.ui_renderer); _render_wallpaper(context.ui_renderer);
if(_launcher_is_open) { context.ui_renderer->flush_text();
_launcher->render(context.ui_renderer);
} /* Pass 2: Windows (back to front). */
_taskbar->render(context.ui_renderer, _windows, _focused_window);
/* Render non-focused windows first. */
for(const auto& win : _windows) { for(const auto& win : _windows) {
if(win.get() != _focused_window && !win->is_minimized()) { if(win.get() != _focused_window && !win->is_minimized()) {
win.get()->render(context); win.get()->render(context);
} }
} }
/* Render focused window last so it's on top. */
if(_focused_window && !_focused_window->is_minimized()) { if(_focused_window && !_focused_window->is_minimized()) {
_focused_window->render(context); _focused_window->render(context);
} }
/* Pass 3: Top-level static UI (taskbar, Launcher, etc.). */
context.ui_renderer->begin_text();
if(_launcher_is_open) {
_launcher->render(context.ui_renderer);
}
_taskbar->render(context.ui_renderer, _windows, _focused_window);
context.ui_renderer->flush_text();
} }
void Desktop::_render_wallpaper(UIRenderer* ui_renderer) { void Desktop::_render_wallpaper(UIRenderer* ui_renderer) {

View File

@ -52,8 +52,20 @@ void Editor::render(const RenderContext& context, int x, int y_screen, int y_gl,
int content_y = y_screen + menu_bar_height; int content_y = y_screen + menu_bar_height;
int content_height = height - menu_bar_height; int content_height = height - menu_bar_height;
/*
* Render editor in two passes to ensure the dropdown
* menu appears on top of the main text view.
*/
/* Pass 1: Main Content. */
context.ui_renderer->begin_text();
_menu_bar->render_bar(context.ui_renderer, x, y_screen, width);
_view->render(context.ui_renderer, x, content_y, width, content_height, context.show_cursor); _view->render(context.ui_renderer, x, content_y, width, content_height, context.show_cursor);
_menu_bar->render(context.ui_renderer, x, y_screen, width); context.ui_renderer->flush_text();
/* Pass 2: Dropdown Menu. */
context.ui_renderer->begin_text();
_menu_bar->render_dropdown(context.ui_renderer, x, y_screen, width);
context.ui_renderer->flush_text();
} }
void Editor::scroll(int amount, int content_height) { void Editor::scroll(int amount, int content_height) {

View File

@ -95,7 +95,7 @@ Screen MainMenu::update(void) {
} }
void MainMenu::render(UIRenderer* ui_renderer) { void MainMenu::render(UIRenderer* ui_renderer) {
_render_background(ui_renderer->get_text_renderer()); _render_background(ui_renderer);
/* Button colours. */ /* Button colours. */
const Color button_color = { 0.1f, 0.15f, 0.2f }; const Color button_color = { 0.1f, 0.15f, 0.2f };
@ -130,10 +130,9 @@ void MainMenu::_update_background(void) {
} }
} }
void MainMenu::_render_background(TextRenderer* txt_renderer) { void MainMenu::_render_background(UIRenderer* ui_renderer) {
const Color background_text_color = { 0.0f, 0.35f, 0.15f }; /* Dark green. */ const Color background_text_color = { 0.0f, 0.35f, 0.15f }; /* Dark green. */
for(const auto& line : _background_text) { for(const auto& line : _background_text) {
txt_renderer->render_text(line.text.c_str(), line.x, line.y, 1.0f, ui_renderer->render_text(line.text.c_str(), line.x, line.y, background_text_color);
background_text_color);
} }
} }

View File

@ -30,7 +30,7 @@ public:
private: private:
void _update_background(void); void _update_background(void);
void _render_background(TextRenderer* txdt_renderer); void _render_background(UIRenderer* ui_renderer);
/* For animated background. */ /* For animated background. */
struct ScrollingText { struct ScrollingText {

View File

@ -72,10 +72,9 @@ void MenuBar::handle_event(SDL_Event* event, int window_x, int window_y) {
} }
} }
void MenuBar::render(UIRenderer* ui_renderer, int x, int y, int width) { void MenuBar::render_bar(UIRenderer* ui_renderer, int x, int y, int width) {
const Color bg_color = {0.15f, 0.17f, 0.19f}; const Color bg_color = {0.15f, 0.17f, 0.19f};
const Color text_color = {0.9f, 0.9f, 0.9f}; const Color text_color = {0.9f, 0.9f, 0.9f};
const Color hover_color = {0.3f, 0.32f, 0.34f};
ui_renderer->draw_rect(x, y, width, _height, bg_color); ui_renderer->draw_rect(x, y, width, _height, bg_color);
@ -84,16 +83,23 @@ void MenuBar::render(UIRenderer* ui_renderer, int x, int y, int width) {
int menu_width = 60; int menu_width = 60;
ui_renderer->render_text(_menus[i].label.c_str(), menu_x+10, y+20, text_color); ui_renderer->render_text(_menus[i].label.c_str(), menu_x+10, y+20, text_color);
if(_open_menu_index == (int)i) { menu_x += menu_width;
int item_y = y + _height; /* Draw items below the bar. */ }
}
void MenuBar::render_dropdown(UIRenderer* ui_renderer, int x, int y, int width ) {
if(_open_menu_index == -1) return;
const Color bg_color = { 0.15f, 0.17f, 0.19f };
const Color text_color = { 0.9f, 0.9f, 0.9f };
int menu_x = x + (_open_menu_index*60);
int item_y = y + _height;
int item_height = 30; int item_height = 30;
int dropdown_width = 150; int dropdown_width = 150;
for(const auto& item : _menus[i].items) { for(const auto& item : _menus[_open_menu_index].items) {
ui_renderer->draw_rect(menu_x, item_y, dropdown_width, item_height, bg_color); ui_renderer->draw_rect(menu_x, item_y, dropdown_width, item_height, bg_color);
ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color); ui_renderer->render_text(item.label.c_str(), menu_x+10, item_y+20, text_color);
item_y += item_height; item_y += item_height;
} }
}
menu_x += menu_width;
}
} }

View File

@ -28,7 +28,8 @@ public:
std::function<void()> action); std::function<void()> action);
void handle_event(SDL_Event* event, int window_x, int window_y); void handle_event(SDL_Event* event, int window_x, int window_y);
void render(UIRenderer* ui_renderer, int x, int y, int width); void render_bar(UIRenderer* ui_renderer, int x, int y, int width);
void render_dropdown(UIRenderer* ui_renderer, int x, int y, int width);
int get_height(void) const; int get_height(void) const;
private: private:

View File

@ -25,7 +25,17 @@ void UIRenderer::render_text(const char* text, int x, int y, const Color& color)
if(!_txt_renderer) return; if(!_txt_renderer) return;
/* Convert the screen-space baseline y-coord to GL-space baseline y-coord. */ /* Convert the screen-space baseline y-coord to GL-space baseline y-coord. */
int y_gl = _screen_height - y; int y_gl = _screen_height - y;
_txt_renderer->render_text(text, x, y_gl, 1.0f, color); _txt_renderer->render_text(text, x, y_gl, color);
}
void UIRenderer::begin_text(void) {
if(!_txt_renderer) return;
_txt_renderer->begin();
}
void UIRenderer::flush_text(void) {
if(!_txt_renderer) return;
_txt_renderer->flush();
} }
TextRenderer* UIRenderer::get_text_renderer(void) { TextRenderer* UIRenderer::get_text_renderer(void) {

View File

@ -17,6 +17,9 @@ public:
void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, const Color& color); void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, const Color& color);
void render_text(const char* text, int x, int y, const Color& color); void render_text(const char* text, int x, int y, const Color& color);
void begin_text(void);
void flush_text(void);
/* Expose underlying text renderer for things like width calculation. */ /* Expose underlying text renderer for things like width calculation. */
TextRenderer* get_text_renderer(void); TextRenderer* get_text_renderer(void);

View File

@ -79,6 +79,8 @@ bool UIWindow::is_point_inside(int x, int y) {
void UIWindow::render(const RenderContext& context) { void UIWindow::render(const RenderContext& context) {
int title_bar_height = 30; int title_bar_height = 30;
context.ui_renderer->begin_text();
/* Define colours. */ /* Define colours. */
const Color frame_color = { 0.2f, 0.2f, 0.25f }; const Color frame_color = { 0.2f, 0.2f, 0.25f };
const Color title_bar_color = { 0.15f, 0.15f, 0.2f }; const Color title_bar_color = { 0.15f, 0.15f, 0.2f };
@ -115,6 +117,8 @@ void UIWindow::render(const RenderContext& context) {
context.ui_renderer->draw_triangle(corner_x, corner_y - 10, corner_x - 10, context.ui_renderer->draw_triangle(corner_x, corner_y - 10, corner_x - 10,
corner_y, corner_x, corner_y, resize_handle_color); corner_y, corner_x, corner_y, resize_handle_color);
context.ui_renderer->flush_text();
if(_content) { if(_content) {
int content_screen_y = _y + title_bar_height; int content_screen_y = _y + title_bar_height;
int content_height = _height - title_bar_height; int content_height = _height - title_bar_height;