Autopositioning, custom fonts

This commit is contained in:
2025-11-08 12:27:38 +00:00
parent 9aae7b6173
commit 065f07bc58
4 changed files with 372 additions and 83 deletions

View File

@@ -8,21 +8,38 @@
namespace Quip {
int windowCount = 0;
bool ttfInitialized = false;
namespace Widget {
TextLabel::TextLabel() = default;
TextLabel::TextLabel(std::string text, int xpos, int ypos) : text(std::move(text)), xpos(xpos), ypos(ypos) {}
void TextLabel::render(const Window &window) const {
TextLabel::TextLabel(std::string text, Font font) : text(std::move(text)), font(std::move(font)) {}
int TextLabel::getVertSize() const {
SDL_Surface* surface = TTF_RenderText_Blended(font.getFont(), text.c_str(), 0, {255, 255, 255, 255});
if (!surface) {
return 20; // Default height if rendering fails
}
int height = surface->h;
SDL_DestroySurface(surface);
return height;
}
void TextLabel::render(Window &window, int customY) const {
constexpr SDL_Color color = {255, 255, 255, 255};
SDL_Surface* surface = TTF_RenderText_Blended(window.font, text.c_str(), 0, color);
SDL_Surface* surface = TTF_RenderText_Blended(font.getFont(), text.c_str(), 0, color);
if (!surface) {
return; // Skip rendering if font fails
}
SDL_Texture* texture = SDL_CreateTextureFromSurface(window.renderer, surface);
SDL_FRect destRect = {static_cast<float>(xpos), static_cast<float>(ypos), static_cast<float>(surface->w), static_cast<float>(surface->h)};
SDL_FRect destRect;
destRect = {static_cast<float>(10), static_cast<float>(customY), static_cast<float>(surface->w), static_cast<float>(surface->h)};
if (customY == -1) {
destRect.y += static_cast<float>(window.getYPos(this->getVertSize()));
}
SDL_RenderTexture(window.renderer, texture, nullptr, &destRect);
SDL_DestroyTexture(texture);
SDL_DestroySurface(surface);
}
Image::Image(const std::string& filepath, int xpos, int ypos, int width, int height) : width(width), height(height), xpos(xpos), ypos(ypos) {
Image::Image(const std::string& filepath, int width, int height) : width(width), height(height) {
surface = IMG_Load(filepath.c_str());
if (surface == nullptr) {
Window().error(4, "Couldn't load image " + filepath);
@@ -35,31 +52,97 @@ namespace Quip {
}
}
void Image::render(const Window &window) const {
void Image::render(Window &window) const {
SDL_Texture* texture = SDL_CreateTextureFromSurface(window.renderer, surface);
SDL_FRect destRect = {static_cast<float>(xpos), static_cast<float>(ypos), static_cast<float>(width), static_cast<float>(height)};
SDL_FRect destRect = {static_cast<float>(10), static_cast<float>(window.getYPos(height)), static_cast<float>(width), static_cast<float>(height)};
SDL_RenderTexture(window.renderer, texture, nullptr, &destRect);
SDL_DestroyTexture(texture);
}
Button::Button(std::string text, std::function<void()> callback, int xpos, int ypos) : text(std::move(text)), onCLick(std::move(callback)), xpos(xpos), ypos(ypos) {}
void Button::render(const Window &window) const {
SDL_SetRenderDrawColor(window.renderer, 40, 60, 80, 255); // Dark gray background
SDL_FRect backgroundRect = {static_cast<float>(xpos), static_cast<float>(ypos), static_cast<float>(width), static_cast<float>(height)};
Button::Button(TextLabel text, std::function<void()> callback) : text(std::move(text)), onCLick(std::move(callback)) {}
void Button::render(Window &window) {
SDL_SetRenderDrawColor(window.renderer, 40, 60, 80, 255);
const int renderYPos = window.getYPos(height);
xpos = 10;
ypos = renderYPos;
SDL_FRect backgroundRect = {static_cast<float>(10), static_cast<float>(renderYPos), static_cast<float>(width), static_cast<float>(height)};
SDL_RenderFillRect(window.renderer, &backgroundRect);
constexpr SDL_Color color = {255, 255, 255, 255};
SDL_Surface* surface = TTF_RenderText_Blended(window.font, text.c_str(), 0, color);
SDL_Texture* texture = SDL_CreateTextureFromSurface(window.renderer, surface);
SDL_FRect destRect = {static_cast<float>(xpos), static_cast<float>(ypos), static_cast<float>(surface->w), static_cast<float>(surface->h)};
SDL_RenderTexture(window.renderer, texture, nullptr, &destRect);
SDL_DestroyTexture(texture);
SDL_DestroySurface(surface);
text.render(window, renderYPos);
}
}
Font::Font() {
if (!ttfInitialized) {
if (!TTF_Init()) {
Font::error(2, "Couldn't initialize TTF");
}
ttfInitialized = true;
}
TTF_Font* rawFont = TTF_OpenFont("/usr/share/fonts/noto/NotoSans-Regular.ttf", 16);
if (!rawFont) {
std::cerr << "Failed to load default font: " << SDL_GetError() << std::endl;
Font::error(4, SDL_GetError());
return; // This won't execute if error throws
}
font = std::shared_ptr<TTF_Font>(rawFont, [](TTF_Font* f) {
if (f) TTF_CloseFont(f);
});
}
Font::Font(const std::string& path, const float ptsize) {
if (!ttfInitialized) {
if (!TTF_Init()) {
Font::error(2, "Couldn't initialize TTF");
}
ttfInitialized = true;
}
TTF_Font* rawFont = TTF_OpenFont(path.c_str(), ptsize);
if (!rawFont) {
std::cerr << "Failed to load font from " << path << ": " << SDL_GetError() << std::endl;
Font::error(2, SDL_GetError());
return;
}
font = std::shared_ptr<TTF_Font>(rawFont, [](TTF_Font* f) {
if (f) TTF_CloseFont(f);
});
}
void Font::setFont(const std::string& path, const float ptsize) {
TTF_Font* rawFont = TTF_OpenFont(path.c_str(), ptsize);
if (!rawFont) {
error(4, SDL_GetError());
}
font = std::shared_ptr<TTF_Font>(rawFont, TTF_CloseFont);
}
TTF_Font* Font::getFont() const {
return font.get();
}
void Font::error(int errCode, const std::string& context) {
std::cerr << "Quip::Font() error " << errCode << ": ";
switch (errCode) {
default:
case 0:
std::cerr << "Generic error (please raise an issue on Chookspace)" << std::endl;
break;
case 1:
std::cerr << "Font file is inaccessible or unable to be read" << std::endl;
break;
}
if (!context.empty()) {
std::cerr << "Context: " << context << std::endl;
}
switch (errCode) {
default:
case 0:
throw std::runtime_error("Quip::Font() generic error");
break;
case 1:
throw std::runtime_error("Quip::Font() file error");
}
}
Window::Window() = default;
Window::Window(std::string title, int width, int height, const std::function<void()>& onFrame) : title(std::move(title)), width(width), height(height), onFrame(onFrame) {
Window::Window(std::string title, const int width, const int height, const std::function<void()>& onFrame) : title(std::move(title)), width(width), height(height), onFrame(onFrame) {
windowCount ++;
if (width <= 0 || height <= 0) {
error(2, "width is " + std::to_string(width) + " and height is " + std::to_string(height));
@@ -72,16 +155,16 @@ namespace Quip {
if (!SDL_CreateWindowAndRenderer(title.c_str(), width, height, SDL_WINDOW_RESIZABLE, &window, &renderer)) {
error(1, SDL_GetError());
}
if (!TTF_Init()) {
error(1, SDL_GetError());
}
font = TTF_OpenFont("/usr/share/fonts/noto/NotoSans-Regular.ttf", 16);
if (!font) {
error(1, SDL_GetError());
}
font = Font("/usr/share/fonts/noto/NotoSans-Regular.ttf", 16);
}
void Window::error(int errCode, const std::string& context) const {
int Window::getYPos(const int vertsize) {
const int retval = nextYPos;
nextYPos += vertsize + 10;
return retval;
}
void Window::error(const int errCode, const std::string& context) const {
SDL_DestroyWindow(window);
SDL_DestroyRenderer(renderer);
SDL_Quit();
@@ -130,33 +213,36 @@ namespace Quip {
if (id.empty()) {
error(3, "Widget ID is empty");
}
widgets[id] = widget;
widgets.emplace_back(widget);
widgetIds.emplace(id, widgets.size() - 1);
}
Widget::Widget* Window::getChild(const std::string& id) {
if (widgets.find(id) == widgets.end()) {
if (widgetIds.find(id) == widgetIds.end()) {
error(3, "Cannot find widget with id " + id);
return nullptr;
}
return &widgets[id];
return &widgets[widgetIds[id]];
}
int Window::run() {
while (true) {
onFrame();
if (onFrame) {
onFrame();
}
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
goto cleanup;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
int mouseX = event.button.x;
int mouseY = event.button.y;
const int mouseX = event.button.x;
const int mouseY = event.button.y;
// Check if click is inside any button
for (auto& [id, widget] : widgets) {
for (auto& widget : widgets) {
if (std::holds_alternative<Widget::Button>(widget)) {
Widget::Button& button = std::get<Widget::Button>(widget);
auto& button = std::get<Widget::Button>(widget);
// Check if click is inside button bounds
if (mouseX >= button.xpos && mouseX <= button.xpos + button.width &&
mouseY >= button.ypos && mouseY <= button.ypos + button.height) {
@@ -167,11 +253,14 @@ namespace Quip {
}
}
}
default: ;
}
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
for (auto const& [id, widget] : widgets) {
nextXpos = 10;
nextYPos = 10;
for (auto &widget : widgets) {
if (std::holds_alternative<Widget::TextLabel>(widget)) {
std::get<Widget::TextLabel>(widget).render(*this);
} else if (std::holds_alternative<Widget::Button>(widget)) {
@@ -187,7 +276,6 @@ namespace Quip {
cleanup:
windowCount --;
if (windowCount == 0) {
TTF_CloseFont(font);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();

View File

@@ -2,60 +2,286 @@
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <SDL3_image/SDL_image.h>
#include <string>
#include <variant>
#include <functional>
#include <map>
#include <memory>
/**
* @brief Main namespace for the Quip GUI library.
*
* Quip provides a simple, easy-to-use GUI framework built on top of SDL3.
* It supports text labels, images, buttons, and window management.
*/
namespace Quip {
/** @brief Flag indicating whether TTF (TrueType Font) has been initialized. */
extern bool ttfInitialized;
class Window;
/**
* @brief Font class for managing TrueType fonts.
*
* Handles font loading, initialization, and provides access to the underlying
* SDL TTF font object. Automatically initializes TTF subsystem on first use.
*/
class Font {
std::shared_ptr<TTF_Font> font;
public:
/**
* @brief Sets the font from a file path.
* @param path Path to the TrueType font file.
* @param ptsize Point size for the font.
* @throws std::runtime_error if the font cannot be loaded.
*/
void setFont(const std::string& path, float ptsize);
/**
* @brief Gets the underlying SDL TTF font pointer.
* @return Pointer to the TTF_Font object, or nullptr if not initialized.
*/
[[nodiscard]] TTF_Font* getFont() const;
/**
* @brief Static error handler for font-related errors.
* @param errCode Error code (0 = generic, 1 = file error, 2 = TTF init error, 4 = file error).
* @param context Additional context string for the error.
* @throws std::runtime_error with appropriate error message.
*/
static void error(int errCode = 0, const std::string& context = "") ;
/**
* @brief Constructs a Font from a file path.
* @param path Path to the TrueType font file.
* @param ptsize Point size for the font.
* @throws std::runtime_error if the font cannot be loaded.
*/
Font(const std::string& path, float ptsize);
/**
* @brief Default constructor that loads the system default font.
*
* Attempts to load NotoSans-Regular from /usr/share/fonts/noto/NotoSans-Regular.ttf
* with a default size of 16 points.
* @throws std::runtime_error if the default font cannot be loaded.
*/
Font();
};
/** @brief Global counter tracking the number of active Window instances. */
extern int windowCount;
/**
* @brief Namespace containing widget classes for UI elements.
*/
namespace Widget {
/**
* @brief Text label widget for displaying text in a window.
*
* Renders text using a specified font. Text is positioned automatically
* based on the window's layout system, or can be positioned manually.
*/
class TextLabel {
public:
/** @brief The text content to display. */
std::string text;
int xpos = 0, ypos = 0;
void render(const Window &window) const;
explicit TextLabel(std::string text, int xpos, int ypos);
/** @brief The font to use for rendering the text. */
Font font;
/**
* @brief Renders the text label in the specified window.
* @param window Reference to the Window to render into.
* @param customY Custom Y position. If -1 (default), uses automatic positioning.
*/
void render(Window &window, int customY = -1) const;
/**
* @brief Gets the vertical size (height) of the rendered text.
* @return Height in pixels of the text when rendered.
*/
[[nodiscard]] int getVertSize() const;
/**
* @brief Constructs a TextLabel with specified text and font.
* @param text The text content to display.
* @param font The font to use for rendering.
*/
TextLabel(std::string text, Font font);
/**
* @brief Default constructor for TextLabel.
*
* Creates an empty text label with default font.
*/
TextLabel();
};
/**
* @brief Image widget for displaying image files in a window.
*
* Supports loading images from file paths and rendering them at
* specified or auto-calculated dimensions.
*/
class Image {
SDL_Surface* surface;
public:
/** @brief Width of the image in pixels. */
int width = 0, height = 0;
int xpos = 0, ypos = 0;
void render(const Window &window) const;
Image(const std::string& filepath, int xpos, int ypos, int width = -1, int height = -1);
/**
* @brief Renders the image in the specified window.
* @param window Reference to the Window to render into.
*/
void render(Window &window) const;
/**
* @brief Constructs an Image from a file path.
* @param filepath Path to the image file to load.
* @param width Desired width in pixels. If -1, uses image's natural width.
* @param height Desired height in pixels. If -1, uses image's natural height.
* @throws std::runtime_error if the image cannot be loaded.
*/
explicit Image(const std::string& filepath, int width = -1, int height = -1);
};
/**
* @brief Button widget for interactive clickable buttons.
*
* Displays a button with text and executes a callback function when clicked.
* Button dimensions and position can be customized.
*/
class Button {
public:
std::string text;
/** @brief Text label displayed on the button. */
TextLabel text;
/** @brief Callback function executed when the button is clicked. */
std::function<void()> onCLick;
int xpos = 0, ypos = 0;
/** @brief Width of the button in pixels. Default: 100. */
int width = 100, height = 30;
void render(const Window &window) const;
explicit Button(std::string text, std::function<void()> callback, int xpos, int ypos);
/** @brief X position of the button in pixels. Set automatically during rendering. */
int xpos = 0, ypos = 0;
/**
* @brief Renders the button in the specified window.
* @param window Reference to the Window to render into.
*/
void render(Window &window);
/**
* @brief Constructs a Button with text and callback.
* @param text TextLabel to display on the button.
* @param callback Function to call when the button is clicked.
*/
explicit Button(TextLabel text, std::function<void()> callback);
};
/**
* @brief Variant type that can hold any widget type.
*
* Used for storing heterogeneous collections of widgets in a Window.
*/
typedef std::variant<TextLabel, Button, Image> Widget;
}
/**
* @brief Window class representing a GUI window.
*
* Manages an SDL window, renderer, and collection of widgets. Handles
* event processing, rendering, and widget layout. Supports custom frame
* callbacks for per-frame logic.
*/
class Window {
public:
/** @brief Pointer to the underlying SDL window. */
SDL_Window* window;
/** @brief Pointer to the SDL renderer for drawing operations. */
SDL_Renderer* renderer;
/** @brief SDL event structure for processing window events. */
SDL_Event event;
TTF_Font* font;
/** @brief Default font used by the window. */
Font font;
/** @brief Window title displayed in the title bar. */
std::string title;
/** @brief Window width in pixels. */
int width, height;
std::map<std::string, Widget::Widget> widgets;
/** @brief Next X position for automatic widget layout. */
int nextXpos = 0, nextYPos = 0;
/** @brief Map of widget IDs to their indices in the widgets vector. */
std::map<std::string, int> widgetIds;
/** @brief Vector containing all widgets added to this window. */
std::vector<Widget::Widget> widgets;
/** @brief Optional callback function called each frame before rendering. */
std::function<void()> onFrame;
/**
* @brief Error handler for window-related errors.
* @param errCode Error code (0 = generic, 1 = SDL error, 2 = size error, 3 = widget name error, 4 = file error).
* @param context Additional context string for the error.
* @throws std::runtime_error with appropriate error message.
*/
void error(int errCode = 0, const std::string& context = "") const;
/**
* @brief Gets the next Y position for automatic widget layout.
* @param vertsize Vertical size of the widget to be positioned.
* @return Y position in pixels for the widget.
*/
int getYPos(int vertsize);
/**
* @brief Runs the window's main event loop.
*
* Processes events, renders widgets, and handles window updates.
* Blocks until the window is closed.
* @return 0 on successful completion.
*/
int run();
/**
* @brief Adds a widget to the window with a unique identifier.
* @param id Unique string identifier for the widget.
* @param widget The widget to add (TextLabel, Button, or Image).
* @throws std::runtime_error if the ID is empty or invalid.
*/
void addChild(const std::string& id, const Widget::Widget& widget);
/**
* @brief Retrieves a widget by its ID.
* @param id The unique identifier of the widget.
* @return Pointer to the widget, or nullptr if not found.
* @throws std::runtime_error if the widget ID is not found.
*/
Widget::Widget* getChild(const std::string& id);
/**
* @brief Constructs a Window with specified parameters.
* @param title Window title displayed in the title bar.
* @param width Window width in pixels. Default: 800.
* @param height Window height in pixels. Default: 600.
* @param onFrame Optional callback function called each frame. Default: nullptr.
* @throws std::runtime_error if window creation fails or dimensions are invalid.
*/
explicit Window(std::string title, int width = 800, int height = 600, const std::function<void()>& onFrame = nullptr);
/**
* @brief Default constructor for Window.
*
* Creates an uninitialized window. Must be properly initialized before use.
*/
Window();
};